九章算法系列(#3 Binary Tree & Divide Conquer)-課堂筆記

前言node

第一天的算法都尚未緩過來,直接就進入了次日的算法學習。前一天一直在整理Binary Search的筆記,也沒有提早預習一下,好在Binary Tree算是本身最熟的地方了吧(LeetCode上面Binary Tree的題刷了4遍,目前95%以上可以Bug Free)因此還能跟得上,今天聽了一下,以爲學習到最多的,就是把Traverse和Divide Conquer分開來討論,以爲開啓了一片新的天地!今天寫這個博客我就儘可能把兩種方式都寫一寫吧。面試

 

Outline:算法

  • 二叉樹的遍歷
    • 前序遍歷traverse方法
    • 前序遍歷非遞歸方法
    • 前序遍歷分治法
  • 遍歷方法與分治法
    • Maximum Depth of Binary Tree
    • Balanced Binary Tree
    • 二叉樹的最大路徑和 (root->leaf)
    • Binary Tree Maximum Path Sum II (root->any)
    • Binary Tree Maximum Path Sum (any->any)
  • 二叉查找樹
    • Validate Binary Search Tree
    • Binary Search Tree Iterator
  • 二叉樹的寬度優先搜索
    • Binary Tree Level-Order Traversal

 

課堂筆記設計模式

 


1.二叉樹的遍歷ide

這個應該是二叉樹裏面最基本的題了,可是在面試過程當中,不必定會考遞歸的方式,頗有可能會讓你寫出非遞歸的方法,上課的時候老師也提到過,應該直接把非遞歸的方法背下來。這裏我就很少說了,直接把中序遍歷的兩種方法貼出來吧,最後再加入一個分治法(這也是第一次寫,感受很棒呢,都不須要太多的思考)。學習

1.1 前序遍歷traverse方法(Bug Free):優化

    vector<int> res;
    void helper(TreeNode* root) {
        if (!root) return;
        res.push_back(root->val);
        if (root->left) {
            helper(root->left);
        }
        if (root->right) {
            helper(root->right);
        }
    }
    vector<int> preorderTraversal(TreeNode *root) {
        if (!root) {
            return res;
        }
        helper(root);
        return res;
    }

1.2 前序遍歷非遞歸方法(Bug Free):this

    vector<int> preorderTraversal(TreeNode *root) {
        vector<int> res;
        if (!root) {
            return res;
        }
        stack<TreeNode*> s;
        s.push(root);
        while (!s.empty()) {
            TreeNode* tmp = s.top();
            s.pop();
            res.push_back(tmp->val);
            // 這裏注意:棧是先進後出,因此先push右子樹
            if (tmp->right) {
                s.push(tmp->right);
            }
            if (tmp->left) {
                s.push(tmp->left);
            }
        }
        return res;
    }

1.3 前序遍歷分治法(Java實現):spa

    vector<int> preorderTraversal(TreeNode *root) {
        vector<int> res;
        if (!root) {
            return res;
        }
        //Divide
        vector<int> left = preorderTraversal(root->left);
        vector<int> right = preorderTraversal(root->right);
        
        //Conquer
        res.push_back(root->val);
        res.insert(res.end(), left.begin(), left.end());
        res.insert(res.end(), right.begin(), right.end());
        return res;
    }

這三種方法也是比較直觀的,前兩個比較基礎,我就不詳細敘述了,可是分治法是值得重點說一說的。前面的遍歷的方法是須要對每個點進行判斷和處理的,根據DFS進入到每個節點,而後操做;可是使用分治法的話,就不須要考慮那麼多,分治法的核心思想就是把一個總體的問題分爲多個子問題來考慮,也就是說:每個子問題的操做方法都是同樣的,子問題的解是能夠合併爲原問題的解的(這裏就是和動態規劃、貪心法不同的地方)。因此使用分治法的話,就不須要對每一個節點都進行判斷,無論左右子樹的狀況(是否存在),直接進行求解,最後把它們合併起來。上課的時候老師也說過度治法就像一個女王大人,處於root的位置,而後派了兩位青蛙大臣去處理一些事物,女王大人只須要管好本身的val是多少,而後把兩個大臣的反饋直接加起來就能夠了。我的認爲分治法算是比較接近普通人思惟的一種方法了。設計

 


2. 遍歷方法與分治法

 遍歷方法其實在我通過以前各類刷題套模板後算是可以熟悉掌握了,所謂「雖不知其內涵,但知其模板」的境界,今天這個總結,確實幫助很多。直接承接了上面所說的兩種思考。接下來我就直接用題解來分析一下:

2.1 Maximum Depth of Binary Tree

http://www.lintcode.com/zh-cn/problem/maximum-depth-of-binary-tree/

給定一個二叉樹,找出其最大深度。

二叉樹的深度爲根節點到最遠葉子節點的距離。

樣例

給出一棵以下的二叉樹:

1
 / \ 
2   3
   / \
  4   5

這個二叉樹的最大深度爲3.

這個題目要是在面試的時候面到,那絕對能夠一分鐘內寫出來,由於若是考慮分治法的話,就是一個簡單的DFS,代碼以下(Bug Free):

    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = maxDepth(root.left) + 1;
        int right = maxDepth(root.right) + 1;
        
        return left > right ? left : right;
    }

就是遞歸查看左右兩邊最大的深度,而後返回就能夠。這個思路也比較簡單,我就很少說了。

接下來再來一個題目:

2.2 Balanced Binary Tree

http://www.lintcode.com/zh-cn/problem/balanced-binary-tree/

給定一個二叉樹,肯定它是高度平衡的。對於這個問題,一棵高度平衡的二叉樹的定義是:一棵二叉樹中每一個節點的兩個子樹的深度相差不會超過1。 

樣例

給出二叉樹 A={3,9,20,#,#,15,7}, B={3,#,20,15,7}

A)  3            B)    3 
   / \                  \
  9  20                 20
    /  \                / \
   15   7              15  7

二叉樹A是高度平衡的二叉樹,可是B不是

這個題目思路也比較簡單,判斷一下左右子樹的高度差是否小於1,也是一個簡單的分治法問題。由於課上用了一種Java的版原本寫,加入了一個ResultType類,這裏我也嘗試着寫了一下代碼(Bug Free):

class ResultType {
    public boolean isBalanced;
    public int MaxDepth;
    public ResultType(boolean isBalanced, int MaxDepth) {
        this.isBalanced = isBalanced;
        this.MaxDepth = MaxDepth;
    }
}
public class Solution {
    /**
     * @param root: The root of binary tree.
     * @return: True if this Binary tree is Balanced, or false.
     */
    public boolean isBalanced(TreeNode root) {
        return helper(root).isBalanced;
    }

    private ResultType helper(TreeNode root) {
        if (root == null) {
            return new ResultType(true, 0);
        }
        ResultType left = helper(root.left);
        ResultType right = helper(root.right);

        if (!left.isBalanced || !right.isBalanced) {
            return new ResultType(false, -1);
        }
        if (Math.abs(left.MaxDepth - right.MaxDepth) > 1) {
            return new ResultType(false, -1);
        }
        return new ResultType(true, Math.max(left.MaxDepth, right.MaxDepth) + 1);
    }
}

這裏的ResultType保存了一個布爾值判斷子樹是不是平衡二叉樹,用一個最大深度表示該子樹的最大深度。而後在Divide階段,分別遞歸調用了左右子樹,以後判斷左右子數的最大深度差,而且判斷它們是否知足平衡二叉樹,最後返回該子樹的最大深度。這個思考也是比較天然合理的。運用了這種調用類的方式來進行解答,很有一番面向對象的感受,可是本人是不太喜歡這種方式的,由於不容易思考,還須要考慮不少本身不熟悉的地方,容易出錯。

接下來就是本篇文章的重要部分了。我要詳細描述一下二叉樹的最大路徑這個問題,記得有一次面試還面到過這個題,我也要把不一樣的狀況寫出來。

先來最簡單的部分吧,給一棵二叉樹,找出從根節點出發到葉節點的路徑中,和最大的一條。這個就比較簡單了,直接遍歷整個樹,而後找到最大的路徑便可,這裏我就很少說了,比較簡單。直接上題目吧:

 

2.3 (1)二叉樹的最大路徑和(root->leaf)

給一棵二叉樹,找出從根節點出發到葉節點的路徑中,和最大的一條。

樣例

給出以下的二叉樹:

1
 / \
2   3

返回4。(最大的路徑爲1→3)

就不須要多解釋了,我就直接把代碼貼出來(Bug Free):

    public int maxPathSum2(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = maxPathSum2(root.left);
        int right = maxPathSum2(root.right);
        
        return root.val + Math.max(left, right);
    }

 

(2)二叉樹的最大路徑和(root->any)

2.4 Binary Tree Maximum Path Sum II

http://www.lintcode.com/zh-cn/problem/binary-tree-maximum-path-sum-ii/

給一棵二叉樹,找出從根節點出發的路徑中,和最大的一條。

這條路徑能夠在任何二叉樹中的節點結束,可是必須包含至少一個點(也就是根了)。

樣例

給出以下的二叉樹:

1
 / \
2   3

返回4。(最大的路徑爲1→3)

這個就跟原始版的題目不同了,這裏是從根到任意的節點,固然就不能採用原始問題的方法了,否則就是指數級別的複雜度了,這裏就採用分治法了:

咱們把分治的基本思想考慮進去:

1.遞歸的出口:當節點爲null

2.Divide:分別對左右進行遞歸

3.Conquer:把獲得的結果進行操做。

Java代碼以下(Bug Free):

    public int maxPathSum2(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = maxPathSum2(root.left);
        int right = maxPathSum2(root.right);
        
        return root.val + Math.max(0, Math.max(left, right));
    }

這裏有一個關鍵點,對於某一個節點來講,獲得了左右子樹的和,這裏我就要判斷是否加上子樹(這個部分就是和原始問題不同的地方,保證了是任意的節點),加上子樹的話是加左子樹仍是右子樹,而後就能獲得最大值了。這個題最大的關鍵仍是在於不考慮左右子樹如何,就把他們派出去,獲得結果之後再進行判斷。

 

(3)二叉樹中的最大路徑和(any->any)

2.5 Binary Tree Maximum Path Sum

http://www.lintcode.com/zh-cn/problem/binary-tree-maximum-path-sum/

給出一棵二叉樹,尋找一條路徑使其路徑和最大,路徑能夠在任一節點中開始和結束(路徑和爲兩個節點之間所在路徑上的節點權值之和)

樣例

給出一棵二叉樹:

     1
      / \
     2   3

返回 6

這個題是上一個題目的升級版,這裏求的就是任意兩個點的最大路徑和了。這樣的題其實就是從上面的題作了一個引伸,不過以前的題必須考慮到root,因此就直接判斷左右子樹,而這裏的話,就不須要考慮root了,因此問題就變成了一個「把每個節點都看成root來考慮的問題」,這裏是我本身的理解,可能我沒有表達清楚,也就是說,在每一步遞歸中,都須要把當前的root考慮爲上一題中的root,而後來判斷哪一個root獲得的值是最大的。因此這裏就須要增長一個全局變量來存儲了。代碼以下:

    int Max = INT_MIN;
    int helper(TreeNode *root) {
        if (!root) {
            return 0;
        }
        int tmp = root->val;
        //Divide
        int left = helper(root->left);
        int right = helper(root->right);
        
        //Conquer
        if (left > 0) {
            tmp += left;
        }
        if (right > 0) {
            tmp += right;
        }
        Max = max(Max, tmp);
        
        return max(0,max(left,right)) + root->val;
    }
    int maxPathSum(TreeNode *root) {
        int t = helper(root);
        return Max;
    }

這道題其實我在好久前的一次面試中就被問到過,當時面試官的描述就是比較奇怪,並無說any to any的問題,而是說任意一段路徑,可是不能有分叉。其實回過頭來思考,這個題也確實須要考慮這個問題:不能有分叉!若是容許分叉的話,那麼這個問題就沒有那麼簡單了。當時我就半天沒有寫出來,而此次在lintcode上能作到Bug Free,果真仍是一個徹底不擅於上戰場的人啊( ▼-▼ )。這個題關鍵就在於你要去判斷左右子樹的值是否會讓這一個小團的值變小,若是會,那就不加上左右子樹。最後的return也是一個關鍵的地方:由於不能有分叉,因此只返回一條路徑。

這兩個題目就是充分運用了分治的方法,還須要你們很深入的去理解一下其中的內涵,仍是有一些須要思考的地方。

 


3. 二叉查找樹

我的認爲在樹的題目中,最使人開心的就是二叉查找樹了,由於這種結構自己就帶有一種光環:左子樹小於root,右子樹大於root,這方面的題只須要牢牢圍繞這個概念來作就能夠。

直接上一個課上說過的題吧:

3.1 Validate Binary Search Tree

http://www.lintcode.com/zh-cn/problem/validate-binary-search-tree/

給定一個二叉樹,判斷它是不是合法的二叉查找樹(BST)

一棵BST定義爲:

  • 節點的左子樹中的值要嚴格小於該節點的值。
  • 節點的右子樹中的值要嚴格大於該節點的值。
  • 左右子樹也必須是二叉查找樹。
  • 一個節點的樹也是二叉查找樹。

樣例

一個例子:

2
 / \
1   4
   / \
  3   5

上述這棵二叉樹序列化爲 {2,1,4,#,#,3,5}.

看了這道題,個人第一個想法就是,判斷左邊最大的是否小於root,而後判斷右邊最小的是否大於root,而後遞歸去判斷。這個算法複雜度也比較高,最後仍是過了,能夠貼上來給你們看看:

    bool isValidBST(TreeNode *root) {
        if (!root) {
            return true;
        }
        if (root->left) {
            TreeNode *left = root->left;
            while (left->right) {
                left = left->right;
            }
            if (left->val >= root->val) {
                return false;
            }
        }
        if (root->right) {
            TreeNode *right = root->right;
            while (right->left) {
                right = right->left;
            }
            if (right->val <= root->val) {
                return false;
            }
        }
        return isValidBST(root->left)&&isValidBST(root->right);
    }

思路很簡單,就是找到左邊,而後找到最右的子樹,而後判斷root的val和它的關係,右子樹同理。以後遞歸往下進行判斷。

課上講過的另外一種方法就優化了不少,用一個全局變量來存儲前一個指針,而後和當前的root比較,而後更新這個指針,代碼以下(Bug Free):

    TreeNode *lastNode = NULL;
    bool isValidBST(TreeNode *root) {
        if (!root) {
            return true;
        }
        if (!isValidBST(root->left)) {
            return false;
        }
        if (lastNode && lastNode->val >= root->val) {
            return false;
        }
        lastNode = root;
        return isValidBST(root->right);
    }

這個方法比較直觀,就是利用二叉樹的中序遍歷的方法,其中last每次都更新爲當前的節點。

關於二叉查找樹還有一個簡單的設計類的題,我就很少說了,直接上題吧:

3.2 Binary Search Tree Iterator

http://www.lintcode.com/en/problem/binary-search-tree-iterator/

Design an iterator over a binary search tree with the following rules:

  • Elements are visited in ascending order (i.e. an in-order traversal)
  • next() and hasNext() queries run in O(1) time in average.

Example

For the following binary search tree, in-order traversal by using iterator is [1, 6, 10, 11, 12]

10
 /    \
1      11
 \       \
  6       12

我使用了隊列的方式來存儲二叉樹,而後進行相應的操做,代碼以下(Bug Free):

class BSTIterator {
private:
    queue<TreeNode*> res;
    void helper(TreeNode *root) {
        if (!root) {
            return;
        }
        helper(root->left);
        res.push(root);
        helper(root->right);
    }
public:
    //@param root: The root of binary tree.
    BSTIterator(TreeNode *root) {
        helper(root);
    }

    //@return: True if there has next node, or false
    bool hasNext() {
        return !res.empty();
    }
    
    //@return: return next node
    TreeNode* next() {
        TreeNode *tmp = res.front();
        res.pop();
        return tmp;
    }
};

 


 
4 . 二叉樹的寬度優先搜索
終於到了我最喜歡的環節,傳說中的BFS,這個環節比較經典,由於基本均可以套模板,不一樣的題只要加入一些不一樣的小trick就能夠作出來,好比拓撲排序、圖遍歷啊等等,都須要用到BFS。前段時間在作個人圖像中像素的最大連通域的時候也用到了BFS,感受比較常見,也相比於DFS的遞歸方法實現要容易思考。
4.1 Binary Tree Level-Order Traversal
http://www.lintcode.com/problem/binary-tree-level-order-traversal/
給出一棵二叉樹,返回其節點值的層次遍歷(逐層從左往右訪問)
樣例

給一棵二叉樹 {3,9,20,#,#,15,7}

3
 / \
9  20
  /  \
 15   7

返回他的分層遍歷結果:

[
  [3],
  [9,20],
  [15,7]
]
 
    
    vector<vector<int>> levelOrder(TreeNode *root) {
        vector<vector<int>> result;
        if (root == NULL) {
            return result;
        }
        
        queue<TreeNode *> Q;
        Q.push(root);
        while (!Q.empty()) {
            int size = Q.size();
            vector<int> level;
            //這裏須要注意的trick
            for (int i = 0; i < size; i++) {
                TreeNode *head = Q.front(); Q.pop();
                level.push_back(head->val);
                if (head->left != NULL) {
                    Q.push(head->left);
                }
                if (head->right != NULL) {
                    Q.push(head->right);
                }
            }
            
            result.push_back(level);
        }
        
        return result;
    }
老師的方法是判斷一下當前隊列的size,而後以此做爲分層的判斷,以後進行size次循環,表示一層。
個人方法(Bug Free):
    vector<vector<int>> levelOrder(TreeNode *root) {
        vector<vector<int>> res;
        vector<int> ans;
        if (!root) {
            return res;
        }
        queue<TreeNode *> q;
        q.push(root);
        //加入一個NULL指針做爲層分界
        q.push(NULL);
        while (!q.empty()) {
            TreeNode *tmp = q.front();
            q.pop();
            //到達分界點
            if (!tmp) {
                if (!q.empty()) {
                    res.push_back(ans);
                    ans.clear();
                    q.push(NULL);
                } else {
                    res.push_back(ans);
                    return res;
                }
            } else {
                ans.push_back(tmp->val);
                if (tmp->left) {
                    q.push(tmp->left);
                }
                if (tmp->right) {
                    q.push(tmp->right);
                }
            }
        }
        return res;
    }    

個人方法是在每層遍歷完以後加入一個NULL指針做爲分界的標準,當到達NULL的時候,判斷q是否爲空,不爲空則表示當前層已經遍歷結束,而後把當前層push_back到res中,而後清空;q爲空則表示到達最後一層,記錄答案而後返回便可。

 


 

總結

本文對二叉樹和分治法進行了一個闡述,其實就是把課堂上和麪試的一些想法拿到這裏來講了一下。在上課以前一直沒有想過太多關於traverse和分治有什麼太大的區別,反正就是遞歸,此次好好總結一下以爲有不少地方須要用到分治。我把之前寫的分治法的總結帖在下面吧:

1、概念

對於一個規模爲n的問題,若該問題能夠容易地解決(好比說規模n較小)則直接解決,不然將其分解爲k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞歸地解決這些子問題,而後將各子問題的解合併獲得原問題的解。這種算法設計策略叫作分治法。
 

 

2、分治法適用狀況
1)問題的規模縮小到必定程度就能夠容易解決
2)具備最子結構的性質(遞歸思想)
3)子問題的解能夠合併爲原問題的解(關鍵,不然爲貪心法或者動態規劃法)
4)子問題是相互獨立的 ,子問題之間不包含公共的子子問題(重複解公共的子問題,通常用動態規劃法比較好)

 

3、分治法的步驟
step1 分解:將原問題分解爲若干個規模較小,相互獨立,與原問題形式相同的子問題
step2 解決:子問題規模較小而容易被解決則直接解決,不然遞歸地解各個子問題
step3 合併:將各個子問題的解合併爲原問題的解

 

設計模式
Divide-and-Conquer(P)
     if |P|<=N0 then return (ADHOC(P))
     將P分解爲較小的字問題P1,P2,…,Pk
     for i<-1 to kß
          do Yi <- Divide-and-Conquer(Pi) 遞歸解決Pi
     T <- MERGE(Y1,Y2,…,Yk) 合併子問題
     return (T)
相關文章
相關標籤/搜索