LeetCode演算法練習——深度優先搜尋 DFS

weixin_34116110發表於2018-07-30

更多幹貨就在我的個人部落格 BlackBlog.tech 歡迎關注!
也可以關注我的csdn部落格:黑哥的部落格
謝謝大家!

網上大部分LeetCode的程式碼都沒有給出註釋和解釋,對於新手學習很不方便。筆者在這裡盡力給每一句程式碼都寫上註釋,每一道題最後都有解釋。

很久都沒有刷LeetCode了,上次LeetCode已經快是兩個月之前的事情了。現在繼續。之前中級演算法差不多刷完了,這次專練資料結構。這一篇主要是dfs題目,標識為簡單的題目一般就跳過了,主要刷中等與困難題。
LeetCode上分類為dfs的題目大多數與樹相關。
給個目錄:
LeetCode98 驗證二叉搜尋樹
LeetCode113 路徑總和 II
LeetCode105 從前序與中序遍歷序列構造二叉樹
LeetCode106 從中序與後序遍歷序列構造二叉樹
LeetCode129 求根到葉子節點數字之和
LeetCode124 二叉樹中的最大路徑和
LeetCode130 被圍繞的區域
LeetCode114 二叉樹展開為連結串列
LeetCode116 填充同一層的兄弟節點
LeetCode117 填充同一層的兄弟節點 II

最後還有一波福利,一個TreeLinkNode的建樹除錯程式碼,官方沒有給出,自己搞了一份方便大家除錯。

LeetCode98 驗證二叉搜尋樹

題目

給定一個二叉樹,判斷其是否是一個有效的二叉搜尋樹。

一個二叉搜尋樹具有如下特徵:

節點的左子樹只包含小於當前節點的數。
節點的右子樹只包含大於當前節點的數。
所有左子樹和右子樹自身必須也是二叉搜尋樹。

示例1:

輸入:
    2
   / \
  1   3
輸出: true

示例2:

輸入:
    5
   / \
  1   4
     / \
    3   6
輸出: false
解釋: 輸入為: [5,1,4,null,null,3,6]。
     根節點的值為 5 ,但是其右子節點值為 4 。

C++程式碼

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        return dfs(root,LONG_MAX,LONG_MIN);
    }

    bool dfs(TreeNode* root, long max,long min){
        if(!root) return true;
        if(root->val<=min || root->val>=max) return false;
        else return (dfs(root->left,root->val,min) && dfs(root->right,max,root->val));
    }
};

體會

這個題利用了二叉檢索樹自身的性質,左邊節點小於根節點,右邊節點大於根節點。初始化時帶入系統最大值和最小值,在遞迴過程中換成它們自己的節點值,用long代替int就是為了包括int的邊界條件。
如果這棵樹遍歷到了葉節點,則返回true。如果在遍歷的過程中出現了當前節點大於等於父節點(左子樹)或小於等於父節點(右子樹)則返回false。對結果做&&運算。返回最終的結果。

LeetCode113 路徑總和 II

題目

給定一個二叉樹和一個目標和,找到所有從根節點到葉子節點路徑總和等於給定目標和的路徑。

說明: 葉子節點是指沒有子節點的節點。

示例:
給定如下二叉樹,以及目標和 sum = 22,

              5
             / \
            4   8
           /   / \
          11  13  4
         /  \    / \
        7    2  5   1
返回:
[
   [5,4,11,2],
   [5,8,4,5]
]

C++程式碼

class Solution {
public:
    vector<vector<int>> res; //最終結果
    vector<int> mark; //每一個小的路徑和
    vector<vector<int>> pathSum(TreeNode* root, int sum) {
        dfs(root,sum,0);
        return res;
    }

    void dfs(TreeNode* root,int sum,int now){
        if(!root) return;//葉節點直接返回
        if(sum == now+root->val && root->left == NULL && root->right == NULL){ //路經總和滿足要求 且該節點是葉節點
            mark.push_back(root->val); //首先將當前節點新增進 mark
            res.push_back(mark); //將mark新增到res中
            mark.pop_back(); //將當前值從mark中彈出,因為節點結束
            return; //返回
        }
        else{
            int tmp = now + root->val; //如果路徑還不滿足 繼續遍歷 先左後右
            mark.push_back(root->val); //把當前節點的值放入mark
            dfs(root->left,sum,tmp);//遍歷左節點
            dfs(root->right,sum,tmp);//遍歷右節點
            mark.pop_back();//彈出當前節點
            return;
        }
    }

};

體會

思路比較清晰,用dfs遍歷整個樹,一旦遇到葉節點且資料總和滿足sum,則將這個路徑儲存到最終結果。中間使用mark記錄路徑,res儲存最終符合條件的路徑。注意記錄路徑的過程中,mark中的數字要pop。

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

題目

根據一棵樹的前序遍歷與中序遍歷構造二叉樹。

注意:
你可以假設樹中沒有重複的元素。

例如,給出

前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]

返回如下的二叉樹:

    3
   / \
  9  20
    /  \
   15   7

C++程式碼

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if(preorder.size()==0 && inorder.size()==0) return NULL; //前序後序都為空,直接return NULL
        return myBuildTree(preorder,inorder,0,0,inorder.size()-1);
    }

    TreeNode* myBuildTree(vector<int>& preorder, vector<int>& inorder,int prestart,int instart,int inend){
        //prestart代表當前子樹的根節點在preorder的位置,instart inend表示當前子樹在inorder中的開頭和結尾 
        if ( prestart>preorder.size()-1 || inend < instart) return NULL; //如果inend<instart 或者 prestart超過了preorder的限制 表示葉節點,直接return NULL
        TreeNode *root = new TreeNode(preorder[prestart]); //新建一個節點 root 當前子樹的根節點
        int inindex = 0; //用於標記根節點的值在前序中的位置
        //找inindex的位置
        for(int i=instart;i<=inend;i++){
            if(inorder[i] == preorder[prestart]){
                inindex = i;
                break;
            }
        }
        //左子樹,prestart後移動一位,instart不變,inend為inindex-1
        root->left  = myBuildTree(preorder,inorder,prestart+1,instart,inindex-1);
        //右子樹,prestart變為prestart+inindex-instart+1,也就是向後移動其在中序遍歷中的位置離開頭的距離後+1,inend不變,instart變為inindex+1
        root->right = myBuildTree(preorder,inorder,prestart+inindex-instart+1,inindex+1,inend);
        //將子樹的根節點返回
        return root;
    }
};

體會

使用先序遍歷與中序遍歷重建二叉樹。通過先序遍歷,我們很好確定根節點。通過根節點在中序遍歷中的位置,我們可以確定一個樹的左子樹,與右子樹。對左子樹,右子樹做遞迴操作,每次都計算出根節點,將根節點與其父節點連線即可。
本題的解法利用元素下標來確定子樹的先序和中序遍歷,也可以新建vector模擬這個過程,但是比較耗時。
唯一可能的難點就是右子樹建立時prestart的計算

LeetCode106 從中序與後序遍歷序列構造二叉樹

題目

根據一棵樹的中序遍歷與後序遍歷構造二叉樹。

注意:
你可以假設樹中沒有重複的元素。

例如,給出

中序遍歷 inorder = [9,3,15,20,7]
後序遍歷 postorder = [9,15,7,20,3]

返回如下的二叉樹:

    3
   / \
  9  20
    /  \
   15   7

C++程式碼

class Solution {
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if(inorder.size()==0 && postorder.size()==0) return NULL; //中序 後序都為空,則直接返回NULL
        return myBuildTree(inorder,postorder,postorder.size()-1,0,inorder.size()-1); //建樹
    }
    
    TreeNode* myBuildTree(vector<int>& inorder,vector<int>& postorder,int poststart,int instart,int inend){
        //poststart代表當前子樹的根節點在postorder的位置,instart inend表示當前子樹在inorder中的開頭和結尾 
        if(instart>inend || poststart<0) return NULL; //如果instart>inend 或者poststart<0 證明是葉節點,返回NULL
        TreeNode *root = new TreeNode(postorder[poststart]); //新建一個子樹的根節點
        int inindex=0;//表示根節點在inorder中的位置
        //尋找inindex的具體值
        for(int i=0;i<inorder.size();i++){
            if(inorder[i]==postorder[poststart]){
                inindex = i;
                break;
            }
        }
        //建立左子樹 poststart向前移動inend-inindex位後再向前移動一位,instart不變,inend變為inindex-1
        root->left = myBuildTree(inorder,postorder,poststart-1-(inend-inindex),instart,inindex-1);
        //建立右子樹 poststart向前移動1位,instart變為inindex+1, inend保持不變
        root->right = myBuildTree(inorder,postorder,poststart-1,inindex+1,inend);
        //將子樹的根節點返回
        return root;
    }
};

體會

整體流程與上一題類似

使用中序遍歷與後序遍歷重建二叉樹。通過後序遍歷,我們很好確定根節點。通過根節點在中序遍歷中的位置,我們可以確定一個樹的左子樹,與右子樹。對左子樹,右子樹做遞迴操作,每次都計算出根節點,將根節點與其父節點連線即可。
唯一可能的難點就是左子樹建立時prestart的計算

LeetCode129 求根到葉子節點數字之和

題目

給定一個二叉樹,它的每個結點都存放一個 0-9 的數字,每條從根到葉子節點的路徑都代表一個數字。

例如,從根到葉子節點路徑 1->2->3 代表數字 123。

計算從根到葉子節點生成的所有數字之和。

說明: 葉子節點是指沒有子節點的節點。

示例 1:

輸入: [1,2,3]
    1
   / \
  2   3
輸出: 25
解釋:
從根到葉子節點路徑 1->2 代表數字 12.
從根到葉子節點路徑 1->3 代表數字 13.
因此,數字總和 = 12 + 13 = 25.

示例 2:

輸入: [4,9,0,5,1]
    4
   / \
  9   0
 / \
5   1
輸出: 1026
解釋:
從根到葉子節點路徑 4->9->5 代表數字 495.
從根到葉子節點路徑 4->9->1 代表數字 491.
從根到葉子節點路徑 4->0 代表數字 40.
因此,數字總和 = 495 + 491 + 40 = 1026.

C++程式碼

class Solution {
public:
    vector<int> path; //記錄每次的路徑
    int sum_all = 0; //數字總和
    int sumNumbers(TreeNode* root) {
        dfs(root); //dfs開始
        return sum_all; //直接返回路徑和
    }
    
    void dfs(TreeNode* root){
        if(!root) return; //如果是空節點直接返回
        if(root->left==NULL && root->right ==NULL){ //如果是葉節點
            path.push_back(root->val); //現將val存入path
            sum_all += count(path); //計算這一個path的數字和
            path.pop_back(); //彈出value
        }
        else{ //如果不是葉節點
            path.push_back(root->val); //將當前節點存入path
            dfs(root->left); //遍歷左子樹
            dfs(root->right); //遍歷右子樹
            path.pop_back(); //左右子樹遍歷完成後 這個根節點用完了 彈出
            return; //終止遞迴
        }
    }
    int count(vector<int> vec){ //將vec轉換為數字
        int sum = 0;
        int power = 1;
        for(int i=vec.size()-1;i>=0;i--){
            sum += power*vec[i];
            power *=10;
        }
        return sum;
    }
};

體會

這個題目與113比較類似。
都是遍歷樹,記錄下來路徑。這個題遍歷到葉節點之後,將記錄的路徑轉換和數字,將所有路徑的數字進行求和,最終輸出整個樹的數字和。

LeetCode124 二叉樹中的最大路徑和

題目

給定一個非空二叉樹,返回其最大路徑和。

本題中,路徑被定義為一條從樹中任意節點出發,達到任意節點的序列。該路徑至少包含一個節點,且不一定經過根節點。

示例 1:

輸入: [1,2,3]

       1
      / \
     2   3

輸出: 6

示例 2:

輸入: [-10,9,20,null,null,15,7]

   -10
   / \
  9  20
    /  \
   15   7

輸出: 42

C++程式碼

class Solution {
public:
    int Max;
    int maxPathSum(TreeNode* root) {
        if(root == NULL) return 0; //根節點為空 直接返回
        Max = INT_MIN; //將MAX初始化為一個極小的值
        dfs(root); //dfs
        return Max; //返回最大值
    }
    
    int dfs(TreeNode* root){
        if(root==NULL) return 0; //節點為NULL 直接返回
        int left = dfs(root->left); //遍歷左子樹
        int right = dfs(root->right); //遍歷右子樹
        Max = max(Max,root->val+max(0,left)+max(0,right)); //計算當前的Max Max為左子樹最大路徑的值 + 右子樹最大路徑的值 + 根節點的值
        return max(0,max(left,right))+root->val; //返回 左子樹 或者 右子樹 中最大不分叉路徑的值。
    }
};

體會

這個題被劃分到了困難題,其實還是有一些巧妙的。
首先我們觀察一下題目中給的樣例,不能發現。這個題不能夠用之前我們寫的求根節點到任意葉節點的路徑和的思路去解答。因為最大值很可能是葉節點到葉節點的。
這個題我們需要設定一個MAX作為最後的結果,遍歷節點的左子樹和右子樹,將左子樹的最大不分叉路徑(大於0)+ 右子樹的最大不分叉路徑(大於0)+ 節點本身的值與當前最大路徑和Max作比較,將較大的數儲存於MAX。dfs返回的是左子樹或者右子樹最大不分叉路徑的值,最終結果返回MAX即可。
建議手動除錯一遍,理解更深入。

LeetCode130 被圍繞的區域

題目

給定一個二維的矩陣,包含 'X' 和 'O'(字母 O)。

找到所有被 'X' 圍繞的區域,並將這些區域裡所有的 'O' 用 'X' 填充。

示例:

X X X X
X O O X
X X O X
X O X X

執行你的函式後,矩陣變為:

X X X X
X X X X
X X X X
X O X X

解釋:

被圍繞的區間不會存在於邊界上,換句話說,任何邊界上的 'O' 都不會被填充為 'X'。 任何不在邊界上,或不與邊界上的 'O' 相連的 'O' 最終都會被填充為 'X'。如果兩個元素在水平或垂直方向相鄰,則稱它們是“相連”的。

C++程式碼

class Solution {
public:
    void solve(vector<vector<char>>& board) {
        if(board.size()==0) return ;
        //遍歷第一行與最後一行
        for(int i=0;i<board[0].size();i++){
            dfs(board,0,i);
            dfs(board,board.size()-1,i);
        }         
        //遍歷第一列與最後一列
        for(int i=0;i<board.size();i++){
            dfs(board,i,0);                
            dfs(board,i,board[0].size()-1);
        }
        //將所有標為A的地方填充為O,剩下的為X
        for(int i=0;i<board.size();i++){
            for(int j=0;j<board[0].size();j++){
                if(board[i][j]=='A') board[i][j]='O';
                else board[i][j]='X';
            }
        }
    }
    void dfs(vector<vector<char>>& board,int x,int y){
        //注意,先判定邊界,再進行訪問,不然可能會出現陣列越界的情況
        if(x<board.size() && x>=0 && y<board[0].size() &&y>=0 && board[x][y]=='O'){
            board[x][y]='A';
            dfs(board,x-1,y);
            dfs(board,x+1,y);
            dfs(board,x,y-1);
            dfs(board,x,y+1);
        }
        return;
    }
};

體會

看到這個題,我們首先會想到,遍歷陣列,對每一個O做連通區域的判斷。但是這樣做非常麻煩。我們可以轉換一下思路,遍歷邊緣的點,如果是O的話,將它所聯通的區域標為A。最後將所有為A的點標為O,剩下的點全是X。

LeetCode114 二叉樹展開為連結串列

題目

給定一個二叉樹,原地將它展開為連結串列。

例如,給定二叉樹

    1
   / \
  2   5
 / \   \
3   4   6

將其展開為:

1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6

C++程式碼

class Solution {
public:
    void flatten(TreeNode* root) {
        dfs(root);
        l2r_reverse(root);
    }
    void dfs(TreeNode* root){
        if(isLeaf(root) || root==NULL) return;
        else {
            dfs(root->left);
            dfs(root->right);
            move(root);
        }
    }
    //判斷一個節點是否是葉節點
    bool isLeaf(TreeNode* root){
        if(!root) return false;
        if(root->left==NULL && root->right==NULL) return true;
        else return false;
    }
    //將該節點右子樹的所有內容全部放到左子樹最後一個節點的下面
    void move(TreeNode* root){
        //如果有左子樹找 到左子樹的最後一個節點
        if(root->left!=NULL) {
            TreeNode *tmp = root->left;
            while (tmp->left) {
                tmp = tmp->left;
            }
            tmp->left = root->right;
            root->right = NULL;
        }
        else{ //如果左子樹不存在,直接將右子樹的變為左子樹
            root->left = root->right;
            root->right = NULL;
        }
    }
    //將單線的左子樹 完全 變為右子樹
    void l2r_reverse(TreeNode* root){
        if(!root)
            return;
        l2r_reverse(root->left);
        if(root->left!=NULL &&root->right==NULL){
            root->right = root->left;
            root->left = NULL;
            return;
        }
    }
};

體會

這個題比較有趣。我採用的方法是(雖然我感覺我搞複雜了)從下開始,將每一個父節點的右子樹放到左子樹最後一個節點下面。通過這個方法最後會得到一個向左偏的單連結串列,我們再將這個樹旋轉為向右偏,得到最後的連結串列。

LeetCode116 填充同一層的兄弟節點

題目

給定一個二叉樹

struct TreeLinkNode {
  TreeLinkNode *left;
  TreeLinkNode *right;
  TreeLinkNode *next;
}

填充它的每個 next 指標,讓這個指標指向其下一個右側節點。如果找不到下一個右側節點,則將 next 指標設定為 NULL。

初始狀態下,所有 next 指標都被設定為 NULL。

說明:

你只能使用額外常數空間。
使用遞迴解題也符合要求,本題中遞迴程式佔用的棧空間不算做額外的空間複雜度。
你可以假設它是一個完美二叉樹(即所有葉子節點都在同一層,每個父節點都有兩個子節點)。
示例:

給定完美二叉樹,

     1
   /  \
  2    3
 / \  / \
4  5  6  7

呼叫你的函式後,該完美二叉樹變為:

     1 -> NULL
   /  \
  2 -> 3 -> NULL
 / \  / \
4->5->6->7 -> NULL

C++程式碼

class Solution {
public:
    void connect(TreeLinkNode *root) {
        dfs(root);
    }
    void dfs(TreeLinkNode* root){
        if(!root) return;//如果節點為空就返回
        if(root->left && root->right){ //如果節點的左右節點都存在(題目已經說了都存在 不判斷也可以)
            root->left->next = root->right; //直接連線
            if(root->next) root->right->next = root->next->left; //這個是用來連線5和6的
            else root->right->next = NULL;//這個是用來連線7與NULL
        }
        dfs(root->left);//遞迴
        dfs(root->right);//遞迴
    }
};

體會

這個題目比較簡單,直接模擬這個過程就可以,程式碼也比較短。

LeetCode117 填充同一層的兄弟節點 II

題目

給定一個二叉樹

struct TreeLinkNode {
  TreeLinkNode *left;
  TreeLinkNode *right;
  TreeLinkNode *next;
}

填充它的每個 next 指標,讓這個指標指向其下一個右側節點。如果找不到下一個右側節點,則將 next 指標設定為 NULL。

初始狀態下,所有 next 指標都被設定為 NULL。

說明:

你只能使用額外常數空間。
使用遞迴解題也符合要求,本題中遞迴程式佔用的棧空間不算做額外的空間複雜度。
示例:

給定二叉樹,

     1
   /  \
  2    3
 / \    \
4   5    7

呼叫你的函式後,該二叉樹變為:

     1 -> NULL
   /  \
  2 -> 3 -> NULL
 / \    \
4-> 5 -> 7 -> NULL

C++程式碼

class Solution {
public:
    void connect(TreeLinkNode *root) {
        //start用於連線兩個層,其next為下一個層的首元素
        //tmp用來遍歷整個樹
        //tmp始終比root低一層
        TreeLinkNode * start = new TreeLinkNode(0);
        TreeLinkNode * tmp = start; //tmp指向start (很重要,用來連線下一層)
        while(root){
            //如果根節點存在左節點,tmp與根節點的左節點連線 修改tmp到同層下一個節點 對一個每一層第一個元素,這個步驟完成了start與下層第一個元素的連線
            if(root->left){
                tmp->next = root->left;
                tmp = tmp->next;
            }
            //如果根節點存在右節點,tmp與根節點的右節點連線,修改tmp到同層下一個節點
            if(root->right){
                tmp->next = root->right;
                tmp = tmp->next;
            }
            root = root->next;//root向同層下一個節點移動
            //如果root不存在,證明這一層遍歷完了,我們修改root 變為下一層第一個節點,修改start後為NULL,tmp重新指向start(很重要,用來連線下一層)
            if(!root){
                root = start->next;
                start->next = NULL;
                tmp = start;
            }
        }
    }
};

體會

這個題是116的通常形式。說實話,這個題寫了很久,最後還是錯了。看了網上的同解,覺得寫的很巧妙。網上的程式碼基本沒有註釋,我在這裡把註釋補全,方便大家。強烈建議自己手動除錯一遍。

附錄

對於116 117所給出的程式碼,LeetCode上面沒有給出除錯的環境,和所依賴的函式。
我根據之前建樹的程式碼寫了一份116 117,TreeLinkNode 的根據字串建樹的程式碼,下面放到這裡,大家自取。

#include <iostream>
#include <string>
#include <queue>
#include <vector>
#include <sstream>
using namespace std;
struct TreeLinkNode {
    int val;
    TreeLinkNode *left, *right, *next;
    TreeLinkNode(int x) : val(x), left(NULL), right(NULL), next(NULL) {}
};
void trimLeftTrailingSpaces(string &input) {
    input.erase(input.begin(), find_if(input.begin(), input.end(), [](int ch) {
        return !isspace(ch);
    }));
}

void trimRightTrailingSpaces(string &input) {
    input.erase(find_if(input.rbegin(), input.rend(), [](int ch) {
        return !isspace(ch);
    }).base(), input.end());
}

TreeLinkNode* stringToTreeNode(string input) {
    trimLeftTrailingSpaces(input);
    trimRightTrailingSpaces(input);
    input = input.substr(1, input.length() - 2);
    if (!input.size()) {
        return nullptr;
    }

    string item;
    stringstream ss;
    ss.str(input);

    getline(ss, item, ',');
    TreeLinkNode* root = new TreeLinkNode(stoi(item));
    queue<TreeLinkNode*> nodeQueue;
    nodeQueue.push(root);

    while (true) {
        TreeLinkNode* node = nodeQueue.front();
        nodeQueue.pop();

        if (!getline(ss, item, ',')) {
            break;
        }

        trimLeftTrailingSpaces(item);
        if (item != "null") {
            int leftNumber = stoi(item);
            node->left = new TreeLinkNode(leftNumber);
            nodeQueue.push(node->left);
        }


        if (!getline(ss, item, ',')) {
            break;
        }

        trimLeftTrailingSpaces(item);
        if (item != "null") {
            int rightNumber = stoi(item);
            node->right = new TreeLinkNode(rightNumber);
            nodeQueue.push(node->right);
        }
    }
    return root;
}

int main(){
    TreeLinkNode * root;
    root = stringToTreeNode("{2,1,3,0,7,9,1,2,null,1,0,null,null,8,8,null,null,null,null,7}");
}

相關文章