参考两篇其他bolg总结的二叉树:
https://github.com/xy7313/lintcode/blob/master/L3-BinaryTree/aboutTree.java
1. 树和分治法的关系
- 分治法:divide-conquer
- 算法题目中,很大部分的树题都可以用分治法的方法来解决
- 关于树的题目比如
- Traverse in Binary tree: Preorder/Inorder/Postorder
前序遍历(Pre-Order):根节点->左子树->右子树(NLR)
中序遍历(In-Order):左子树->根节点->右子树(LNR)
后序遍历(Post-Order):左子树->右子树->根节点(LRN)
leetcode-solution-java传送门(github)(解析在文件夹下readme中):
postorder
preorder
inorder
my gist 代码答案传送门(go):
preorder
inorder
DFS in Binary Tree(BFS : non-recursive)
Basic operations on Binary Tree: Insert/Remove/Find/Validate
解决这些树的搜索,遍历,操作问题的方法:
non-recursive : iteration,
recursive : traverse, 全局,递归开始状态是当前状态。但是全局变量有缺点,比如占内存;全局内可修改,易出错
preorder下递归三要素:
1. 递归的定义:把root为根的proorder放入result
2. 递归结束的时候,该root的树节点都加入了,当前为null了
3. 递归的拆解:放root。放左子树,放右子树
- recursive : divide conquer。分治,没有全局的变量,(自己本身就是递归的一部分)1. 分;2. 合
*. other diffs between traverse and divide-conquer:
1. result in parameter vs result in return value
2. top down vs bottom up
- basic code of preorder, inorder, postorder 包括4中讲的三种方法的实现
2. 复杂度分析
- tree problem time complexity:
二叉树通用时间复杂度计算公式 =(二叉树的节点个数n * 每个节点的处理时间)
比如divide conquer中都是if,处理时间就是O(1),总共就是O(n)
notice!! only complete binary tree is logn, others , n/h - 时间复杂度分析方法
上篇回顾: - binary search: 通过O(1)的时间,把规模为n的问题变为n/2
T(n)=T(n/2)+O(1)
=T(n/4)+O(1)+O(1)
…
=T(1)+logn*O(1)
==> O(logn)
- more: 思考:通过O(n)的时间,把规模为n的问题变为n/2?
T(n)=T(n/2)+O(n)
=T(n/4)+O(n)+O(n/2) 这里不能把O(n/2)约成O(n),之后再约
…
=T(1)+O(n+n/2+n/4+…)
=T(1)+O(2n)
*因为:1+2+4+8+…+n
=2+2+4+…+n-1
=2n
==>O(n)
本节两个延伸问题:
- 通过O(n)的时间,把n的问题,变为了两个n/2的问题,复杂度是多少? (比如:quick sort, merge sort)
T(n)=2 * T(n/2) + O(n)
=2 * (2T(n/4) + O(n/2)) + O(n)
=4 * T(n/4) + 2 * O(n/2) + O(n)
=4 * (2T(n/8) + O(n/4)) + 2 * O(n/2) + O(n)
=8 * T(n/8) + 4 * O(n/4) + 2 * O(n/2) + O(n)
...
=2^k * T(n/2^k)+k * O(n)
k=logn
==>O(nlong) + n * T(1)
=O(nlong)
- 通过O(1)的时间,把n的问题,变成了两个n/2的问题,复杂度是多少?
T(n)=2 * T(n/2) + O(n)
=2 * (2T(n/4) + O(n/2)) + O(1)
=4 * T(n/4) + 2 * O(1) + O(1)
=4 * (2T(n/8) + O(1)) + 2 * O(1) + O(1)
=8 * T(n/8) + 4 * O(1) + 2 * O(1) + O(1)
...
=2^k * T(n/2^k)+(1+2+4+...+2^k) * O(1)
k=logn(下面1+2+4+...上次证明过了)
==>(1+2+4+...+n) * O(1) + n * T(1)
=O(n)+n * T(1)
=O(n)
这个问题也可以通过画树的方式来看,得到第一层树的时候做了O(1)的工作,得到两个n/2,下一步是做2 * O(1)的工作,得到4个n/4,... 最后还是(1+2+4+...+n) * O(1)得到最终结果O(1)
3. 解法
- 分析整棵树在该问题上的结果 和左右儿子在该问题上的结果之间的联系是什么
- Result类:一般在divide-conquer方法中需要返回一堆东西时使用:class ResultType { int var1, var2; }
- recursive
递归的基本思想是广义地把规模大的问题转化为规模小的相似的子问题或者相似的子问题集合来解决。广义针对规模的,规模的缩小具体可以是指递归函数的参数,也可以是其参数之一。相似是指解决大问题的方法和解决小问题的方法往往是同一个方法,还可以是指解决子问题集的各子问题的方法是同一个方法。解决大问题的方法可以是由解决次规模问题的方法和解决剩余部分的方法组成,也可以是由一系列解决次规模问题的方法组成--http://zisong.me/post/suan-fa/ren-nao-li-jie-di-gui
4. 分类题目总结
- 比较简单的例子,用divide-conquer和traverse都可以的:
- 题一:Binary Tree Paths:这个是两种方法实现:两种方法都是递归,所以都要先判断root==null和左右节点==null的情况,做适合的返回
- divide-conquer: 类似于先找左右子树的path,然后和root分别组成path,所以上面说的左右节点==null的情况要把当前节点加入,左右子树的path也通过该方法获得,最后把左子树的所有path遍历都加入root,右边同理,得到path
- traverse:通常是站在root,向左右走,所以root先加入path,走到尽头的路上一次加入节点,最后,左右节点==null的时候,要把当前已生成的path加入到结果集中。
//divide-conquer
public List binaryTreePaths(TreeNode root) {
List paths = new ArrayList<>();
if(root==null) return paths;
if(root.left == null && root.right == null){
paths.add(root.val+"");
return paths;
}
List left =binaryTreePaths(root.left);
List right =binaryTreePaths(root.right);
//left==[], skip for()
for(String l : left){
paths.add(root.val+"->"+l);
}
for(String r : right){
paths.add(root.val+"->"+r);
}
return paths;
}
// helper-traverse
public List binaryTreePaths(TreeNode root) {
List result = new ArrayList<>();
if(root==null) return result;
helper(root,String.valueOf(root.val),result);
return result;
}
private void helper(TreeNode root, String path, List result){
if(root==null) return;
if(root.left == null && root.right == null){
result.add(path);
}
if(root.left!=null){
helper(root.left, path+"->"+String.valueOf(root.left.val), result);
}
if(root.right!=null){
helper(root.right, path+"->"+String.valueOf(root.right.val), result);
}
}
- 题二:Maximum Depth of Binary Tree:左边取最大,右边取最大,结果是这两者之中的较大值+1(root),左右两边取最大就是递归调用自己,divide-conquer比较简单
- 题三:Minimum Subtree:跟上面思路一样,都取最小,区别在于上面是要返回最大值int的,这个要返回node,所以用一样的分治法需要new class, Result{node; min; sum}来存储我们需要的结果。 也可以用traverse+全局变量+divide-conquer的方法做
贴一下这两个题的divide-conquer方法
//max depth
public int maxDepth(TreeNode root) {
int dep = 0;
// null or leaf
if (root == null) {
return dep;
}
// Divide
int left = maxDepth(root.left);
int right = maxDepth(root.right);
// Conquer
return Math.max(left,right)+1;
}
// min subtree
public class Solution {
class Result{
int sum;
int min;
TreeNode minroot;
public Result(int sum, TreeNode minroot,int min){
this.sum = sum;
this.minroot=minroot;
this.min = min;
}
}
public TreeNode findSubtree(TreeNode root){
return helper(root).minroot;
}
private Result helper(TreeNode root){
if(root==null) return new Result(0,null,Integer.MAX_VALUE);
//divide
Result left = helper(root.left);
Result right = helper(root.right);
//conquer
Result r = new Result(root.val,root,root.val);
r.sum = root.val+left.sum+right.sum;
if(r.sumleft.min&&left.min
- 用Result类的一些例子(包括上面的min subtree的方法)
- Balanced Binary Tree
首先看定义:左子树是平衡的,右子树是平和的,左右高度差距不大于1,或者说高度是想同的,三个条件 - 第一种解法看起来代码短小精悍,很好,但这个解法最难的地方应该是maxdepth这个方法的返回值部分,最后如果遍历到目前为止,是balanced的话,记录当前的height就是通过return语句做的。应该是因为这里只要求返回Boolean,所以可以省个Result类
- Result类实现,两种解法的思路其实就一样,写过第一种,第二种就可以自己写出来了,需要注意一点的是当 unbalanced, maxdepth怎么表示合适,我选了0,也可以是-1,可以通过交流来选择合适的值。
public class Solution {
public boolean isBalanced(TreeNode root) {
return maxDepth(root)!=-1;
}
private int maxDepth(TreeNode root){
if(root==null) return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
if(left==-1 || right==-1 || Math.abs(left-right)>1){
return -1;
}
return Max.max(left,right)+1;
}
//with Result class
public class Solution {
class Result{
int maxdepth;
boolean isBalanced;
public Result(boolean isBalanced, int maxdepth){
this.isBalanced = isBalanced;
this.maxdepth = maxdepth;
}
}
public boolean isBalanced(TreeNode root) {
return helper(root).isBalanced;
}
private Result helper(TreeNode root){
if(root==null) return new Result(true,0);
Result left = helper(root.left);
Result right = helper(root.right);
if(!left.isBalanced || !right.isBalanced || Math.abs(left.maxdepth-right.maxdepth)>1){
return new Result(false,0);
}
return new Result(true,Math.max(left.maxdepth,right.maxdepth)+1);
}
}
- Validate Binary Search Tree
一个traverse方法,一个divide conquer,前者代码更简洁,后者思路更清晰,要注意一点是validate函数中如果没有违背BST规则的话最后返回的是更新的Result,更新max用right.max, 更新min用left.min不要弄错了
class Result{
boolean is_bst;
int maxValue, minValue;
Result(boolean is_bst, int maxValue, int minValue) {
this.is_bst = is_bst;
this.maxValue = maxValue;
this.minValue = minValue;
}
}
public class Solution {
public boolean isValidBST(TreeNode root) {
Result r = validate(root);
return r.is_bst;
}
private Result validate(TreeNode root){
//要想好这里返回什么
if(root==null ) return new Result(true,Integer.MIN_VALUE,Integer.MAX_VALUE);
Result left = validate(root.left);
Result right = validate(root.right);
if(!left.is_bst || !right.is_bst){
return new Result(false,0,0);
}
if(root.left!=null&& left.maxValue>=root.val){
return new Result(false,0,0);
}
if(root.right!=null&& right.minValue<=root.val){
return new Result(false,0,0);
}
return new Result(true,
Math.max(right.maxValue,root.val),
Math.min(left.minValue,root.val)
);
}
}
- Subtree with Maximum Average
九章答案里只有traverse+divide/conquer的方法,和上面那个min subtree很类似,在用全局变量的基础上加入了Result class,全局变量也有一个是Result 类型。一个小trick是求ave的时候用了 转换成乘法的方式,避免除法带来的误差等各种问题
private TreeNode subtree = null;
private ResultType subtreeResult = null;
private class ResultType {
public int sum, size;
public ResultType(int sum, int size) {
this.sum = sum;
this.size = size;
}
}
public TreeNode findSubtree2(TreeNode root) {
helper(root);
return subtree;
}
private ResultType helper(TreeNode root) {
if (root == null) {
return new ResultType(0, 0);
}
ResultType left = helper(root.left);
ResultType right = helper(root.right);
ResultType result = new ResultType(
left.sum + right.sum + root.val,
left.size + right.size + 1
);
if (subtree == null ||
subtreeResult.sum * result.size < result.sum * subtreeResult.size
) {
subtree = root;
subtreeResult = result;
}
return result;
}
Flatten Binary Tree to Linked List
思路是:flattern左子树和右子树,然后root—>left.head, left.tail—>right.head 。就是左子树flatten,右子树flatten,那我们想把root,左,右三部分拼起来就需要root->left.first--left.last->right.first
有三种方法,都看一下Binary Tree Longest Consecutive Sequence (google onsite)
比较简单的一种traverse+divide conquer方法:一个全局变量longest和一个helper方法
private int helper(TreeNode root, TreeNode parent, int lengthWithoutRoot) {
if (root == null) {
return 0;
}
int length = (parent != null && parent.val + 1 == root.val)
? lengthWithoutRoot + 1
: 1;
int left = helper(root.left, root, length);
int right = helper(root.right, root, length);
return Math.max(length, Math.max(left, right));
}
- LCA系列:给两个node A, B 找最近公共祖先
- 新手版,简单的divide+conquer,因为这里不用管是不是都存在,所以后面那里直接返回left,right。看起来是种很取巧的方法
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode node1, TreeNode node2) {
if (root == null || root == node1 || root == node2) {
return root;
}
// Divide
TreeNode left = lowestCommonAncestor(root.left, node1, node2);
TreeNode right = lowestCommonAncestor(root.right, node1, node2);
// Conquer
if (left != null && right != null) {
return root;
}
if (left != null) {
return left;
}
if (right != null) {
return right;
}
return null;
}
2. 包含parent指针:找到给定的A,B两点到root的path,然后从root开始一起遍历两条path,path上最后一个相同的点,就是LCA。这个思路很直接
public ParentTreeNode lowestCommonAncestorII(ParentTreeNode root,
ParentTreeNode A,
ParentTreeNode B) {
if(root==null) return null;
ArrayList left = pathToRoot(A);
ArrayList right = pathToRoot(B);
int idxl = left.size()-1;
int idxr = right.size()-1;
ParentTreeNode lca = null;
while (idxl >= 0 && idxr >= 0) {
if(left.get(idxl)!=right.get(idxr)) break;
lca = left.get(idxl);
idxl--;
idxr--;
}
return lca;
}
private ArrayList pathToRoot(ParentTreeNode node){
ArrayList path = new ArrayList();
//理解思路情况下这里写错了,判断了parent并添加parent,不对。
while(node!=null ){
path.add(node);
node = node.parent;
}
return path;
}
3. 如果给的节点有一个不存在,返回null(新手版返回存在的那个节点)这个方法反正想不到,而且判断是否存在那里,很绕。抄的lintcode答案。。。
/**
* Definition of TreeNode:
* public class TreeNode {
* public int val;
* public TreeNode left, right;
* public TreeNode(int val) {
* this.val = val;
* this.left = this.right = null;
* }
* }
*/
// check if a exists, b exists
class Result {
public boolean a_exist, b_exist;
public TreeNode node;
Result(boolean a, boolean b, TreeNode n) {
a_exist = a;
b_exist = b;
node = n;
}
}
public class Solution {
public TreeNode lowestCommonAncestor3(TreeNode root, TreeNode A, TreeNode B) {
Result re = help(root,A,B);
if(re.a_exist && re.b_exist) return re.node;
else return null;
}
private Result help(TreeNode root, TreeNode A, TreeNode B){
if(root==null) return new Result(false, false, null);
Result left = help(root.left, A, B);
Result right = help(root.right, A, B);
boolean aexi = left.a_exist || right.a_exist || root==A;
boolean bexi = left.b_exist || right.b_exist || root==B;
if(root==A || root==B || (left.node!=null && right.node!=null)){
return new Result(aexi, bexi, root);
}
if(left.node!=null) return new Result(aexi, bexi, left.node);
if(right.node!=null) return new Result(aexi, bexi, right.node);
return new Result(aexi, bexi, null);
}
}
- Binary Tree Path Sum系列:给一个target,找root到leaf的一条path和==target
- 正常版
public List> binaryTreePathSum(TreeNode root, int target) {
List> result = new ArrayList<>();
if (root == null) {
return result;
}
ArrayList path = new ArrayList();
//A valid path is from root node to any of the leaf nodes. So we always need to add root in each path.
path.add(root.val);
helper(root, path, root.val, target, result);
return result;
}
//preorder、DFS + backtracking
private void helper(TreeNode root,
ArrayList path,
int sum,
int target,
List> result) {
// 递归的出口:meet leaf && sum==target
if (root.left == null && root.right == null) {
if (sum == target) {
result.add(new ArrayList(path));
}
return;
}
//递归的拆解:分别去左右子树,用来算sum
if (root.left != null) {
path.add(root.left.val);
helper(root.left, path, sum + root.left.val, target, result);
//back-tracking, delete the last elementm of path to contruct new path
path.remove(path.size() - 1);
}
if (root.right != null) {
path.add(root.right.val);
helper(root.right, path, sum + root.right.val, target, result);
path.remove(path.size() - 1);
}
}
1. The path does not need to start or end at the root or a leaf, but it must go in a straight line down.
思路是:
到每个点的时候,都几下所有到这个点的path,包括不从头出发的,只要是在当前点end的path,都存。比如1--2--4这样的树(请脑补成一棵树),走到2, store[[],[2],[1,2]],同样的走到4, store [[],[4],[1,2,4],[2,4]]. 其他见代码注释,也是个自己写估计写不出来的方法
public List> binaryTreePathSum2(TreeNode root, int target) {
List> results = new ArrayList>();
//buffer这名字意味着这个东西是起辅助作用的,帮助记录path,从而在得到target的时候可以在buffer中倒推找到validpath
ArrayList buffer = new ArrayList();
if (root == null)
return results;
findSum(root, target, buffer, 0, results);
return results;
}
//递归的定义:level的意思显而易见,但不太知道为什么用level
public void findSum(TreeNode head, int sum, ArrayList buffer, int level, List> results) {
if (head == null) return;
//因为后面还要传入sum,所以copy一个sum值来操作而不在原数据上操作。这里对tmp sum的操作是-=,target-=nodes.vals直到等于0时,就说明这几个nodes sum==target
int tmp = sum;
buffer.add(head.val);
for (int i = level;i >= 0; i--) {
tmp -= buffer.get(i);
if (tmp == 0) {
//在buf中回找validPath
List validPath = new ArrayList();
for (int j = i; j <= level; ++j)
validPath.add(buffer.get(j));
results.add(validPath);
}
}
findSum(head.left, sum, buffer, level + 1, results);
findSum(head.right, sum, buffer, level + 1, results);
buffer.remove(buffer.size() - 1);
}
2. the path could be start and end at any node in the tree.(n个节点的树,任意两点选路径共有n choose 2条,可以暴力解,枚举所有两点之间有路径的情况,以当前点分左右所有节点两部分,两部分两两配对)
可以拐弯的follow-up,关注点都在拐点,拐点前后,必是直上直下,所以可以用正常方法得到,之后在拼接(代码自己并不能独立写,不贴了)
5. 树和分治总结
二叉树
给出一棵Binary Tree的字符串表示,比如[1,[2,3]],还原这棵二叉树(高频)
给出一棵Binary Tree的先序和中序遍历序列,还原这棵二叉树
给出一棵Binary Tree,按照深度(同样深度从左往右)遍历并输出结果
给出一棵Binary Tree,输出每一条从根节点到叶子节点的路径
给出一棵Binary Tree,输出与之镜面对称的二叉树
给出两棵Binary Tree,判断这两棵二叉树是否完全一样(形状和每个点的value都要相同才算完全一样)
给出两棵Binary Tree,A和B,判断B是否为A的子树
分治法
求一棵二叉树的最大深度(分治思想的简单应用)
给出一棵Binary Tree,求出这棵二叉树上和最大的路径
给出一棵Binary Search Tree,问是否是Balanced Binary Search Tree
合并k个排好序的List(高频)
求一个Array中的中位数(高频,partition方法)
给出两个排好序的List,输出这两个序列中的中位数,如果存在两个中位数则输出这两个数的平均数
给出一个Array,求出Array中的每个元素的右边比其小的元素个数(归并排序应用)
给出一个平面上的若干个点,求其中最近的两个点的距离(要求时间复杂度小于n^2)
给出一个n*n的棋盘,n是2的幂,开始时这个棋盘上只有一个格子是黑色的,其他均是白色的。现在需要用一个黑色的L型去填满这个棋盘,求一种填满的方案(要求时间复杂度尽可能低)
Reference
- http://www.geeksforgeeks.org/category/algorithm/divide-and-conquer/
6. 其他相关问题
• Binary Search Tree Iterator
• http://www.lintcode.com/problem/binary-search-tree-iterator
• http://www.jiuzhang.com/solutions/binary-search-tree-iterator
• In-order Successor in Binary Search Tree
• http://www.lintcode.com/problem/inorder-successor-in-binary-search-tree/
• http://www.jiuzhang.com/solutions/inorder-successor-in-binary-search-tree/
• Search Range in Binary Search Tree
• http://www.lintcode.com/problem/search-range-in-binary-search-tree/
• Insert Node in a Binary Search Tree
• http://www.lintcode.com/problem/insert-node-in-a-binary-search-tree/
• Remove Node in a Binary Search Tree
• http://www.lintcode.com/problem/remove-node-in-binary-search-tree/
• http://www.mathcs.emory.edu/~cheung/Courses/171/Syllabus/9-BinTree/BST-delete.html
- 求二叉树中的节点个数:
getNodeNumRec(递归),getNodeNum(迭代) - 求二叉树的深度:
getDepthRec(递归),getDepth - 分层遍历二叉树(按层次从上往下,从左往右):
levelTraversal, levelTraversalRec(递归解法) - 将二叉查找树变为有序的双向链表:
convertBST2DLLRec, convertBST2DLL
如果二叉查找树不为空:
如果左子树为空,对应双向有序链表的第一个节点是根节点,左边不需要其他操作;
如果左子树不为空,转换左子树,二叉查找树对应双向有序链表的第一个节点就是左子树转换后双向有序链表的第一个节点,同时将根节点和左子树转换后的双向有序链 表的最后一个节点连接;
如果右子树为空,对应双向有序链表的最后一个节点是根节点,右边不需要其他操作;
如果右子树不为空,对应双向有序链表的最后一个节点就是右子树转换后双向有序链表的最后一个节点,同时将根节点和右子树转换后的双向有序链表的第一个节点连 接。
- 求二叉树第K层的节点个数:
getNodeNumKthLevelRec, getNodeNumKthLevel
(1)如果二叉树为空或者k<1返回0
(2)如果二叉树不为空并且k==1,返回1
(3)如果二叉树不为空且k>1,返回左子树中k-1层的子节点个数与右子树k-1层子节点个数之和(这个描述有点奇怪)
private int GetNodeNumKthLevel(BinaryTreeNode Root, int k){
if(Root == NULL || k < 1) return 0;
if(k == 1) return 1;
int numLeft = GetNodeNumKthLevel(root.left, k-1);
int numRight = GetNodeNumKthLevel(root.right, k-1);
return (numLeft + numRight);
}
- 求二叉树中叶子节点的个数:
(1)如果二叉树为空,返回0
(2)如果二叉树不为空且左右子树为空,返回1
(3)如果二叉树不为空,且左右子树不同时为空,返回左子树中叶子节点个数加上右子树中叶子节点个数 - 判断两棵二叉树是否相同的树:
(1)如果两棵二叉树都为空,返回真
(2)如果两棵二叉树一棵为空,另一棵不为空,返回假
(3)如果两棵二叉树都不为空,如果对应的左子树和右子树都同构返回真,其他返回假 - 求二叉树的镜像(破坏和不破坏原来的树两种情况):
(1)如果二叉树为空,返回空
(2)如果二叉树不为空,求左子树和右子树的镜像,然后交换左子树和右子树
判断两个树是否互相镜像 - 求二叉树中节点的最大距离:
(1)如果二叉树为空,返回0,同时记录左子树和右子树的深度,都为0
(2)如果二叉树不为空,最大距离要么是左子树中的最大距离,要么是右子树中的最大距离,要么是左子树节点中到根节点的最大距离+右子树节点中到根节点的最大距离,同时记录左子树和右子树节点中到根节点的最大距离。 - 由前序遍历序列和中序遍历序列重建二叉树:
rebuildBinaryTreeRec - 判断二叉树是不是完全二叉树:
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全
二叉树。
有如下算法,按层次(从上到下,从左到右)遍历二叉树,当遇到一个节点的左子树为空时,则该节点右子树必须为空,且后面遍历的节点左
右子树都必须为空,否则不是完全二叉树。