二叉树问题
二叉树遍历相关
二叉树的前序遍历 144题:
给定一个二叉树,返回它的前序遍历。
分析:
递归的容易;迭代版用栈做,写一个辅助函数visitAlongLeftBranch()
代码:
前序递归版:
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
preorder(root,res);
return res;
}
public void preorder(TreeNode root,List<Integer> res){
if (root==null) return;
res.add(root.val);
preorder(root.left,res);
preorder(root.right,res);
}
前序迭代版:
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();//辅助栈
while (true){
visitAlongleftbranch(root,res,stack);
if (stack.isEmpty()) break;
root=stack.pop();
}
return res;
}
public void visitAlongleftbranch(TreeNode root,List<Integer> res,Stack<TreeNode> stack){
while (root!=null){
res.add(root.val);
stack.push(root.right);
root=root.left;
}
}
前序迭代版图示:
二叉树的中序遍历 94题:
给定一个二叉树,返回它的中序遍历。
分析:
递归容易,迭代用栈,辅助函数goAlongleftbranch()
代码:
中序递归版:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
inorder(root,res);
return res;
}
private void inorder(TreeNode root,List<Integer> res){
if (root==null) return;
inorder(root.left,res);
res.add(root.val);
inorder(root.right,res);
}
中序迭代版:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Stack<TreeNode> s=new Stack<>();
while (true){
goAlongleftbranch(root,s);
if (s.isEmpty()) break;
root=s.pop();
res.add(root.val);
root=root.right;
}
return res;
}
private void goAlongleftbranch(TreeNode root,Stack<TreeNode> s){
while (root!=null){
s.add(root);
root=root.left;
}
}
中序迭代版图示:
二叉树的后序遍历 145题(hard)
给定一个二叉树,返回它的后序遍历。
分析:
递归版依旧简单;对于迭代版,按照指定的left->right->root顺序在这个给定的二叉树结构下(没有对父节点的引用)正着做并不好做,所以可以反着想:按照root->right->left的顺序遍历,再将结果reverse一下就可以了。这样,就能用先序遍历的方法了,改动非常小。
代码:
后序递归版:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
postorder(root,res);
return res;
}
public void postorder(TreeNode root,List<Integer> res) {
if (root == null) return;
postorder(root.left, res);
postorder(root.right, res);
res.add(root.val);
}
后序迭代版:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Stack<TreeNode> stack=new Stack<>();//辅助栈
while (true){
visitAlongRightbranch(root,res,stack);
if (stack.isEmpty()) break;
root=stack.pop();
}
return res;
}
public void visitAlongRightbranch(TreeNode root,List<Integer> res,Stack<TreeNode> stack){
while (root!=null){
res.add(0,root.val);//每次都在结果列表头部加入元素,与最后reverse的效果相同
stack.push(root.left); //先序遍历这里是:stack.push(root.right)
root=root.right; //先序遍历这里是:root=root.left
}
}
二叉树展为链表 114题:
给定一个二叉树,原地将它展开为链表。
例如,给定二叉树
1
/ \
2 5
/ \ \
3 4 6
将其展开为:
1
\
2
\
3
\
4
\
5
\
6
分析:
这题是在递归版遍历基础上实现的;展开后的顺序是按照先序(root,left,right)连接的,如果以相反的顺序遍历,即(right,left,root)顺序,那么就是依次遍历[6->5->4->3->2->1];想法就是按照此顺序遍历原始树。遍历部分如下:
public void flatten(TreeNode root) {
if (root == null)
return;
flatten(root.right);
flatten(root.left);
//在这里添加遍历中的操作
}
在遍历中还要进行连接操作:先把每个节点的右孩子节点置为它的前一个(前一个指的是遍历顺序中的前一个,即在当前节点之前一个遍历的节点),这一步类似与链表中的连接操作;再把左孩子节点在一次递归结束之前置为null。
代码:
private TreeNode prev = null;
public void flatten(TreeNode root) {
if (root == null)
return;
flatten(root.right);
flatten(root.left);
root.right = prev;
root.left = null;
prev = root;
}
二叉树的层次遍历 102题:
给定一个二叉树,返回其按层次遍历的节点值。(即逐层地,从左到右访问所有节点)。
例如:
给定二叉树: [3,9,20,null,null,15,7],
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
分析:
层次遍历用队列的结构做,仔细看这题的输出其实和正常的层次遍历不一样:不止要输出遍历顺序,还要分不同的层输出,这就要记录每层的节点个数。
代码:
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
if (root==null) return res;
Queue<TreeNode> queue=new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()){
int size=queue.size();
List<Integer> cur=new ArrayList<>();
for (int i = 0; i <size ; i++) {
TreeNode tmp=queue.poll();
cur.add(tmp.val);
if (tmp.left!=null) queue.add(tmp.left);
if (tmp.right!=null) queue.add(tmp.right);
}
res.add(cur);
}
return res;
}
二叉树的层次遍历 II 107题(easy):
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如:
给定二叉树 [3,9,20,null,null,15,7],
返回其自底向上的层次遍历为:
[
[15,7],
[9,20],
[3]
]
分析:
这题和I是一样的,只不过要求自底向上,那么只需要输出结果reverse一下就可以了(类似后序遍历中参照前序遍历),如果不最后reverse,只要在add时,每次指定插在头部。
代码:
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
if (root==null) return res;
Queue<TreeNode> queue=new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()){
int size=queue.size();
List<Integer> cur=new ArrayList<>();
for (int i = 0; i <size ; i++) {
TreeNode tmp=queue.poll();
cur.add(tmp.val);
if (tmp.left!=null) queue.add(tmp.left);
if (tmp.right!=null) queue.add(tmp.right);
}
res.add(0,cur);//和I只有这一处变化
}
return res;
}
二叉树的锯齿形层次遍历 103题:
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
给定二叉树 [3,9,20,null,null,15,7],
返回锯齿形层次遍历如下:
3
/ \
9 20
/ \
15 7
[
[3],
[20,9],
[15,7]
]
分析:
这题和层次遍历相同的方法,只是在添加到子list时选择正序还是反序。
代码:
public List<List<Integer>>zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
Queue<TreeNode> q=new LinkedList<>();
if (root==null) return res;
q.add(root);
boolean flag=true;//用标志来标记正向添加还是反相添加
while (!q.isEmpty()){
int len=q.size();
List<Integer> cur=new ArrayList<>();
for (int i=0;i<len;i++){
TreeNode x=q.poll();
if (flag){
cur.add(x.val); //正常顺序遍历 但不同顺序添加到cur中
}else {
cur.add(0,x.val); //相当于从右向左添加
}
if (x.left!=null) q.add(x.left);
if (x.right!=null) q.add(x.right);
}
flag=!flag;
res.add(cur);
}
return res;
}
从前序与中序遍历序列构造二叉树 105题:
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出:
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
分析:
这题属于DFS。
假设一个前序遍历序列中出现的节点node,那么它的左右节点是哪两个?
不难想出它的左节点是前序遍历序列中node的下一个节点;
它的右节点也可以根据node在中序遍历序列中的位置来确定,即它的右节点在前序遍历序列中的位置=node在前序遍历中的位置+node的左子树中节点个数+1。
所以现在的关键在于如何确定node的左子树中节点个数?
方法就是在中序遍历子序列(注意是子序列概念,子序列对应于子树)中搜索node,以node为间隔点将中序遍历子序列分为两个部分,从起始点开始到node为界都是node左子树中的节点,这样就能确定节点个数。每次递归子序列都会一分为二作为左右子树的搜索范围,直到到达递归基。
代码:
public TreeNode buildTree(int[] preorder, int[] inorder) {
return helper(0,0,preorder.length-1,preorder,inorder);
}
private TreeNode helper(int pre,int instart,int inend,int[] preorder,int[] inorder){
if (pre>=preorder.length||instart>inend){
return null;
}
TreeNode root=new TreeNode(preorder[pre]);
int index=0;
for (int i=instart;i<=inend;i++){
if (preorder[pre]==inorder[i]){
index=i;
break;
}
}
root.left=helper(pre+1,instart,index-1,preorder,inorder);
root.right=helper(pre+index-instart+1,index+1,inend,preorder,inorder);
return root;
}
从中序与后序遍历序列构造二叉树 106题:
根据一棵树的中序遍历与后序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出:
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
分析:
和上一题的方法一样
代码:
public TreeNode buildTree(int[] inorder, int[] postorder) {
return helper(postorder.length-1,0,inorder.length-1,postorder,inorder);
}
private TreeNode helper(int post,int instart,int inend,int[] postorder,int [] inorder){
if (post<0||instart>inend) return null;
TreeNode root=new TreeNode(postorder[post]);
int index=0;
for (int i=instart;i<=inend;i++){
if (postorder[post]==inorder[i]){
index=i;
break;
}
}
root.left=helper(post-(inend-index+1),instart,index-1,postorder,inorder);
root.right=helper(post-1,index+1,inend,postorder,inorder);
return root;
}
二叉树中的DFS运用问题
相同的树 100题(easy):
给定两个二叉树,编写一个函数来检验它们是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例:
输入: 1 1
/ \ / \
2 3 2 3
[1,2,3], [1,2,3]
输出: true
输入: 1 1
/ \
2 2
[1,2], [1,null,2]
输出: false
分析:
用DFS来做。对于给定两个节点考虑几种情况:都为null;其中一个为null;都不为null且val不等;都不为null且val相等。
代码:
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p==null&&q==null) return true; //都为空
if (p==null||q==null) return false; //一个为空
if (p.val==q.val){
return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
}
return false; //值不等返回假
}
对称二叉树 101题(easy):
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
分析:
和相同二叉树相同的方法DFS。但因为原函数参数是根节点,要比较两个节点就要定义一个有两个参数的辅助递归函数。
代码:
public boolean isSymmetric(TreeNode root) {
if (root==null) return false;
return helper(root.left,root.right);
}
public boolean helper(TreeNode x,TreeNode y){
if (x==null&&y==null) return true;
if (x==null||y==null) return false;
if (x.val==y.val){
return helper(x.left,y.right)&&helper(x.right,y.left);
}
return false;
}
二叉树的最大深度 104题(easy):(注:二叉树定义中的深度就是指的最大深度)
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
分析:
简单题目。返回 当前节点左右孩子节点深度的最大值+1 就行
代码:
public int maxDepth(TreeNode root) {
if (root==null) return 0;
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
二叉树的最小深度 111题 (easy):
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最小深度 2.
分析:
这题和最大深度思路是一样的,但要注意当一个节点只含有左孩子或者只含有右孩子时,不能单纯返回左右孩子树的最小深度的最小值,因为这时候左右子树有一个是不存在的不能认定为它深度为0;
代码:
public int minDepth(TreeNode root) {
if (root==null) return 0;
int leftmin=minDepth(root.left);
int rightmin=minDepth(root.right);
if (leftmin==0&&rightmin==0) return 1;
if (leftmin==0) return rightmin+1;
if (rightmin==0) return leftmin+1;
return Math.min(leftmin,rightmin)+1;
}
平衡二叉树 110题(easy):
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
示例:
给定二叉树 [3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回 true
给定二叉树 [1,2,2,3,3,null,null,4,4]
1
/ \
2 2
/ \
3 3
/ \
4 4
返回 false
分析:
这道题有两种思路,一种是自上而下,一种是自下而上;
1.自上而下的方法:
严格按照平衡二叉树的定义检查树是否平衡:两个子树的高度差不大于1,左子树和右子树也是平衡的。
辅助函数是104题计算最大深度函数maxDepth()。
对于当前节点根,调用其左右子节点的maxDepth()实际上必须访问其所有子节点,因此复杂度为O(N)。我们对树中的每个节点执行此操作,因此isBalanced的总体复杂度将为O( N^2 )。这是一种自上而下的方法。
代码:
public boolean isBalanced(TreeNode root) {
if (root==null) return true;
if (Math.abs(maxDepth(root.left)-maxDepth(root.right))<=1){
return isBalanced(root.left)&&isBalanced(root.right);
}
return false;
}
public int maxDepth(TreeNode root) {
if (root==null) return 0;
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
2.自下而上的方法:
这种方法基于DFS。我们不是为每个子节点显式调用maxDepth(),而是在DFS递归中返回当前节点的高度。当当前节点(包括)的子树平衡时,函数dfsHeight()返回非负值作为高度。否则返回-1。根据两个孩子的leftHeight和rightHeight,父节点可以检查子树是否平衡,并确定其返回值。
在这种自下而上的方法中,树中的每个节点只需要访问一次。因此,时间复杂度为O(N),优于第一解决方案。
代码:
public boolean isBalanced(TreeNode root) {
return dfsHeight(root)!=-1;
}
public int dfsHeight(TreeNode root) {
if (root==null) return 0;
int leftdheight=dfsHeight(root.left);
if (leftdheight==-1) return -1;
int rightheight=dfsHeight(root.right);
if (rightheight==-1) return -1;
if (Math.abs(leftdheight-rightheight)>1) return -1;
return Math.max(leftdheight,rightheight)+1;
}
路径总和 112题(easy):
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
分析:
DFS方法。注意递归基。
代码:
public boolean hasPathSum(TreeNode root, int sum) {
if (root==null) return false;
if (root.left==null&&root.right==null&&sum==root.val) return true;
return hasPathSum(root.left,sum-root.val)||hasPathSum(root.right,sum-root.val);
}
路径总和II
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
返回:
[
[5,4,11,2],
[5,8,4,5]
]
分析:
和上一题判断路径总和的思路一致,关键在于得到符合总和的路径时列表的添加,这里和回溯中几道题的技巧一样,参数中传入结果列表(List<List
代码:
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<List<Integer>> res=new ArrayList<>();
List<Integer> cur=new ArrayList<>();
helper(root,sum,cur,res);
return res;
}
public void helper(TreeNode root,int sum,List<Integer> cur,List<List<Integer>> res){
if (root==null) return;
cur.add(root.val);
if (root.left==null&&root.right==null&&root.val==sum){
res.add(new ArrayList<>(cur));
}else {
helper(root.left, sum - root.val, cur, res);
helper(root.right, sum - root.val, cur, res);
}
cur.remove(cur.size() - 1);
}
求根到叶子节点数字之和 129题:
给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。
例如,从根到叶子节点路径 1->2->3 代表数字 123。
计算从根到叶子节点生成的所有数字之和。
说明: 叶子节点是指没有子节点的节点。
示例:
输入: [1,2,3]
1
/ \
2 3
输出: 25
解释:
从根到叶子节点路径 1->2 代表数字 12.
从根到叶子节点路径 1->3 代表数字 13.
因此,数字总和 = 12 + 13 = 25.
输入: [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.
分析
这题比路径总和II的简单,那题实际上需要回溯剪枝找到符合要求的,这题就是纯DFS递归,把每条路径最终结果相加就行,只需传一个参数cur记录当前路径到目前的计算结果,也不用传一个参数sum。
代码:
public int sumNumbers(TreeNode root) {
return helper(root,0);
}
public int helper(TreeNode root,int cur){
if (root==null) return 0;
cur=cur*10+root.val;
if (root.left==null&&root.right==null){
return cur;
}
return helper(root.left,cur)+helper(root.right,cur);
}
二叉树的所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
输入:
1
/ \
2 3
\
5
输出: ["1->2->5", "1->3"]
解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3
分析:
这题不难,一个注意的地方是可以用StringBuilder代替“+”连接字符串来提高效率。
代码:
public List<String> binaryTreePaths(TreeNode root) {
List<String> rst = new ArrayList<String>();
if(root == null) return rst;
StringBuilder sb = new StringBuilder();
helper(rst, sb, root);
return rst;
}
public void helper(List<String> rst, StringBuilder sb, TreeNode root){
if(root == null) return;
int tmp = sb.length();
if(root.left == null && root.right == null){
sb.append(root.val);
rst.add(sb.toString());
sb.delete(tmp , sb.length());
return;
}
sb.append(root.val + "->");
helper(rst, sb, root.left);
helper(rst, sb, root.right);
sb.delete(tmp , sb.length());
return;
}
public List<String> binaryTreePaths(TreeNode root) {
List<String> res=new ArrayList<>();
helper(res,"",root);
return res;
}
public void helper(List<String> res,String cur,TreeNode root){
if (root==null) return;
if (root.left==null&&root.right==null){
cur+=Integer.toString(root.val);
res.add(cur);
}
helper(res,cur+Integer.toString(root.val)+"->",root.left);
helper(res,cur+Integer.toString(root.val)+"->",root.right);
}
二叉树的右视图
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例:
输入: [1,2,3,null,5,null,4]
输出: [1, 3, 4]
解释:
1 <---
/ \
2 3 <---
\ \
5 4 <---
分析:
右视图就是每一层最右边的数字,递归时先向右边深入,右边不能再深入后再向左边深入,递归时传参数记录当前递归深度,如果当前递归深度小于等于输出列表中的最后添加元素的深度则不添加,反之,将当前元素添加到输出列表。
代码:
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res=new ArrayList<>();
helper(root,res,0);
return res;
}
public void helper(TreeNode root,List<Integer> res,int curdepth){
if (root==null) return;
if (curdepth==res.size()){ //res.size()来获取已经添加的深度
res.add(root.val);
}
helper(root.right,res,curdepth+1);
helper(root.left,res,curdepth+1);
}
//函数参数中加一个记录递归深度的变量
翻转二叉树 226题:
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
分析:
谷歌:我们90%的工程师使用您编写的软件(Homebrew),但是您却无法在面试时在白板上写出翻转二叉树这道题,这太糟糕了。
。。。。
这题不难,用DFS或者BFS都可以做。对每一个节点都执行左右孩子节点的交换。discuss中的DFS没用到辅助函数,递归调用的返回值就是TreeNode,比较好。
代码:
DFS:深度优先操作
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode left = root.left, right = root.right;
root.left = invertTree(right);
root.right = invertTree(left);
return root;
}
BFS:存队列中,一层一层地操作
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
final Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
final TreeNode node = queue.poll();
final TreeNode left = node.left;
node.left = node.right;
node.right = left;
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
return root;
}
二叉搜索树系列
将又序数组转换为二叉搜索树 108题(easy):
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定有序数组: [-10,-3,0,5,9],
一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
分析:
类似于二分查找,每次取数组中间的数作为节点val。
代码:
public TreeNode sortedArrayToBST(int[] nums) {
return helper(nums,0,nums.length);
}
public TreeNode helper(int[] nums,int low,int hight){
if (low>=hight) return null;
int mid=low+(hight-low)/2;
TreeNode root=new TreeNode(nums[mid]);
root.left=helper(nums,low,mid);
root.right=helper(nums,mid+1,hight);
return root;
}
二叉搜索树的最近公共祖先 235题(easy):
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
_______6______
/ \
___2__ ___8__
/ \ / \
0 _4 7 9
/ \
3 5
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉搜索树中。
分析:
这题指定了是二叉搜索树,而且节点值唯一,所以只要比较当前节点值和要查找的两个节点值就行了。然后确定是在左子树还是右子树中继续找。迭代就能实现。
代码:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while (true){
if (root.val<p.val&&root.val<q.val){
root=root.right;
}
else if (root.val>p.val&&root.val>q.val){
root=root.left;
}
else return root;
}
}
二叉树的最近公共祖先 236题:
题目要求同上,只不过将二叉搜索树改为二叉树。
分析:
注意给定的p和q就是指向树中的节点,递归,从底层 层层“推导出”最低父节点。理解其中的推导逻辑。
代码:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q); //分别深入左右子节点
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left != null && right != null) return root;
return left != null ? left : right;
}
验证二叉搜索树 98题:
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例:
输入:
2
/ \
1 3
输出: true
输入:
5
/ \
1 4
/ \
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
分析:
验证二叉搜索树,根据二叉搜索树和二叉树中序遍历的关系,不难想出可以用中序遍历(迭代版)二叉树,即如果一个二叉树是二叉搜索树,那么它的中序遍历一定是单调递增的。
discuss中还有一种简单的递归方法,传参数min,max限定范围。
代码:
递归方法:
public boolean isValidBST(TreeNode root) {
return helper(root,Long.MIN_VALUE,Long.MAX_VALUE);
}
public boolean helper(TreeNode root,long minval,long maxval){
if (root==null) return true;
if (root.val<=minval||root.val>=maxval)return false;
return helper(root.left,minval,root.val)&&helper(root.right,root.val,maxval);
}
中序遍历(迭代版)法:
public boolean isValidBST(TreeNode root) {
Stack<TreeNode> s=new Stack<>();
TreeNode x=root;
TreeNode before=null;
while (true){
golongLeftBranch(x,s);
if (s.isEmpty()) break;
x=s.pop();
if (before!=null&&(x.val<=before.val)) return false;
before=x;
x=x.right;
}
return true;
}
private void golongLeftBranch(TreeNode root,Stack<TreeNode> s){
TreeNode x=root;
while(x!=null){
s.push(x);
x=x.left;
}
}
其他问题:
填充同一层的兄弟节点II
给定一个二叉树:
public class TreeLinkNode {
int val;
TreeLinkNode left, right, next;
TreeLinkNode(int x) { val = x; }
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
示例:
给定二叉树:
1
/ \
2 3
/ \ \
4 5 7
调用你的函数后,该二叉树变为:
1 -> NULL
/ \
2 -> 3 -> NULL
/ \ \
4-> 5 -> 7 -> NULL
分析:
这题的一个基础版本是I,I中加的条件是在完美二叉树的情况下,稍简单但是一个思路。也可以用II的这个通用方法做。
自上而下的层次,如果上层的填充完就很容易通过上层的next指针,来填充下一层的;而第一层的默认就是填充好了,按照流程一层一层填充。
需要额外定义2个指针,一个dummy来定位每层的起始节点(每层填充完更新),一个pre来定位当前要连接的操作节点(每次都更新)。初始时两节点同指向。
代码:
public void connect(TreeLinkNode root) {
TreeLinkNode dummy=new TreeLinkNode(0);
TreeLinkNode pre=dummy;
while (root!=null){
if (root.left!=null){
pre.next=root.left;
pre=pre.next;
}
if (root.right!=null){
pre.next=root.right;
pre=pre.next;
}
root=root.next;
if (root==null){
root=dummy.next;
dummy.next=null;
pre=dummy;
}
}
}
完全二叉树中节点个数
给出一个完全二叉树,求出该树的节点个数。
说明:
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。
示例:
输入:
1
/ \
2 3
/ \ /
4 5 6
输出: 6
分析:
如果不限定是完全二叉树,就是求二叉树的节点个数可以DFS来做,复杂度是O(n)(每个节点都要访问到)。
这题限定了是完全二叉树,最优算法可以将复杂度降到O( (logN)^2 )
关键在于对于一个子树,可以通过判断是否是满二叉树(完美二叉树),如果是就可以直接得到子树的节点个数了( 2^h-1 );不是再继续递归。其中判断满二叉树的方法可以通过沿着当前节点的左右分支一直向下,并统计节点深度。
这题也可以用另一个不同方法(discuss)的迭代方法,优点是不用重复统计深度。
代码:
public int countNodes(TreeNode root) {
if (root==null) return 0;
int lc=countLeft(root.left);
int rc=countRight(root.right);
if (lc==rc) return (1<<lc)-1;
else return countNodes(root.left)+countNodes(root.right);
}
public int countLeft(TreeNode left){
int count=0;
while (left!=null){
count++;
left=left.left;
}
return count;
}
public int countRight(TreeNode right){
int count=0;
while (right!=null){
count++;
right=right.right;
}
return count;
}
另一种方法:height只统计节点最左侧分支高度。类似于二分查找
public int countNodes(TreeNode root) {
int nodes=0,h=height(root);
while (root!=null){
if (height(root.right)==h-1){ //说明左子树是满的
nodes+=1<<(h-1);
root=root.right;
}else { //这时height(root.right)只有等于h-2这种情况
// 右子树是满的
nodes+=1<<(h-2);
root=root.left;
}
h--; //向下迭代,对应高度要-1
}
return nodes; //返回总节点数
}
public int height(TreeNode root){
return root==null?0:height(root.left)+1;
}
遗留题目:
不同的二叉搜索树 I II 95题 96题
二叉搜索树迭代器 173题