九章算法系列(#3 Binary Tree & Divide Conquer)-课堂笔记

前言

第一天的算法都还没有缓过来,直接就进入了第二天的算法学习。前一天一直在整理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.二叉树的遍历

这个应该是二叉树里面最基本的题了,但是在面试过程中,不一定会考递归的方式,很有可能会让你写出非递归的方法,上课的时候老师也提到过,应该直接把非递归的方法背下来。这里我就不多说了,直接把中序遍历的两种方法贴出来吧,最后再加入一个分治法(这也是第一次写,感觉很棒呢,都不需要太多的思考)。

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):

    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实现):

    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 ;
}
int left = maxDepth(root.left) + ;
int right = maxDepth(root.right) + ; 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, );
}
ResultType left = helper(root.left);
ResultType right = helper(root.right); if (!left.isBalanced || !right.isBalanced) {
return new ResultType(false, -);
}
if (Math.abs(left.MaxDepth - right.MaxDepth) > ) {
return new ResultType(false, -);
}
return new ResultType(true, Math.max(left.MaxDepth, right.MaxDepth) + );
}
}

这里的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 ;
}
int tmp = root->val;
//Divide
int left = helper(root->left);
int right = helper(root->right); //Conquer
if (left > ) {
tmp += left;
}
if (right > ) {
tmp += right;
}
Max = max(Max, tmp); return max(,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 = ; 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和分治有什么太大的区别,反正就是递归,这次好好总结一下觉得有很多地方需要用到分治。我把以前写的分治法的总结帖在下面吧:

一、概念

对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解决这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
 
二、分治法适用情况
1)问题的规模缩小到一定程度就可以容易解决
2)具有最子结构的性质(递归思想)
3)子问题的解可以合并为原问题的解(关键,否则为贪心法或者动态规划法)
4)子问题是相互独立的 ,子问题之间不包含公共的子子问题(重复解公共的子问题,一般用动态规划法比较好)
三、分治法的步骤
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)


 
上一篇:2016huasacm暑假集训训练四 递推_B


下一篇:2016huasacm暑假集训训练四 _排列