dfs題目這樣去接題,秒殺leetcode題目

點個贊,看一看,好習慣!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收錄,這是我花了 3 個月總結的一線大廠 Java 面試總結,本人已拿大廠 offer。
另外,原創文章首發在我的個人博客:blog.ouyangsihai.cn,歡迎訪問。

今天來聊聊 dfs 的解題方法,這些方法都是總結之后的出來的經驗,有值得借鑒的地方。

1 從二叉樹看 dfs

二叉樹的思想其實很簡單,我們剛剛開始學習二叉樹的時候,在做二叉樹遍歷的時候是不是最常見的方法就是遞歸遍歷,其實,你會發現,二叉樹的題目的解題方法基本上都是遞歸來解題,我們只需要走一步,其他的由遞歸來做。

我們先來看一下二叉樹的前序遍歷、中序遍歷、后序遍歷的遞歸版本。

//前序遍歷
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);
}

我們在解題的時候,我們只需要去想當前的操作應該怎么實現,后面的由遞歸去實現,至于用前序中序還是后序遍歷,由具體的情況來實現。

下面來幾個二叉樹的熱身題,來體會一下這種解題方法。

另外,這些知識的話,我都寫了原創文章,比較系統的講解了,大家可以看看,會有一定得收獲的。

序號 原創精品
1 【原創】分布式架構系列文章
2 【原創】實戰 Activiti 工作流教程
3 【原創】深入理解Java虛擬機教程
4 【原創】Java8最新教程
5 【原創】MySQL的藝術世界

1. 如何把?叉樹所有的節點中的值加?

首先還是一樣,我們先寫出框架。

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);
}

發現是不是水到渠成?

不爽?再來一個簡單的。

2. 如何判斷兩棵?叉樹是不是同一棵二叉樹

這個問題我們直接考慮當前一步需要做什么,也就是什么情況,這是同一顆二叉樹?

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);
}

有了上面的講解,我相信你已經有了基本的思路了,下面我們來點有難度的題目,小試牛刀。

3. leetcode中等難度解析

114. 二叉樹展開為鏈表

這個題目是二叉樹中的中等難度題目,但是通過率很低,那么我們用上面的思路來看看是否可以輕松解決這個題目。

這個題目乍一看,根據前面的思路,你可以能首先會選擇前序遍歷的方式來解決,是可以的,但是,比較麻煩,因為前序遍歷的方式會改變右節點的指向,導致比較麻煩,那么,如果前序遍歷不行,就考慮中序和后序遍歷了,由于,在展開的時候,只需要去改變左右節點的指向,所以,這里其實最好的方式還是用后續遍歷,既然是后續遍歷,那么我們就可以快速的把后續遍歷的框架寫出來了。

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這種算法的思想。

105. 從前序與中序遍歷序列構造二叉樹

這個題目也是挺不錯的題目,而且其實在我們學習數據結構的時候,這個題目經常會以解答題的方式出現,讓我們考試的時候來做,確實印象深刻,這里,我們看看用代碼怎么解決。

還是同樣的套路,同樣的思路,已經同樣的味道,再來把這道菜炒一下。

首先,確定先序遍歷、中序遍歷還是后序遍歷,既然是由前序遍歷和中序遍歷來推出二叉樹,那么,前序遍歷是更好一些的。

這里我們直接考慮當前一步應該做什么,然后直接做出來這道菜。

當前一步:回想一下以前做這個題目的思路你會發現,我們去構造二叉樹的時候,思路是這樣的,前序遍歷第一個元素肯定是根節點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,這道菜就這么按照模板炒出來了,相信你,后面的菜你也會抄著炒的。

2 從leetcode的島嶼問題看dfs

1. 步步為營

這一類題目在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;
}

沒有爽夠?再來一題。

2. 再來一發

這個題目跟上面的那題很像,但是這里是求最大的一個島嶼的面積,由于每一個單元格的面積是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的操作,然后再求所有島嶼中的最大值。

我們看一下我們代碼的效率如何。

看起來是不是還不錯喲,對的,就是這么搞事情?。。?/p>

最后,這篇文章前前后后寫了快一周的時間把,不知道寫的怎么樣,但是,我盡力的把自己所想的表達清楚,主要是一種思路跟解題方法,肯定還有很多其他的方法,去LeetCode去看就明白了。

好了,寫的也夠久了,下篇文章再來看看其他的,希望對大家有幫助,再次再見!!

最后,再分享我歷時三個月總結的 Java 面試 + Java 后端技術學習指南,這是本人這幾年及春招的總結,已經拿到了大廠 offer,整理成了一本電子書,拿去不謝,目錄如下:

現在免費分享大家,在下面我的公眾號 程序員的技術圈子 回復 面試 即可獲取。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373