點個贊,看一看,好習慣!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收錄,這是我花了 3 個月總結的一線大廠 Java 面試總結,本人已拿大廠 offer。
另外,原創文章首發在個人我的博客:blog.ouyangsihai.cn,歡迎訪問。html
今天來聊聊 dfs 的解題方法,這些方法都是總結以後的出來的經驗,有值得借鑑的地方。java
二叉樹的思想其實很簡單,咱們剛剛開始學習二叉樹的時候,在作二叉樹遍歷的時候是否是最多見的方法就是遞歸遍歷,其實,你會發現,二叉樹的題目的解題方法基本上都是遞歸來解題,咱們只須要走一步,其餘的由遞歸來作。mysql
咱們先來看一下二叉樹的前序遍歷、中序遍歷、後序遍歷的遞歸版本。git
//前序遍歷 void traverse(TreeNode root) { System.out.println(root.val); traverse(root.left); traverse(root.right); } //中序遍歷 void traverse(TreeNode root) { traverse(root.left); System.out.println(root.val); traverse(root.right); } //後續遍歷 void traverse(TreeNode root) { traverse(root.left); traverse(root.right); System.out.println(root.val); }
其實你會發現,二叉樹的遍歷的過程就可以看出二叉樹遍歷的一個總體的框架,其實這個也是二叉樹的解題的總體的框架就是下面這樣的。程序員
void traverse(TreeNode root) { //這裏將輸出變成其餘操做,咱們只完成第一步,後面的由遞歸來完成。 traverse(root.left); traverse(root.right); }
咱們在解題的時候,咱們只須要去想當前的操做應該怎麼實現,後面的由遞歸去實現,至於用前序中序仍是後序遍歷,由具體的狀況來實現。github
下面來幾個二叉樹的熱身題,來體會一下這種解題方法。面試
另外,這些知識的話,我都寫了原創文章,比較系統的講解了,你們能夠看看,會有必定得收穫的。算法
序號 | 原創精品 |
---|---|
1 | 【原創】分佈式架構系列文章 |
2 | 【原創】實戰 Activiti 工做流教程 |
3 | 【原創】深刻理解Java虛擬機教程 |
4 | 【原創】Java8最新教程 |
5 | 【原創】MySQL的藝術世界 |
首先仍是同樣,咱們先寫出框架。sql
void traverse(TreeNode root) { //這裏將輸出變成其餘操做,咱們只完成第一步,後面的由遞歸來完成。 traverse(root.left); traverse(root.right); }
接下來,考慮當前的一步須要作什麼事情,在這裏,固然是給當前的節點加一。後端
void traverse(TreeNode root) { if(root == null) { return; } //這裏改成給當前的節點加一。 root.val += 1; traverse(root.left); traverse(root.right); }
發現是否是水到渠成?
不爽?再來一個簡單的。
這個問題咱們直接考慮當前一步須要作什麼,也就是什麼狀況,這是同一顆二叉樹?
1)兩棵樹的當前節點等於空:root1 == null && root2 == null,這個時候返回 true。
2)兩棵樹的當前節點任意一個節點爲空:root1 == null || root2 == null,這個時候固然是 false。
3)兩棵樹的當前節點都不爲空,可是 val 不同:root1.val != root2.val,返回 false。
因此,答案就顯而易見了。
boolean isSameTree(TreeNode root1, TreeNode root2) { // 都爲空的話 if (root1 == null && root2 == null) return true; // ⼀個爲空,⼀個⾮空 if (root1 == null || root2 == null) return false; // 兩個都⾮空,但 val 不⼀樣 if (root1.val != root2.val) return false; // 遞歸去作 return isSameTree(root1.left, root2.left) && isSameTree(root1.right, root2.right); }
有了上面的講解,我相信你已經有了基本的思路了,下面咱們來點有難度的題目,小試牛刀。
這個題目是二叉樹中的中等難度題目,可是經過率很低,那麼咱們用上面的思路來看看是否能夠輕鬆解決這個題目。
這個題目乍一看,根據前面的思路,你能夠能首先會選擇前序遍歷的方式來解決,是能夠的,可是,比較麻煩,由於前序遍歷的方式會改變右節點的指向,致使比較麻煩,那麼,若是前序遍歷不行,就考慮中序和後序遍歷了,因爲,在展開的時候,只須要去改變左右節點的指向,因此,這裏其實最好的方式仍是用後續遍歷,既然是後續遍歷,那麼咱們就能夠快速的把後續遍歷的框架寫出來了。
public void flatten(TreeNode root) { if(root == null){ return; } flatten(root.left); flatten(root.right); //考慮當前一步作什麼 }
這樣,這個題目的基本思路就出來了,那麼,咱們只須要考慮當前一步須要作什麼就能夠把這個題目搞定了。
當前一步:因爲是後序遍歷,因此順序是左右中
,從展開的順序咱們能夠看出來,明顯是先鏈接左節點,後鏈接右節點,因此,咱們確定要先保存右節點的值,而後鏈接左節點,同時,咱們的展開以後,只有右節點,因此,左節點應該設置爲null。
通過分析,代碼直接就能夠寫出來了。
public void flatten(TreeNode root) { if(root == null){ return; } flatten(root.left); flatten(root.right); //考慮當前一步作什麼 TreeNode temp = root.right;// root.right = root.left;//右指針指向左節點 root.left = null;//左節點值爲空 while(root.right != null){ root = root.right; } root.right = temp;//最後再將右節點連在右指針後面 }
最終這就是答案了,這不是最佳的答案,可是,這多是解決二叉樹這種題目的最好的理解方式,同時,很是有助於你理解dfs這種算法的思想。
這個題目也是挺不錯的題目,並且其實在咱們學習數據結構的時候,這個題目常常會以解答題的方式出現,讓咱們考試的時候來作,確實印象深入,這裏,咱們看看用代碼怎麼解決。
仍是一樣的套路,一樣的思路,已經一樣的味道,再來把這道菜炒一下。
首先,肯定先序遍歷、中序遍歷仍是後序遍歷,既然是由前序遍歷和中序遍從來推出二叉樹,那麼,前序遍歷是更好一些的。
這裏咱們直接考慮當前一步應該作什麼,而後直接作出來這道菜。
當前一步:回想一下之前作這個題目的思路你會發現,咱們去構造二叉樹的時候,思路是這樣的,前序遍歷第一個元素確定是根節點a,那麼,當前前序遍歷的元素a,在中序遍歷中,在a這個元素的左邊就是左子樹的元素,在a這個元素右邊的元素就是左子樹的元素,這樣是否是就考慮清楚了當前一步,那麼咱們惟一要作的就是在中序遍歷數組中找到a這個元素的位置,其餘的遞歸來解決便可。
話很少說,看代碼。
public TreeNode buildTree(int[] preorder, int[] inorder) { //當前前序遍歷的第一個元素 int rootVal = preorder[0]; root = new TreeNode(); root.val = rootVal; //獲取在inorder中序遍歷數組中的位置 int index = 0; for(int i = 0; i < inorder.length; i++){ if(rootVal == inorder[i]){ index = i; } } //遞歸去作 }
這一步作好了,後面就是遞歸要作的事情了,讓計算機去工做吧。
public TreeNode buildTree(int[] preorder, int[] inorder) { //當前前序遍歷的第一個元素 int rootVal = preorder[0]; root = new TreeNode(); root.val = rootVal; //獲取在inorder中序遍歷數組中的位置 int index = 0; for(int i = 0; i < inorder.length; i++){ if(rootVal == inorder[i]){ index = i; } } //遞歸去作 root.left = buildTree(Arrays.copyOfRange(preorder,1,index+1),Arrays.copyOfRange(inorder,0,index)); root.right = buildTree(Arrays.copyOfRange(preorder,index+1,preorder.length),Arrays.copyOfRange(inorder,index+1,inorder.length)); return root; }
最後,再把邊界條件處理一下,防止root爲null的狀況出現。
TreeNode root = null; if(preorder.length == 0){ return root; }
ok,這道菜就這麼按照模板炒出來了,相信你,後面的菜你也會抄着炒的。
這一類題目在leetcode仍是很是多的,並且在筆試當中你都會常常遇到這種題目,因此,找到解決的方法很重要,其實,最後,你會發現,這類題目,你會了以後就是再也不以爲難的題目了。
咱們先來看一下題目哈。
題目的意思很簡單,有一個二維數組,裏面的數字都是0和1,0表明水域,1表明陸地,讓你計算的是陸地的數量,也就是島嶼的數量。
那麼這類題目怎麼去解決呢?
其實,咱們能夠從前面說的從二叉樹看dfs的問題來看這個問題,二叉樹的特徵很明顯,就是隻有兩個分支能夠選擇。
因此,就有了下面的遍歷模板。
//前序遍歷 void traverse(TreeNode root) { System.out.println(root.val); traverse(root.left); traverse(root.right); }
可是,迴歸到這個題目的時候,你會發現,咱們的整個數據結構是一張二維的圖,以下所示。
當你遍歷這張圖的時候,你會怎麼遍歷呢?是否是這樣子?
在(i,j)的位置,是否是能夠有四個方向都是能夠進行遍歷的,那麼是否是這個題目就有了新的解題思路了。
這樣咱們就能夠把這個的dfs模板代碼寫出來了。
void dfs(int[][] grid, int i, int j) { // 訪問上、下、左、右四個相鄰方向 dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); }
你會發現是否是和二叉樹的遍歷很像,只是多了兩個方向而已。
最後還有一個須要考慮的問題就是:base case,其實二叉樹也是須要討論一下base case的,可是,很簡單,當root == null
的時候,就是base case。
這裏的base case其實也不難,由於這個二維的圖是有邊界的,當dfs的時候發現超出了邊界,是否是就須要判斷了,因此,咱們再加上邊界條件。
void dfs(int[][] grid, int i, int j) { // 判斷 base case if (!inArea(grid, i, j)) { return; } // 若是這個格子不是島嶼,直接返回 if (grid[i][j] != 1) { return; } // 訪問上、下、左、右四個相鄰方向 dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); } // 判斷座標 (r, c) 是否在網格中 boolean inArea(int[][] grid, int i, int j) { return 0 <= i && i < grid.length && 0 <= j && j < grid[0].length; }
到這裏的話其實這個題目已經差很少完成了,可是,還有一點咱們須要注意,當咱們訪問了某個節點以後,是須要進行標記的,能夠用bool也能夠用其餘數字標記,否則可能會出現循環遞歸的狀況。
因此,最後的解題就出來了。
void dfs(int[][] grid, int i, int j) { // 判斷 base case if (!inArea(grid, i, j)) { return; } // 若是這個格子不是島嶼,直接返回 if (grid[i][j] != 1) { return; } //用2來標記已經遍歷過 grid[i][j] = 2; // 訪問上、下、左、右四個相鄰方向 dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); } // 判斷座標 (r, c) 是否在網格中 boolean inArea(int[][] grid, int i, int j) { return 0 <= i && i < grid.length && 0 <= j && j < grid[0].length; }
沒有爽夠?再來一題。
這個題目跟上面的那題很像,可是這裏是求最大的一個島嶼的面積,因爲每個單元格的面積是1,因此,最後的面積就是單元格的數量。
這個題目的解題方法跟上面的那個基本同樣,咱們把上面的代碼複製過去,改改就能夠了。
class Solution { public int maxAreaOfIsland(int[][] grid) { if(grid == null){ return 0; } int max = 0; for(int i = 0; i < grid.length; i++){ for(int j = 0; j < grid[0].length; j++){ if(grid[i][j] == 1){ max = Math.max(dfs(grid, i, j), max); } } } return max; } int dfs(int[][] grid, int i, int j) { // 判斷 base case if (!inArea(grid, i, j)) { return 0; } // 若是這個格子不是島嶼,直接返回 if (grid[i][j] != 1) { return 0; } //用2來標記已經遍歷過 grid[i][j] = 2; // 訪問上、下、左、右四個相鄰方向 return 1 + dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1); } // 判斷座標 (r, c) 是否在網格中 boolean inArea(int[][] grid, int i, int j) { return 0 <= i && i < grid.length && 0 <= j && j < grid[0].length; } }
基本思路: 每次進行dfs的時候都對島嶼數量進行+1的操做,而後再求全部島嶼中的最大值。
咱們看一下咱們代碼的效率如何。
看起來是否是還不錯喲,對的,就是這麼搞事情!!!
最後,這篇文章前先後後寫了快一週的時間把,不知道寫的怎麼樣,可是,我盡力的把本身所想的表達清楚,主要是一種思路跟解題方法,確定還有不少其餘的方法,去LeetCode去看就明白了。
好了,寫的也夠久了,下篇文章再來看看其餘的,但願對你們有幫助,再次再見!!
最後,再分享我歷時三個月總結的 Java 面試 + Java 後端技術學習指南,這是本人這幾年及春招的總結,已經拿到了大廠 offer,整理成了一本電子書,拿去不謝,目錄以下:
如今免費分享你們,在下面個人公衆號 程序員的技術圈子 回覆 面試 便可獲取。