前言

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


 

九章算法系列(#3 Binary Tree & Divide Conquer)-课堂笔记的更多相关文章

  1. 九章算法系列(#5 Linked List)-课堂笔记

    前言 又是很长时间才回来发一篇博客,前一个月确实因为杂七杂八的事情影响了很多,现在还是到了大火燃眉毛的时候了,也应该开始继续整理一下算法的思路了.Linked List大家应该是特别熟悉不过的了,因为 ...

  2. 九章算法系列(#4 Dynamic Programming)-课堂笔记

    前言 时隔这么久才发了这篇早在三周前就应该发出来的课堂笔记,由于懒癌犯了,加上各种原因,实在是应该反思.好多课堂上老师说的重要的东西可能细节上有一些急记不住了,但是幸好做了一些笔记,还能够让自己回想起 ...

  3. 九章算法系列(#2 Binary Search)-课堂笔记

    前言 先说一些题外的东西吧.受到春跃大神的影响和启发,推荐了这个算法公开课给我,晚上睡觉前点开一看发现课还有两天要开始,本着要好好系统地学习一下算法,于是就爬起来拉上两个小伙伴组团报名了.今天听了第一 ...

  4. (lintcode全部题目解答之)九章算法之算法班题目全解(附容易犯的错误)

    --------------------------------------------------------------- 本文使用方法:所有题目,只需要把标题输入lintcode就能找到.主要是 ...

  5. 7九章算法强化班全解--------Hadoop跃爷Spark

    ------------------------------------------------------------第七周:Follow up question 1,寻找峰值 寻找峰值 描述 笔记 ...

  6. LeetCode算法题-Invert Binary Tree

    这是悦乐书的第194次更新,第199篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第55题(顺位题号是226).反转二叉树.例如: 输入: 4 / \ 2 7 / \ / ...

  7. 九章算法:BAT国内班 - 课程大纲

    第1章 国内笔试面试风格及准备方法 --- 分享面试经验,通过例题分析国内面试的风格及准备方法 · 1) C/C++部分: 实现 memcpy 函数 STL 中 vector 的实现原理 · 2)概率 ...

  8. LeetCode算法题-Balanced Binary Tree(Java实现)

    这是悦乐书的第167次更新,第169篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第26题(顺位题号是110).给定二叉树,判断它是否是高度平衡的.对于此问题,高度平衡二 ...

  9. 【九章算法免费讲座第一期】转专业找CS工作的“打狗棒法”

    讲座时间: 美西时间6月5日18:30-20:00(周五) 北京时间6月6日09:30-11:00(周六a.m) 讲座安排: 免费在线直播讲座 报名网址: http://t.cn/R2XgMSH,或猛 ...

随机推荐

  1. iOS自动打发布包-备用

    #!/bin/bash #  autoPublishH.sh#  ##  Created by 刘志托 liu on 12-2-8.#  Copyright (c) 2012年 null. All r ...

  2. cf E. Valera and Queries

    http://codeforces.com/contest/369/problem/E 题意:输入n,m; n 代表有多少个线段,m代表有多少个询问点集.每一个询问输出这些点的集合所占的线段的个数. ...

  3. QuickReport多页打印

    You use composite reports for this(TQrCompositeReport) on the quickreports tabTake a look in the Dem ...

  4. Linux企业级项目实践之网络爬虫(14)——使用正则表达式抽取HTML正文和URL

    正则表达式,又称正规表示法.常规表示法(英语:Regular Expression,在代码中常简写为regex.regexp或RE),计算机科学的一个概念.正则表达式使用单个字符串来描述.匹配一系列符 ...

  5. AIR检测网络

    package com.juyou.util.net { import flash.events.StatusEvent; import flash.net.URLRequest; import ai ...

  6. usaco5.5-Hidden Passwords

    最小表示法,感觉可以做成个模板,第一次RE是因为字符串长度变2倍了而我把数组开小了 Executing...   Test 1: TEST OK [0.008 secs, 3760 KB]   Tes ...

  7. Codeforce 217 div2

    C 假设每种颜色的个数都相同,可以用轮换的方式,让答案达到最大n,当不同的时候,可以每次从每种颜色中取出相同个数的手套来操作; 一直迭代下去直到只剩下1种颜色; 再将这一种颜色与之前交换过的交换就行了 ...

  8. n%i之和

    题目:http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1168 题意:给定一个n,注意这里n小于10^12,求 分析:早些时 ...

  9. 免费邮件服务器:MailEnable

    官方网站地址:www.mailenable.com 下载最新版的 Standard Edition (FREE) 安装之前请留意安装指引就可以了,安装上去之后,直接就可以使用了 安装指引上写的清清楚楚 ...

  10. 操作系统——IO缓存技术

    一.为什么引入缓存技术 为了解决cpu速度和外部设备速度不匹配的问题. 降低了io对cpu的中断的次数.每进行一次IO设备的时间都非常长,所以把数据先放入缓冲区,再进行IO操作. 二.缓冲技术的实现 ...