二叉树
一、二叉树简介
1.基本概念
- 根节点:根节点是一个没有前驱结点的特殊结点。
-
叶节点(终端节点):度为零的节点,即没有子树的节点。
- 节点的度:一个节点的子树的个数,叫做该节点的度。
- 节点的层次:从根节点开始算起,根节点为第1层,根的子节点为第2层,以此类推。
- 节点的高度:该节点距离叶子节点的距离。
-
节点的深度:该节点距离根节点的距离。
- 二叉树:每个节点最多只能有两棵子树,且有左右之分。
2.二叉树种类
在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。
(1)满二叉树
如果二叉树的每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。即如果一个二叉树有 k 层,且结点总数是 2^k -1 ,则它就是满二叉树。
(2)完全二叉树
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最地层的节点都集中在该层最左边的若干位置。
(3)二叉排序树(二叉查找树、二叉搜索树)
前面介绍的树都是没有数值的,而二叉排序树是有数值的了,同时二叉排序树还是一个有序树(左小右大),具有以下性质:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
(4)平衡二叉排序树(AVL树)
平衡二叉排序树又被称为AVL(Adelson-Velsky and Landis)树,具有以下性质:
- 是一棵空树或左右子树的高度差的绝对值不超过1;
- 左右两个子树都是一棵平衡二叉树
最后一个不是AVL树,因为根节点10的左子树高度为2,右子树高度为0,左右子树高度差绝对值大于1了。
3.二叉树的存储方式
二叉树既可以链式存储,也可以顺序存储。链式存储就用指针, 顺序存储就用数组。顺序存储顾名思义元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。
我们一般采用链式存储的方式存储二叉树。
(1)链式存储示意图
(2)顺序存储
顺序存储其实就是用数组按照从上到下、从左到右的顺序来存储二叉树,顺序存储的方式如图:
用数组存储二叉树,如果父节点的数组下标是 i,那么左孩子就是 2 * i + 1,右孩子就是 2 * i + 2。
4.★二叉树的遍历
二叉树主要有两种遍历方式:
-
深度优先遍历:先往深走,遇到叶子节点再往回走。即对于每个树(至少两层)都遵循同一种遍历方式。
- 前序遍历(递归法,迭代法):先根节点,再左右节点;
- 中序遍历(递归法,迭代法):先左节点,再根节点,最后右节点;
- 后序遍历(递归法,迭代法):先左右节点,再根节点。
-
广度优先遍历:一层一层的去遍历。
- 层次遍历(迭代法)
(1)前中后序遍历二叉树
这里的“前中后”都是指的根节点(中间节点)的顺序,而左右节点的相对顺序始终不变,一直是先左后右。
(2)层序遍历
层序遍历就是从上到下、从左到右依次遍历,以上图为例:5,4,6,1,2,7,8。
(3)前中后序遍历的实现
做二叉树相关题目,经常会使用递归的方式来实现前中后序遍历。
同时,栈其实就是递归的一种实现结构,★所以前中后序遍历的递归实现都可以借助栈使用迭代(非递归)的方式来实现。
同时,栈其实就是递归的一种实现结构,★所以前中后序遍历的递归实现都可以借助栈使用迭代(非递归)的方式来实现。
- 递归遍历
- 迭代遍历
广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能层序遍历二叉树。
5.★二叉树的链式存储定义
二叉树节点的定义和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针,即有两个指针,分别指向左右孩子。
二、相关题目推荐
1. 144. 二叉树的前序遍历
-
思路一:递归法。前序遍历:中左右。首先确定三要素:
①返回值类型:我们将结果都放在list里,所以不需要返回值了;
②终止条件:当前节点为空时,终止。即遍历到最深处再往上层走;
③递归逻辑:按中左右的顺序遍历,因为我们知道前中后序遍历的“前中序”说的就是中间节点,因此这个中就是当前要输出的节点。class Solution { //前序遍历:中左右 public List<Integer> preorderTraversal(TreeNode root) { List<Integer> rslt=new ArrayList<>(); traversal(root,rslt); return rslt; } //确定返回值 public void traversal(TreeNode cur,List<Integer> rslt){ //确定终止条件 if(cur==null){ return; } //确定每次递归的逻辑 //中即当前节点,把当前节点输出 rslt.add(cur.val); //左 traversal(cur.left,rslt); //右 traversal(cur.right,rslt); } }
-
思路二:迭代法。迭代法其实就是用栈模拟简单的递归。
public List<Integer> preorderTraversal(TreeNode root) { List<Integer> rslt=new ArrayList<Integer>(); if(root==null){ return rslt; } Deque<TreeNode> stack=new ArrayDeque<>(); //★当栈为空且节点为空时遍历结束,root可以看做当前节点 while(!stack.isEmpty()||root!=null){ //中左 while(root!=null){ rslt.add(root.val); stack.push(root); root=root.left; } //右 root=stack.pop(); root=root.right; } return rslt; }
2. 94. 二叉树的中序遍历
-
思路一:递归法。
public List<Integer> inorderTraversal(TreeNode root) { List<Integer> rslt=new ArrayList<Integer>(); traversal(root,rslt); return rslt; } public void traversal(TreeNode cur,List<Integer> rslt) { if(cur==null) return; traversal(cur.left,rslt); rslt.add(cur.val); traversal(cur.right,rslt); }
-
思路二:迭代法。
public List<Integer> preorderTraversal(TreeNode root) { List<Integer> rslt=new ArrayList<Integer>(); if(root==null){ return rslt; } Deque<TreeNode> stack=new ArrayDeque<>(); //★当栈为空且节点为空时遍历结束,root可以看做当前节点 while(!stack.isEmpty()||root!=null){ //中左 while(root!=null){ rslt.add(root.val); stack.push(root); root=root.left; } //右——从栈里弹出来的元素一定是根节点和左子树访问完了的,接下来轮到右子树了 root=stack.pop(); root=root.right; } return rslt; }
3. ★145. 二叉树的后序遍历
-
思路一:递归法。
public List<Integer> postorderTraversal(TreeNode root) { List<Integer> rslt=new ArrayList<Integer>(); traversal(root,rslt); return rslt; } public void traversal(TreeNode cur,List<Integer> rslt){ if(cur==null) return; //后序遍历——左右中 traversal(cur.left,rslt); traversal(cur.right,rslt); rslt.add(cur.val); }
-
思路二:★迭代法。与前序和中序遍历不同,后序遍历(左右中)时需要两次经过一颗子树的根节点。要输出根节点(中、当前节点)有两种情况:
①根节点无右节点(root.right==null);
②根节点的右节点已被访问。
那么如何实现“根节点的右节点已被访问”呢?
——定义一个指针节点pre,用来记录历史访问节点。这样通过判断 root.right 是否等于 pre,如果 root.right == pre,说明上一个循环访问的节点是当前节点的右节点(可以画个满二叉树看看),此时就该输出当前节点了。
public List<Integer> postorderTraversal(TreeNode root) { List<Integer> rslt=new ArrayList<Integer>(); if(root==null) return rslt; Deque<TreeNode> stack=new ArrayDeque<>(); //用来记录历史访问记录 TreeNode pre=null; while(!stack.isEmpty()||root!=null){ //访问左子树 while(root!=null){ stack.push(root); root=root.left; } root=stack.pop(); //如果root的右子树为空,或者右子树访问完了(root.right==pre),输出中 if(root.right==null||root.right==pre){ rslt.add(root.val); //★更新历史访问记录,在下一个循环中,pre代表上一个访问的节点 pre=root; //★一颗子树的左右中都访问完了,该回到上一个父节点(已入栈),把root置空这样下次循环就能拿到栈顶元素的上一个父节点了。 root=null; }else{ //如果root右子树不为空,访问右子树 //★访问右节点前要把当前节点再次入栈!!!因为访问完右还要访问中 stack.push(root); root=root.right; } } return rslt; }
4. 102. 二叉树的层序遍历
-
思路:用队列实现层序遍历。层序遍历是从上到下从左到右一层一层访问节点,而队列这种先进先出的特点刚好满足层序遍历的需求。要做好这道题记住使用队列的模拟过程就好多了。
具体实现:先将根节点入队,然后在队列不为空时遍历二叉树:
每一次大循环代表二叉树的一层遍历,所以要记录队列的长度size,用来在遍历队列时控制要出队几个元素(即每层要访问几个元素)。然后遍历队列,不断出队,并将出队节点不空的左右节点加入队列。
public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> rslt=new ArrayList<List<Integer>>(); if(root==null) return rslt; Deque<TreeNode> queue=new ArrayDeque<>(); queue.offer(root); while(!queue.isEmpty()){ //记录队列长度,遍历时用来控制每层访问几个元素 int size=queue.size(); List<Integer> level=new ArrayList<>(); while(size>0){ root=queue.poll(); level.add(root.val); //出队列时将其左右子节点加入队列 if(root.left!=null){ queue.offer(root.left); } if(root.right!=null){ queue.offer(root.right); } size--; } rslt.add(level); } return rslt; }
5. 107. 二叉树的层序遍历 II
-
思路一:使用ArrayList作为结果列表。先层序遍历,再反转list得到rslt。
public List<List<Integer>> levelOrderBottom(TreeNode root) { List<List<Integer>> list=new ArrayList<List<Integer>>(); if(root==null) return list; Deque<TreeNode> queue=new ArrayDeque<>(); queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); List<Integer> level=new ArrayList<>(); while(size>0){ root=queue.poll(); level.add(root.val); if(root.left!=null){ queue.offer(root.left); } if(root.right!=null){ queue.offer(root.right); } size--; } list.add(level); } //反转list List<List<Integer>> rslt=new ArrayList<List<Integer>>(); for(int i=list.size()-1;i>=0;i--){ rslt.add(list.get(i)); } return rslt; }
-
思路二:使用LinkedList作为结果列表。
如果要求从上到下的层序遍历,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。这道题要求从下到上输出每一层的节点值,在遍历完一层节点之后,只要将存储该层节点值的列表添加到结果列表的头部即可。ArrayList(数组列表)和LinkedList(链表列表)都可以直接在列表头添加元素,但使用链表的列表结构,在链表列表头部添加元素的时间复杂度是 O(1)。public List<List<Integer>> levelOrderBottom(TreeNode root) { List<List<Integer>> rslt=new LinkedList<>(); if(root==null) return rslt; Deque<TreeNode> queue=new ArrayDeque<>(); queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); List<Integer> level=new ArrayList<>(); while(size>0){ root=queue.poll(); level.add(root.val); if(root.left!=null){ queue.offer(root.left); } if(root.right!=null){ queue.offer(root.right); } size--; } //★将每一层节点加到列表头部 rslt.add(0,level); } return rslt; }
6. 199. 二叉树的右视图
-
思路一:使用层序遍历,并只保留每层最后一个节点的值。
public List<Integer> rightSideView(TreeNode root) { List<Integer> rslt=new ArrayList<>(); if(root==null) return rslt; Deque<TreeNode> queue=new ArrayDeque<>(); queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); while(size>0){ root=queue.poll(); if(root.left!=null){ queue.offer(root.left); } if(root.right!=null){ queue.offer(root.right); } //记录每一层的最后一个节点 if(size==1){ rslt.add(root.val); } size--; } } return rslt; }
-
思路二:★使用深度优先搜索的思想,采用中→右→左的顺序,这样每层第一个访问的节点一定是最右边的节点。这与前序遍历(中→左→右)刚好相反,前序遍历每层第一个访问的节点一定是最左边的节点。
★要注意depth的作用:因为每一层只需要最右边的节点,所以二叉树有几层,结果集中就有几个元素。★★因此可以根据当前节点的所在深度(从0开始)是否等于结果集长度来判断当前节点是否该层的第一个节点。class Solution { List<Integer> rslt=new ArrayList<>(); public List<Integer> rightSideView(TreeNode root) { dfs(root,0); return rslt; } public void dfs(TreeNode root,int depth){ if(root==null) return; //中右左 //不是每一个节点都要放进结果集,只把每一层访问的第一个节点放进去。 //★当前节点深度等于rslt长度时,说明当前节点是本层第一个访问的节点,放入结果集 if(depth==rslt.size()){ rslt.add(root.val); } //访问孩子时记得深度要+1了! depth++; dfs(root.right,depth); dfs(root.left,depth); } }
7. 637.二叉树的层平均值
-
思路:层序遍历的时候把一层的节点值求和再取平均。要注意分母不为0,因此size不能直接做为循环变量,减完就成0了。
public List<Double> averageOfLevels(TreeNode root) { List<Double> rslt=new ArrayList<Double>(); Deque<TreeNode> queue=new LinkedList<>(); queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); double sum=0.0; for (int i = 1; i <= size; i++) { root = queue.poll(); sum += root.val; if (root.left != null) { queue.offer(root.left); } if (root.right != null) { queue.offer(root.right); } } //注意上面的for循环不能直接用size做循环变量,因为结束循环后size=0,0不能做除数。 //0做除数会报错:Infinite&nbs***bsp;NaN rslt.add(sum/size); } return rslt; }
8. 429.N叉树的层序遍历
-
思路:依旧是模板题,只不过一个节点有多个孩子罢了。
public List<List<Integer>> levelOrder(Node root) { List<List<Integer>> rslt=new ArrayList<List<Integer>>(); if(root==null) return rslt; Deque<Node> queue=new LinkedList<>(); queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); List<Integer> level=new ArrayList<Integer>(); while(size>0){ root=queue.poll(); level.add(root.val); for(int i=0;i<root.children.size();i++){ queue.offer(root.children.get(i)); } size--; } rslt.add(level); } return rslt; }
9. 515.在每个树行中找最大值
-
思路:层序遍历,取每一层的最大值。
public List<Integer> largestValues(TreeNode root) { List<Integer> rslt=new ArrayList<>(); if(root==null) return rslt; Deque<TreeNode> queue=new ArrayDeque<>(); queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); //★-2^31 <= Node.val <= 2^31 - 1 int max=Integer.MIN_VALUE; while(size>0){ root=queue.poll(); //更新最大值 if (root.val > max) { max = root.val; } if(root.left!=null){ queue.offer(root.left); } if(root.right!=null){ queue.offer(root.right); } size--; } rslt.add(max); } return rslt; }
10. 116. 填充每个节点的下一个右侧节点指针
-
思路一:迭代法。在处理每一个节点时,要分三点:
①如果当前节点node是最后一个节点,则next→null;(不作处理就是null)
②如果node有左孩子(因为是满二叉树,有左必有右),则node.left.next→node.right;
③如果node有兄弟且node不是叶节点,则node的右孩子需要指向node兄弟(即node.next)的左孩子,因为②中兄弟之间都有next连接了,所以可以直接处理:node.right.next=node.next.left。
public Node connect(Node root) { if(root==null) return root; Deque<Node> que=new ArrayDeque<>(); que.offer(root); while(!que.isEmpty()){ int size=que.size(); for(int i=0;i<size;i++){ Node node=que.poll(); //每层最后一个节点指向null(其实每个node不做任何处理next都是null) //node的两个孩子节点:左→右 if(node.left!=null){ que.offer(node.left); que.offer(node.right); node.left.next=node.right; } //★node的右孩子 和 node兄弟的左孩子 如何处理? //如果node不是当前层最后一个节点也不是叶子节点的话,node的右孩子需要指向node兄弟(即node.next)的左孩子,因为兄弟之间都有next连接了,所以可以直接处理 if(node.next!=null && node.right!=null){ node.right.next=node.next.left; } } } return root; }
-
思路二:★递归法。
//★递归逻辑:处理当前节点root public Node connect(Node root) { //终止条件:当前节点root为空或root为叶节点 if(root==null||root.left==null){ return root; } //root不空不为叶节点 //root的左→右 root.left.next=root.right; //如果root有兄弟,root的右→兄弟的左 if(root.next!=null){ root.right.next=root.next.left; } //层序遍历,从左到右 connect(root.left); connect(root.right); //★记得返回! return root; }
-
思路三:通用迭代法。对于一个普通的二叉树也适用。层序遍历,在单层遍历的时候记录一下本层的第一个节点,然后在遍历的时候让前一个节点指向本节点。
public Node connect(Node root) { Queue<Node> queue=new LinkedList<>(); if(root==null) return root; queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); Node preNode=null; Node node=null; //单层遍历时让上一个节点指向当前节点 for(int i=0;i<size;i++){ //如果是每层的第一个节点,他没有上一个节点,但是是别人的上一个节点 if(i==0){ preNode=queue.poll(); //★为了让左右孩子入队,因为下面判断时都是node.left/right node=preNode; }else{ //如果不是第一个节点 node=queue.poll(); //上一个节点指向当前节点 preNode.next=node; //★更新上一个节点 preNode=node; } //左右孩子入队 if(node.left!=null){ queue.offer(node.left); } if(node.right!=null){ queue.offer(node.right); } } } return root; }
11. 117.填充每个节点的下一个右侧节点指针II
-
思路:通用迭代法。本题是普通的二叉树,同上一道题思路三。
public Node connect(Node root) { Queue<Node> queue=new LinkedList<>(); if(root==null) return root; queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); Node preNode=null; Node node=null; //单层遍历时让上一个节点指向当前节点 for(int i=0;i<size;i++){ //如果是每层的第一个节点,他没有上一个节点,但是是别人的上一个节点 if(i==0){ preNode=queue.poll(); //★为了让左右孩子入队,因为下面判断时都是node.left/right node=preNode; }else{ //如果不是第一个节点 node=queue.poll(); //上一个节点指向当前节点 preNode.next=node; //★更新上一个节点 preNode=node; } //左右孩子入队 if(node.left!=null){ queue.offer(node.left); } if(node.right!=null){ queue.offer(node.right); } } } return root; }
12. 104.二叉树的最大深度
-
思路一:二叉树的最大深度等于二叉树的层数。使用层序遍历,每遍历一层 depth + 1 即可。
public int maxDepth(TreeNode root) { Queue<TreeNode> queue=new LinkedList<>(); if(root==null) return 0; queue.offer(root); int depth=0; while(!queue.isEmpty()){ int size=queue.size(); for(int i=0;i<size;i++){ root=queue.poll(); if(root.left!=null) queue.offer(root.left); if(root.right!=null) queue.offer(root.right); } depth++; } return depth; }
-
思路二:递归法。如果我们知道了左子树和右子树的最大深度 leftMaxDepth 和 rightMaxDepth,那么该二叉树的最大深度 = max( leftMaxDepth , rightMaxDepth ) + 1。
public int maxDepth(TreeNode root) { if(root==null) return 0; //★★这里不用判断左右节点是否存在,因为有上面的终止条件 //获取左右子树的最大深度 int leftDepth=maxDepth(root.left); int rightDepth=maxDepth(root.right); //★二叉树的最大深度 = Max(左子树最大深度,右子树最大深度) + 1(加1是因为算上当前中间节点) return Math.max(leftDepth,rightDepth)+1; }
13. ★111. 二叉树的最小深度
注意这道题说的最小深度是指从根节点到最近的叶子节点的最短路径上的节点数量。也就是说,如果一个根节点,左孩子为空,右孩子只有一个节点,那么他的最小深度是1而不是0(因为左孩子为空不是叶子节点)。如图:
-
思路一:层序遍历时遇到左右节点都为空,此时的depth就是最小深度。注意更新depth的位置。
public int minDepth(TreeNode root) { Queue<TreeNode> queue=new LinkedList<>(); if(root==null) return 0; queue.offer(root); int depth=0; while(!queue.isEmpty()){ //★注意depth++的位置,只要开始遍历当前层,就把depth++。最大深度是遍历完才++。 depth++; int size=queue.size(); for(int i=0;i<size;i++){ root=queue.poll(); if(root.left==null&&root.right==null) return depth; if(root.left!=null) queue.offer(root.left); if(root.right!=null) queue.offer(root.right); } } return depth; }
-
思路二:★递归法。
public int minDepth(TreeNode root) { //遇到空节点返回0,表示当前节点的高度为0。 if(root==null) return 0; //获取左子树最小深度 int leftDepth=minDepth(root.left); //获取右子树最小深度 int rightDepth=minDepth(root.right); //★左子树为空右子树不为空,返回右子树最小高度+1 if(root.left==null&&root.right!=null){ return rightDepth+1; } //★同理,右子树为空左子树不为空,返回左子树最小高度+1 if(root.right==null&&root.left!=null){ return leftDepth+1; } //左右子树都不为空,返回二者的最小高度+1 return Math.min(leftDepth,rightDepth)+1; }
14. 226.翻转二叉树
-
思路一:递归法。翻转二叉树其实就是交换左右子节点,可以通过深度优先遍历的方式处理每一个节点。注意中序遍历时,由于是左中右,处理完左(即把左子树翻到了右边)再以后处理右,此时这个右其实是刚才翻过来的左。因此中序遍历时要注意一点,处理完左其实还应该处理“左”。
class Solution { //返回值类型和形参 public TreeNode invertTree(TreeNode root) { //终止条件:空节点返回 if(root==null) return root; //递归逻辑(前序) //中:交换root(当前节点)的左右子节点 TreeNode temp=root.left; root.left=root.right; root.right=temp; //左 invertTree(root.left); //右 invertTree(root.right); return root; } }
public TreeNode invertTree(TreeNode root) { //终止条件:空节点返回 if(root==null) return root; //递归逻辑 //左 invertTree(root.left); //中:交换root(当前节点)的左右子节点 TreeNode temp=root.left; root.left=root.right; root.right=temp; //★右(之前的右成了现在的左) invertTree(root.left); return root; }
-
思路二:层序遍历。
public TreeNode invertTree(TreeNode root) { if(root==null) return root; Queue<TreeNode> queue=new LinkedList<>(); queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); for(;size>0;size--){ TreeNode node=queue.poll(); //交换左右子节点 TreeNode temp=node.left; node.left=node.right; node.right=temp; if(node.left!=null) queue.offer(node.left); if(node.right!=null) queue.offer(node.right); } } return root; }
15. ★101. 对称二叉树
-
思路:要判断一棵二叉树是否是对称二叉树,也就是判断根节点的两棵左右子树是否对称(是否可以由翻转得到另一棵)。要想清楚以下几个问题:
那么如何判断两棵树是否对称呢?
——比较两棵树的内侧和外侧的节点是否相等。
该选择何种遍历方式?
——因为我们是比较的两棵子树的内侧和外侧的节点是否相等,也就是说需要在遍历完左右子节点后将结果返回给当前节点,所以应该用后序遍历(左右 中)。
class Solution { public boolean isSymmetric(TreeNode root) { //判断根节点的左右子树是否对称 return compare(root.left,root.right); } //返回值类型:左右子树是否可翻转; 形参:当前节点的左右子树 public boolean compare(TreeNode left,TreeNode right){ //终止条件: //1. 左为空右不为空,不对称 //2. 左不为空右为空,不对称 //3. 左右都为空,对称但不同继续向下遍历了 //4. 左右都不为空但值不相等,不对称 if(left==null&&right!=null) return false; if(left!=null&&right==null) return false; if(left==null&&right==null) return true; if(left!=null&&right!=null&&left.val!=right.val) return false; //左右不为空且值相等,可能对称且要继续向下遍历 //递归逻辑:判断左右子树是否对称(是否可以翻转) //如何判断两棵子树可以翻转?——同一层对称置的两个节点的外侧节点与外侧节点相同,内侧节点与内侧节点相同,则可以翻转 //比较外侧节点与外侧节点 boolean outNode=compare(left.left,right.right); //比较内侧节点与内侧节点 boolean inNode=compare(left.right,right.left); //内外侧都相同(&&)才可以翻转 return outNode && inNode; } }
16. ★222.完全二叉树的节点个数
-
思路一:对于所有二叉树都适用的方法:
①后序递归遍历。class Solution { //表示统计以root为根节点的树的节点个数。 public int countNodes(TreeNode root) { if(root==null) return 0; int countLeft=countNodes(root.left); int countRight=countNodes(root.right); //+1是当前节点,比如说传进来的root是叶子节点,表示计算以root为根的树的节点个数(1个)。上面的countLeft和countRight都是0,返回以root为根的树的节点个数=0+0+1。 return countLeft+countRight+1; } }
②层序遍历统计每个节点(不推荐)。class Solution { public int countNodes(TreeNode root) { if(root==null) return 0; Deque<TreeNode> que=new ArrayDeque<>(); que.offer(root); int count = 0; while (!que.isEmpty()) { int size = que.size(); for (int i = 0; i < size; i++) { TreeNode node = que.peek(); que.poll(); count++; // 记录节点数量 if (node.left!=null) que.offer(node.left); if (node.right!=null) que.offer(node.right); } } return count; } }
-
思路二:★利用完全二叉树和满二叉树的性质。完全二叉树只有两种情况,要么是满二叉树,要么最后一层叶子节点没有满。
对于满二叉树的节点个数,可以直接用公式: 2 ^ 树的深度 - 1 来计算(注意这里根节点深度为1);
对于第二种情况,分别递归左子树和右子树,递归到某一深度一定会有左子树或者右子树为满二叉树,然后这部分可以用满二叉树的节点计算公式来计算。
【如何判断完全二叉树的一个子树是不是满二叉树呢?】
——一直往左深度遍历得到最左侧深度,一直往右深度遍历得到最右侧深度:如果最左侧深度等于最右侧深度,则该子树是满二叉树(如下面第一第二幅图)。因为前提是完全二叉树,所以不会出现下图三虽然左右深度相同但不是满二叉树的情况。
【具体实现】判断其子树是不是满二叉树,如果是则利用公式计算这个满二叉子树的节点个数;如果不是,继续递归。★★要注意终止条件有两个!空节点返回和满二叉子树返回!class Solution { //1. 返回值类型和形参 public int countNodes(TreeNode root) { //2. 终止条件 //2.1 空节点返回0 if(root==null) return 0; //2.2 子树是满二叉树,返回子树节点个数 //左侧深度;右侧深度(我们规定根节点深度为0) int leftDepth=0; int rightDepth=0; TreeNode leftNode=root.left; TreeNode rightNode=root.right; //一直往左深度遍历,得到左侧深度 while(leftNode!=null){ leftNode=leftNode.left; leftDepth++; } //一直往右深度遍历,得到右侧深度 while(rightNode!=null){ rightNode=rightNode.right; rightDepth++; } //左侧深度等于右侧深度,说明是满二叉树,直接根据公式返回满二叉树的节点个数 if(leftDepth==rightDepth) return (2<<leftDepth)-1;//★相当于2^(leftDepth+1)-1 //3. 单层递归逻辑 int countLeft=countNodes(root.left); int countRight=countNodes(root.right); return countLeft+countRight+1; } }
17. ★110.平衡二叉树
-
思路:每一个节点的左右子树都满足高度差绝对值不超过1的二叉树才是平衡二叉树。我们需要计算当前节点左右子树的高度(即树的最大深度),通过判断来确定以当前节点为根节点的二叉树是不是平衡二叉树。★因为高度肯定不能是-1,所以我们用-1来表示非平衡二叉树,如果通过判断发现不是平衡二叉树,我们就没有必要返回树的高度了,直接返回-1即可。
class Solution { public boolean isBalanced(TreeNode root) { //计算树的高度,根据高度判断是否平衡 return getTreeHigh(root)!=-1; } //用来计算树的高度并判断是不是平衡二叉树,用-1来表示非平衡二叉树 public int getTreeHigh(TreeNode root){ //终止条件 if(root==null) return 0; int leftHigh=getTreeHigh(root.left); //★如果当前树的左子树高度为-1,说明左子树不是平衡二叉树,那么当前树也不是平衡二叉树,当前树就返回-1表示自己不是平衡二叉树 if(leftHigh==-1) return -1; int rightHigh=getTreeHigh(root.right); //同上 if(rightHigh==-1) return -1; //左右子树都是平衡二叉树,看二者高度差 if(Math.abs(leftHigh-rightHigh)<=1){ //左右子树高度差绝对值不超过1,当前树是平衡二叉树,返回当前树的高度 return Math.max(leftHigh,rightHigh)+1; }else{ ★//左右子树高度差绝对值超过1了,当前树不是平衡二叉树,返回-1 return -1; } } }
18. ★★257. 二叉树的所有路径
-
思路:要求从根节点到叶子节点的路径,需要让父节点指向孩子节点,很明显需要前序遍历,这样才能找到对应的路径。
★在这道题目中将第一次涉及到回溯,因为我们要把路径记录下来,需要回溯来回退一个路径再进入另一个路径。以前序遍历为例演示回溯的过程:
其实回溯和递归是一一对应的,有一个递归,就要有一个回溯。回溯类似于在递归时 return 到程序执行的上一个位置。
这道题细节很多,比如终止条件怎么写、中左右的递归逻辑怎么写、是否为null的判断等,对着代码和注释多琢磨,多手动模拟模拟!
class Solution { public List<String> binaryTreePaths(TreeNode root) { List<String> rslt=new ArrayList<>(); //用来记录单条路径 List<Integer> path=new ArrayList<>(); getPath(root,rslt,path); return rslt; } //前序递归遍历获取每一条路径,并加入结果集 public void getPath(TreeNode root,List<String> rslt,List<Integer> path) { //中——把当前节点加入path中(★因为终止条件比较特殊,是叶子节点,而叶子节点也需要加到path中,所以这一步要在终止条件之前) path.add(root.val); //★终止条件:遇到叶子节点就停止递归(因为空节点不需要加到路径中) if(root.left==null&&root.right==null){ //root为叶子节点说明当前路径到头了,此时需要将path中保存的当前路径的节点按要求拼成字符串存到结果集中 StringBuilder sb=new StringBuilder(); //路径终点不需要->,需要单独处理,因此这里只遍历到倒数第二个元素即可 for(int i=0;i<path.size()-1;i++){ sb.append(String.valueOf(path.get(i))+"->"); } //路径终点单独处理 sb.append(String.valueOf(path.get(path.size()-1))); //一条路径完毕,加到结果集中 rslt.add(sb.toString()); return; } //递归逻辑——前序遍历 //中(在最前面,整体上还是中左右的逻辑顺序) //左 if(root.left!=null){ getPath(root.left,rslt,path); //★path只是单条路径,不能一直往里面加节点,因此还需要从里面删节点。这里就要用到回溯。 //★回溯和递归是一一对应的,有一个递归,就要有一个回溯。 path.remove(path.size()-1); } //右 if(root.right!=null){ getPath(root.right,rslt,path); //★回退(回溯) path.remove(path.size()-1); } } }
19. 404.左叶子之和
-
思路:首先这道题要注意是判断左叶子,不是左侧节点。
什么是左叶子?
——是父节点的左孩子的叶子节点,如图:
★之前无论是递归还是迭代,我们判断的都是当前节点是否满足条件,而现在要判断当前节点的左孩子是否满足条件(是否是叶子节点)。因此直接判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。
【具体实现】递归的遍历顺序为后序遍历(左右中),因为要通过递归函数的返回值来累加求取左叶子数值之和。
①如果当前节点有左叶子的话,记录左叶子的值,再计算右子树左叶子之和即可;
②如果左孩子不是左叶子的话,分别计算左、右子树左叶子之和,加起来就是该二叉树的左叶子之和。
class Solution { //求以root为根的二叉树的左叶子之和 public int sumOfLeftLeaves(TreeNode root) { //终止条件 if(root==null) return 0; //递归逻辑 //如果当前节点有左节点且左节点为叶子节点的话,记录该左叶子的值 int rslt=0; if(root.left!=null&&root.left.left==null&&root.left.right==null){ rslt=root.left.val; //★左节点为叶子节点,不需要递归左边了,再知道右子树的左叶子之和即可 return sumOfLeftLeaves(root.right)+rslt; } //左节点不是叶子节点 //以root为根的二叉树的左叶子之和=左子树左叶子之和+右子树左叶子之和 return sumOfLeftLeaves(root.left)+sumOfLeftLeaves(root.right); } }
20. 513.找树左下角的值
-
思路:层序遍历,返回最后一层最左边的值。
class Solution { public int findBottomLeftValue(TreeNode root) { //层序遍历,返回最后一层最左边的值 //记录当前层第一个节点的值,层序遍历时,更新rslt,层序遍历结束时,rslt记录的就是最后一层最左边的值,即左下角的值。 int rslt=0; //题目规定至少有一个节点 Deque<TreeNode> queue=new ArrayDeque<>(); queue.offer(root); while(!queue.isEmpty()){ int size=queue.size(); for(int i=0;i<size;i++){ root=queue.poll(); //获取当前层第一个节点的值 if(i==0) rslt=root.val; if(root.left!=null) queue.offer(root.left); if(root.right!=null) queue.offer(root.right); } } return rslt; } }
-
优化:层序遍历是从左到右,也就是说每层最后遍历的节点是最右侧的节点,那么最后出队的元素就是最右侧的节点。现在要求最后一层第一个节点(最左侧),我们就从右往左遍历,这样直接返回最后出队的节点的值即可。
class Solution { public int findBottomLeftValue(TreeNode root) { Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while(!queue.isEmpty()){ root = queue.poll(); //从右往左 if (root.right != null) queue.offer(root.right); if (root.left != null) queue.offer(root.left); } return root.val; } }
21. 112. 路径总和
-
思路:参考上面的【18. 257. 二叉树的所有路径】,在找每一条路径时,记录该路径节点之和。
class Solution { List<Integer> path=new ArrayList<>(); public boolean hasPathSum(TreeNode root, int targetSum) { if(root==null) return false; path.add(root.val); if(root.left==null&&root.right==null){ int sum=0; //记录该路径节点之和 for(int i=0;i<path.size();i++){ sum+=path.get(i); } if(sum==targetSum) return true; } //★不能放下面的if里面,那就成if的局部变量了。而且初始化时要置为false,因为 boolean leftPath=false; boolean rightPath=false; if(root.left!=null){ leftPath=hasPathSum(root.left,targetSum); path.remove(path.size()-1); } if(root.right!=null){ rightPath=hasPathSum(root.right,targetSum); path.remove(path.size()-1); } // return leftPath||rightPath; } }
★【优化】:上面用了List来保存路径节点之和,然后与目标值进行比较,这样额外用了List空间。可以通过每经过一个节点,就用目标值减去该节点值的方法,最后与0进行判断即可。class Solution { public boolean hasPathSum(TreeNode root, int targetSum) { //终止条件 if(root==null) return false; targetSum=targetSum-root.val; //叶子节点表示到路径终点了,同时如果targetSum减到0了说明路径之和满足目标值了,返回true if(root.left==null&&root.right==null){ if(targetSum==0) return true; } //递归逻辑 if(root.left!=null){ boolean leftPath=hasPathSum(root.left,targetSum); //如果递归函数返回true,说明找到了合适的路径,应该立刻返回true。(包含了回溯的思想,这里的targetSum回到了当前的值) if(leftPath) return true; } if(root.right!=null){ boolean rightPath=hasPathSum(root.right,targetSum); if(rightPath) return true; } //如果往左和往右都没找到合适路径,就返回false return false; } }
22. ★113.路径之和II
-
思路:这道题就是在【18. 257. 二叉树的所有路径】的基础上多了对路径节点之和与目标值的判断。
注意值传递和引用传递在递归中的区别!!!
——值传递相当于复制了一份,所以别的递归层次对targetSum的修改不影响当前递归的targetSum,因此不需要对targetSum进行回溯。引用传递是对内存地址进行传递,所以多次递归操作的是都是同一块内存地址,所以需要对path进行回溯。
class Solution { List<List<Integer>> rslt=new ArrayList<List<Integer>>(); List<Integer> path=new ArrayList<Integer>(); public List<List<Integer>> pathSum(TreeNode root, int targetSum) { if(root==null) return rslt; getPath(root,targetSum); return rslt; } public void getPath(TreeNode root,int targetSum){ //★对中的操作一定要放在if判断的前面,不然叶子节点就加不到path里了! path.add(root.val); targetSum-=root.val; if(root.left==null&&root.right==null){ if(targetSum==0){ rslt.add(new ArrayList(path)); } return; } //★不能保证左右节点一定存在,所以要先进行非空判断! if(root.left!=null){ getPath(root.left,targetSum); path.remove(path.size()-1); } if(root.right!=null){ getPath(root.right,targetSum); path.remove(path.size()-1); } return; } }
23. ★106.从中序与后序遍历序列构造二叉树
-
思路:后序或前序+中序=>构建二叉树。以中序+后序构建二叉树为例,原理是根据中序和后序的特点:
中序数组:[左子树结点,根结点,右子树结点]
后序数组:[左子树结点,右子树结点,根结点]
具体实现:
-
第一步:如果任一数组大小为零,说明是空节点(null)了。
-
第二步:如果不为空,那么后序数组最后一个元素作为节点元素(根节点)。
-
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点。
-
第四步:根据切割点切割中序数组,切成中序左数组和中序右数组。
-
第五步:根据中序左数组长度切割后序数组,切成后序左数组和后序右数组。(因为中序左数组长度=后序左数组长度)
-
第六步:递归处理左区间和右区间,获取左右子节点
★在递归的过程中要始终保持区间一致!
class Solution { //★用来保存中序数组inorder中各元素及对应的下标,不用每次都用循环去找root在中序数组中的下标 Map<Integer,Integer> map; public TreeNode buildTree(int[] inorder, int[] postorder) { map=new HashMap<>(); //保存中序数组inorder中各元素及对应的下标 for(int i=0;i<inorder.length;i++){ map.put(inorder[i],i); } return findNode(inorder,0,inorder.length,postorder,0,postorder.length); } //根据中序和后序数组,找出根节点及其左右子节点。 //切割数组采用start和end指针来标记,而不是真正把数组切割:通过inorder,inStart,inEnd确定切割后的数组。 public TreeNode findNode(int[] inorder,int inStart,int inEnd,int[] postorder,int postStart,int postEnd) { //终止条件:如果任一数组里没有元素了,说明是空节点 if(inStart>=inEnd||postStart>=postEnd) return null; //递归逻辑 //1.根据后序数组的最后一个元素构建根节点 int rootVal=postorder[postEnd-1]; TreeNode root=new TreeNode(rootVal); //2.根据后序切割中序——找的中序数组中根节点的位置,以此为基准切割中序数组 int rootIdx=map.get(rootVal); //3.根据中序切割后序——记录中序数组切割后的左子树节点的长度,用来切割后序数组 int leftLength=rootIdx-inStart; //4.构建左子节点 root.left=findNode(inorder,inStart,rootIdx,postorder,postStart,postStart+leftLength); //5.构建右子节点 root.right=findNode(inorder,rootIdx+1,inEnd,postorder,postStart+leftLength,postEnd-1); //记得返回根节点!!! return root; } }
-
-
相似题目:
105.从前序与中序遍历序列构造二叉树class Solution { Map<Integer,Integer> map; public TreeNode buildTree(int[] preorder, int[] inorder) { map=new HashMap<>(); for(int i=0;i<inorder.length;i++){ map.put(inorder[i],i); } //左闭右开 return findNode(preorder,0,preorder.length,inorder,0,inorder.length); } public TreeNode findNode(int[] preorder,int preStart,int preEnd,int[] inorder,int inStart,int inEnd) { if(preStart>=preEnd||inStart>=inEnd) return null; int rootVal=preorder[preStart]; TreeNode root=new TreeNode(rootVal); int rootIdx=map.get(rootVal); int leftLen=rootIdx-inStart; root.left=findNode(preorder,preStart+1,preStart+leftLen+1,inorder,inStart,rootIdx); root.right=findNode(preorder,preStart+leftLen+1,preEnd,inorder,rootIdx+1,inEnd); return root; } }
654. 最大二叉树class Solution { //左闭右开 public TreeNode constructMaximumBinaryTree(int[] nums) { return findNode(nums,0,nums.length); } public TreeNode findNode(int[] nums,int start,int end){ //数组为空,空节点 if(start>=end) return null; //找到当前数组的最大值 int maxIdx=findMaxIdx(nums,start,end); TreeNode root=new TreeNode(nums[maxIdx]); //递归处理左部分和右部分 root.left=findNode(nums,start,maxIdx); root.right=findNode(nums,maxIdx+1,end); return root; } public int findMaxIdx(int[] nums,int start,int end){ int maxIdx=-1; int max=-1; for(int i=start;i<end;i++){ if(max<nums[i]){ max=nums[i]; maxIdx=i; } } return maxIdx; } }
24. 617.合并二叉树
-
思路:同时遍历两个二叉树其实和遍历一个树的逻辑是一样的,只不过传入两个树的节点,同时操作。
class Solution { //形参和返回值类型:要合并两个二叉树,那么参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。 public TreeNode mergeTrees(TreeNode root1, TreeNode root2) { //终止条件 if(root1==null&&root2==null) return null; if(root1==null) return root2; if(root2==null) return root1; //递归逻辑——前序同时遍历两个二叉树 TreeNode root=new TreeNode(root1.val+root2.val); root.left=mergeTrees(root1.left,root2.left); root.right=mergeTrees(root1.right,root2.right); return root; } }
25. 700.二叉搜索树中的搜索
整体思路就是根据二叉搜索树是一个有序树的性质:
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 左、右子树也分别为二叉搜索树
-
思路一:迭代法(推荐)。
class Solution { public TreeNode searchBST(TreeNode root, int val) { if(root==null) return root; while(root!=null){ if(root.val>val){ root=root.left; }else if(root.val<val){ root=root.right; }else{ return root; } } //val不在树中 return null; } }
-
思路二:递归法。
class Solution { //前序递归遍历 public TreeNode searchBST(TreeNode root, int val) { if(root==null) return null; if(root.val==val) return root; TreeNode rslt=null; if(root.val>val) rslt=searchBST(root.left,val); if(root.val<val) rslt=searchBST(root.right,val); return rslt; } }
26. ★98.验证二叉搜索树
二叉搜索树的中序遍历是严格递增序列!
-
思路一:先递归求出中序遍历的序列,再通过循环判断该序列是否严格递增。(耗时长,不推荐)
class Solution { List<Integer> list=new ArrayList<>(); public boolean isValidBST(TreeNode root) { //获取中序遍历序列 inOrder(root); //判断中序遍历序列是否严格单调递增 for(int i=0;i<list.size();i++){ if(i>0&&list.get(i)<=list.get(i-1)){ return false; } } return true; } //获取中序遍历序列 public void inOrder(TreeNode root){ if(root==null) return; inOrder(root.left); list.add(root.val); inOrder(root.right); } }
-
★思路二:以上代码中,我们先求出了中序序列,然后再判断是否为严格递增的。但其实可以在中序递归遍历的过程中直接判断是否严格递增。
通过记录上一个节点 pre,与当前节点的值进行比较来判断是否严格递增。
在 左中右 的中序遍历中,“右”的上一个节点是“中”,所以“中”之后要更新 pre。
class Solution { //★用来记录上一个节点 TreeNode pre=null; //中序遍历并判断是否严格递增 public boolean isValidBST(TreeNode root) { //终止条件:二叉搜索树可以为空! if (root == null) return true; // 左 boolean left = isValidBST(root.left); // ★中 if (pre != null && root.val <= pre.val) {//不满足严格递增。★要注意判断pre不为空,因为可能是序列中的第一个节点,他没有上一个节点。 return false; } pre = root;//记录上一个节点。★因为是中序遍历左中右,所以中序序列中,“右”的上一个节点是“中”,所以“中”之后要更新pre! // 右 boolean right = isValidBST(root.right); return left&&right; } }
27. 530.二叉搜索树的最小绝对差
-
思路一:把二叉搜索树转化成中序数组,再比较相邻两元素的最小绝对差。
class Solution { List<Integer> list=new ArrayList<>(); public int getMinimumDifference(TreeNode root) { inorder(root); int min=Integer.MAX_VALUE; for(int i=1;i<list.size();i++){ min=Math.min(min,Math.abs(list.get(i)-list.get(i-1))); } return min; } public void inorder(TreeNode root){ if(root==null) return; inorder(root.left); list.add(root.val); inorder(root.right); } }
-
思路二:★遇到在二叉搜索树上求最值,求差值之类的题目,要考虑能否用二叉搜索树是有序的这一特点。其实就是将二叉搜索树转成中序有序数组,而在中序递归遍历过程中记录前后两个节点指针,就能实现中序有序数组中两两相邻元素的比较。
class Solution { int min=Integer.MAX_VALUE; //用来记录上一个节点 TreeNode pre=null; public int getMinimumDifference(TreeNode root) { inorder(root); return min; } //中序遍历,每次比较【当前节点与前一节点差值】与最小值min的大小,将较小的保存在min中。 public void inorder(TreeNode root){ if(root==null) return; inorder(root.left); if(pre!=null){ //★因为中序得到的已经是递增的了,所以root.val一定比pre.val大,所以不需要abs了 //min=Math.min(min,Math.abs(pre.val-root.val)); min=Math.min(min,root.val-pre.val); } //记录上一个节点 pre=root; inorder(root.right); } }
28. ★501. 二叉搜索树中的众数
-
思路:遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。这又想到二叉搜索树的中序序列有序+递归中比较两个节点。
注意“中”的处理逻辑。
class Solution { List<Integer> list=new ArrayList<>(); //用来记录当前统计的值的频率 int count=0; //用来记录最大频率 int maxCount=0; TreeNode pre=null; public int[] findMode(TreeNode root) { inorder(root); int[] rslt=new int[list.size()]; //list->int[] rslt for(int i=0;i<list.size();i++){ rslt[i]=list.get(i); } return rslt; } public void inorder(TreeNode root){ //终止条件 if(root==null) return; //左 inorder(root.left); //中★ if(pre==null){//★第一个节点 count=1; }else if(root.val==pre.val){//当前节点值=上一个节点值 count++; }else{//★当前节点值不等于上一个节点值,重新统计当前节点的出现频率 count=1;//为什么不是0呢?因为现在已经统计到了当前节点,已经出现一次了,所以是1不是0 } //如果统计的频率count比最高频率maxCount大 if(count>maxCount){ //★清空结果集,因为结果集之前的元素都失效了。 list.clear(); //将新值加到结果集中 list.add(root.val); //更新最高频率 maxCount=count; }else if(count==maxCount){//如果统计的频率count与最高频率maxCount相等,说明树中有不止一个众数 //将新众数加到结果集中 list.add(root.val); } pre=root; //右 inorder(root.right); } }
29. 236. 二叉树的最近公共祖先
-
思路:要判断节点A是不是公共祖先,显然需要先遍历他的左右子节点,所以可以确定用后序遍历。
那么如何判断一个节点是节点q和节点p的公共祖先呢?
情况①:如果找到一个节点,发现左子树出现结点p,右子树出现节点q(或者左子树出现结点q,右子树出现节点p),那么该节点就是节点p和q的最近公共祖先(如下图)。那么我们在递归后序遍历时,遇到空节点就返回空(表示没出现pq);遇到p或者q就返回p或者q(表示遇到了p或者q)。然后通过判断节点A的左右子树的返回值,如果左右子树的返回值都不为空,说明A是pq的最近公共祖先,将A返回(此时的返回值代表公共祖先了)。因为题目说了节点数值不重复,而且一定存在 q 和 p,所以不会出现左子树返回p右子树也返回p的情况。
★找到公共祖先了如何把公共祖先一层一层返回上去呢?
——比如下图4是公共祖先,对于节点6来说它的左子树遍历返回的是公共祖先4,右子树遍历返回的空(因为没遇到pq),此时我们判断左不为空且右为空,说明左返回的是公共祖先,将左返回即可。
情况②:节点本身就是公共祖先(如下图)。如果是这种情况,都不需要遇到另一个p或者q就可以解决了。因为节点4为q,满足终止条件,节点6的左子树返回节点4;而节点6的右子树返回空,所以满足左不为空且右为空的条件,返回左,可以看到都没遍历到p就找到了公共祖先q。
class Solution { //返回值:pq的最近公共祖先 //参数:根节点,pq public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { //终止条件 if(root==null) return null; if(root==p||root==q) return root;//遇到p或q就返回p或q //递归逻辑——后序 //左 //left记录了左子树有没有出现p或q,为空表示没出现,不为空表示出现了p或q TreeNode left=lowestCommonAncestor(root.left,p,q); //右 TreeNode right=lowestCommonAncestor(root.right,p,q); //★中——分三种情况 //1.左右都不为空,此时root就是最近公共祖先 if(left!=null&&right!=null) return root; //2.其中一个为空,另一个不为空。说明需要往上传递公共祖先了,返回不是空的那个节点。 if(left==null&&right!=null) return right; if(left!=null&&right==null) return left; //3.左右都为空,说明root不是公共祖先,返回null return null; } }
30. 235. 二叉搜索树的最近公共祖先
-
思路:利用二叉搜索树的性质来做。
-- 如果pq比当前节点都小,说明pq一定在当前节点的左子树,此时往左遍历;
-- 如果pq比当前节点都大,说明pq一定在当前节点的右子树,此时往右遍历;
-- 如果当前节点在pq之间,则当前节点一定是最近公共祖先。
因为我们可以根据二叉搜索树的有序性质来规定遍历的顺序,所以就不需要考虑要选择哪种遍历顺序了。
class Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { //pq比当前节点都小,说明pq一定在当前节点的左子树,因此往左遍历 if(root.val>p.val&&root.val>q.val){ return lowestCommonAncestor(root.left,p,q); } //pq比当前节点都大,生命pq一定在当前节点的右子树,因此往右遍历 if(root.val<p.val&&root.val<q.val){ return lowestCommonAncestor(root.right,p,q); } //else: (root.val>p.val&&root.val<q.val)||(root.val>q.val&&root.val<p.val) //即当前节点在pq之间,一定是最近公共祖先 return root; } }
31. 701.二叉搜索树中的插入操作
-
思路:只需要按照二叉搜索树的有序规则去遍历,遇到空节点就插入新节点就可以了。因为我们是按照有序规则去遍历,最后一定能找到唯一一个空节点,也就是插入位置。所以这道题类似递归建树的过程。
class Solution { //递归建树 public TreeNode insertIntoBST(TreeNode root, int val) { //如果遇到空节点了,说明找到了插入位置,直接返回新节点 if(root==null) return new TreeNode(val); //递归创建右子树 if(val>root.val){ root.right=insertIntoBST(root.right,val); } //递归创建左子树 if(val<root.val){ root.left=insertIntoBST(root.left,val); } return root; } }
32. ★★450.删除二叉搜索树中的节点
-
思路:删除二叉搜索树的节点,不是单纯把目标节点删除了那么简单,因为有序性,删除后还需要对树的结构进行调整,使其仍满足二叉搜索树。
目标节点有五种情况:
①目标节点为空,说明找不到目标节点;
②目标节点为叶子节点,即左为空右也为空,此时直接删除目标节点,即让目标节点的父节点的相应指针指向null;
③目标节点左为空右不为空,此时删除目标节点后,要让目标节点的父节点的右指针指向目标节点的右孩子;
④目标节点左不为空右为空,同③,删除后要让目标节点的父节点的左指针指向目标节点的左孩子;
⑤目标节点左右都不为空,这也是最复杂的情况,此时删除后对树结构有两种调整方式:
-- 让目标节点的左孩子代替目标节点,目标节点的右子树要挂在目标节点左子树中最大节点的右侧,也就是要挂在目标节点左子树最右边的位置上。
-- 让目标节点的右孩子代替目标节点,目标节点的左子树要挂在目标节点右子树中最小节点的左侧,也就是要挂在目标节点右子树最左边的位置上。(如下图所示)
如何实现让目标节点的父节点指向相应的节点呢?
——其实这跟递归建树是一样的,这里相当于把新的节点返回给上一层,上一层就要用 root.left 或者 root.right接住。
class Solution { public TreeNode deleteNode(TreeNode root, int key) { //终止条件 //1.目标节点为空,说明找不到目标节点 if(root==null) return null; //找到目标节点了,要进行删除了 if(key==root.val){ //2.目标节点左为空右也为空,此时直接删除目标节点 if(root.left==null&&root.right==null){ return null; //3.目标节点左为空右不为空,要让目标节点的父节点的右指针指向目标节点的右孩子; }else if(root.left==null&&root.right!=null){ return root.right; //4.目标节点左不为空右为空,同③,删除后要让目标节点的父节点的左指针指向目标节点的左孩子; }else if(root.left!=null&&root.right==null){ return root.left; //5.★目标节点左右都不为空:root.left!=null&&root.right!=null }else{ //5.1 右孩子代替目标节点时,先让目标节点左子树挂在其右子树的最左下角 TreeNode temp=root.right; while(temp.left!=null){ temp=temp.left; } temp.left=root.left; //5.2 再让右孩子代替目标节点,此时相当于"3.目标节点左为空右不为空"的情况 return root.right; } } //递归逻辑——找目标节点的过程 if(key>root.val){ root.right=deleteNode(root.right,key); } if(key<root.val){ root.left=deleteNode(root.left,key); } return root; } }
33. 669. 修剪二叉搜索树
-
思路:root可能在区间 [low,high] 内,也可能不在,因此有两种情况:
①如果root在区间内,说明root是要保留的,继续处理root的左右子树;
②如果root不在区间内:
-- 如果root比low还小,根据二叉搜索树的性质,说明root的左子树一定都小于low,所以只处理右子树即可。注意要将处理后的右子树的根节点返回给当前节点的父节点,所以要return,相当于删除了当前节点!
-- 如果root比high还大,同理,说明root的右子树一定都大于high,所以只处理左子树即可。
class Solution { public TreeNode trimBST(TreeNode root, int low, int high) { //终止条件 if(root==null) return null; //1.如果root不在区间内,又分两种情况: //1.1 root比low还小,说明root左子树一定都小于low,所以只处理右子树即可 if(root.val<low){ return trimBST(root.right,low,high); } //1.2 root比high还大,说明root右子树一定都大于high,所以只处理左子树即可 if(root.val>high){ return trimBST(root.left,low,high); } //2.如果root在区间[low,high]中,处理左右子树 root.left=trimBST(root.left,low,high); root.right=trimBST(root.right,low,high); return root; } }
34. 108.将有序数组转换为二叉搜索树
-
思路:构造二叉树的本质其实就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。对于用有序数组构造平衡二叉搜索树,寻找分割点就比较容易了,分割点就是数组中间位置的节点。
如果数组长度为偶数,中间节点有两个,应该取哪一个作为根节点?
——取哪一个都可以,只不过构成了不同的平衡二叉搜索树,题目要求答案可以不唯一,只要是平衡的二叉搜索树即可。
注意区间不变原则——我下面用的是左闭右闭。
注意终止条件——在左闭右闭的前提下,当left大于right时,说明当前分割后的数组没有元素了,返回空节点。如果是左闭右开,当left>=right时说明没有元素了(其实二者等于时就没有元素了)。
class Solution { public TreeNode sortedArrayToBST(int[] nums) { //左闭右闭 int left=0; int right=nums.length-1; return buildBST(nums,left,right); } public TreeNode buildBST(int[] nums,int left,int right){ //终止条件:★数组里没元素了,返回空节点 if(left>right) return null; //递归逻辑:中间元素作为根节点,递归建树 int mid=left+(right-left)/2; TreeNode root=new TreeNode(nums[mid]); root.left=buildBST(nums,left,mid-1); root.right=buildBST(nums,mid+1,right); return root; } }
35. 538.把二叉搜索树转换为累加树
-
思路一:这道题的意思就是从二叉搜索树最大节点开始,从大到小累加,一直累加到最小节点。我们知道中序遍历(左中右)二叉搜索树的序列是从小到大的,那么反过来,以右中左的顺序去遍历二叉搜索树得到的序列就是从大到小的了。因此,这道题可以用右中左的顺序遍历二叉搜索树,将上一个节点的值加到当前节点上即可。注意!对于最大的节点,也就是序列的第一个节点来说,他只需要加 0 即可。(所以定义的全局变量num的初始值是0)
class Solution { //★8+0=8 int num=0;//记录上一个节点的值,对于第一个节点来说,他的上一个节点是null,值是0! public TreeNode convertBST(TreeNode root) { //终止条件 if(root==null) return null; //递归逻辑 //右 root.right=convertBST(root.right); //中 root.val+=num;//将上一个节点的值加到当前节点 num=root.val;//更新上一个节点的值 //左 root.left=convertBST(root.left); return root; } }
-
思路二:整体思路与思路一相同,只是用指针来表示上一个节点。
class Solution { //用来记录上一个节点,对于第一个节点来说,他的上一个节点为空,但是值为0。所以这里我们不能写pre=null! TreeNode pre=new TreeNode(0); public TreeNode convertBST(TreeNode root) { //终止条件 if(root==null) return null; //递归逻辑 //右 root.right=convertBST(root.right); //中 root.val+=pre.val; pre.val=root.val; //左 root.left=convertBST(root.left); return root; } }