总结二叉树相关的算法题,包括常见的遍历代码模板,持续更新中。
目录
- 二叉树的遍历
- 二叉树属性
- 对称二叉树
- 完全二叉树
- 平衡二叉树
- 二叉树深度问题
- 二叉树路径问题
- LeetCode 257. 二叉树的所有路径(返回所有从根节点开始的路径)
- LeetCode 112. 路径总和(判断该树中是否存在 根节点到叶子节点 的路径)
- LeetCode 113. 路径总和 II(找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径)
- LeetCode 437. 路径总和 III(求该二叉树里节点值之和等于 targetSum 的 路径 的数目)
- LeetCode 543. 二叉树的直径(树中任意两个节点之间最长路径的长度,可以不经过根节点)
- LeetCode 124. 二叉树中的最大路径和(2023/3/29 快手实习推荐算法一面)
- LeetCode 687. 最长同值路径(还没做)
- 二叉树修改与构造
- 二叉搜索树属性
- 二叉搜索树修改与构造
- 公共祖先问题
二叉树的遍历
二叉树的前中后序遍历
递归方法
//前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<Integer>();//用res来存遍历结果
preOrder(res,root);
return res;
}
public void preOrder(List<Integer> res,TreeNode root){
if(root==null)return;//递归终止条件,遍历到空节点就返回上一层
res.add(root.val);//访问结点(这句位置决定递归顺序)
preOrder(res,root.left);
preOrder(res,root.right);
}
}
//中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
inOrder(res,root);
return res;
}
public void inOrder(List<Integer> res,TreeNode root){
if(root==null)return;
inOrder(res,root.left);
res.add(root.val);
inOrder(res,root.right);
}
}
//后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new LinkedList<>();
postOrder(res,root);
return res;
}
public void postOrder(List<Integer> res,TreeNode root){
if(root==null)return;
postOrder(res,root.left);
postOrder(res,root.right);
res.add(root.val);
}
}
迭代方法(未统一写法)
因为递归调用会把函数的局部变量、参数值和返回的地址入栈,return的时候从栈顶弹出上次的各项参数,所以递归可以返回上一层位置(实质是用栈记录下了上一层的信息)。所以三种遍历方式可以用栈来模拟递归的过程,也就是所谓的迭代(递归是一环套一环的执行,而迭代是按照一定的逻辑顺序向前推进)。
·前序迭代
前序遍历的顺序是根->左->右,需要先处理根结点(根节点先入栈,在后续每一次处理中,都将栈顶元素出栈,加入res中),然后将右孩子入栈,再将左孩子入栈,这样可以保证出栈的顺序是左孩子在右孩子前;
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res= new LinkedList<>();
Deque<TreeNode> stack=new LinkeList<>();//栈里面存的是TreeNode类型的结点
if(root!=null)stack.push(root);//如果空树,什么都不管,栈就是空的,最后直接返回空的res
//前序遍历顺序(根左右),用栈模拟要根先入栈再出栈,然后右结点入栈,左节点入栈,这样出栈顺序就符合
while(!stack.isEmpty()){
root=stack.poll();
res.add(root.val);
if(root.right!=null)stack.push(root.right);
if(root.left!=null)stack.push(root.left);
}
return res;
}
}
·中序迭代
做完了前序迭代,肯定是想稍微改改前序迭代代码就套上中序,但这样是不行的。根源就在于对根结点的访问和处理上,前序的顺序是中左右,遍历过程中最先访问(遍历)的是根结点,每次循环中最先处理(出栈加入res)的也都是中间节点,然后是孩子结点。也就是说对结点访问和处理是连在一起的
但是中序遍历的顺序是左中右,要先访问根节点,然后一直向左子树访问,直到底部,开始处理结点(出栈加入res中),也就是说对结点的访问和处理是分开进行的,导致了处理和访问顺序的不一致。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res =new LinkedList<>();
if(root==null)return res;
Deque<TreeNode> stack=new LinkeList<>();
TreeNode cur=root;//用cur来遍历树
//当栈或当前遍历结点非空的时候
while(!stack.isEmpty()||cur!=null){
if(cur!=null){//当前遍历结点非空,就要入栈,然后往左子树遍历
stack.push(cur);
cur=cur.left;//左
}else{//当前遍历结点为空
cur=stack.peek();//栈顶元素就是要处理的数据,用cur保存
stack.poll();
res.add(cur.val);//中
cur=cur.right;//再往右子树遍历
}
}
return res;
}
}
·后序迭代
前序迭代顺序是中左右,后序是左右中,其实只要在前序基础上,将顺序变为中右左,再将最后的res翻转,就可以得到左右中的顺序;
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res =new LinkedList<>();
if(root==null)return res;
Deque<TreeNode> stack=new LinkeList<>();
stack.push(root);
while(!stack.isEmpty()){
root=stack.poll();
res.add(root.val);
if(root.left!=null)stack.push(root.left);
if(root.right!=null)stack.push(root.right);
}
Collections.reverse(res);
return res;
}
}
迭代方法(统一模板)
在前一组迭代写法中,中序遍历是因为需要先访问到最左下才可以开始处理结点,而前序是可以一边访问一边处理,这样导致了代码写法的不一致,为了统一这个风格,可以在遍历的过程中,都将结点入栈,只不过在要处理的节点(也就是要加入res的结点)入栈后再入栈一个null结点作为标记,这样在后续出栈中如果碰到null结点,就知道下一个出栈的结点是要加入res的,这样一来只要调整左中右结点的入栈顺序,就可以控制最后出栈的顺序;说得有些绕,直接看三种遍历的对比代码找感觉:
//前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Deque<TreeNode> stack=new LinkedList<>();
if(root!=null)stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.peek();//cur获取栈顶元素
if(cur!=null){//栈顶元素非空,出栈
stack.poll();//避免后续对栈顶的元素重复操作,统一先出栈(前面已经用cur记录了,不怕丢)
if(cur.right!=null)stack.push(cur.right);//若左右子树为空,则空节点不入栈,保证栈顶元素为空节点只发生在是中结点入栈后
if(cur.left!=null)stack.push(cur.left);
//因为是前序遍历(中左右),入栈要按右左中的顺序
stack.push(cur);
stack.push(null);//在中结点入栈后,null入栈作为标记;
}else{//若栈顶元素为空,说明下一个栈顶元素是“中”结点
stack.poll();//null出栈
cur=stack.peek();//null的下一个栈顶元素就是“中结点”,用cur暂存
stack.poll();//存下后就可出栈
res.add(cur.val);
}
}
return res;
}
}
//中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
Deque<TreeNode> stk = new LinkedList<>();
if(root!=null)stk.push(root);
while(!stk.isEmpty()){
// 不管栈顶元素是否为空,都需要出栈
TreeNode cur = stk.poll();
// 栈顶元素不为null,继续遍历树,进行对应入栈操作
if(cur!=null){
//因为是中序遍历(中左右),入栈要按右中左的顺序
if(cur.right!=null)stk.push(cur.right);
stk.push(cur);
stk.push(null);//在中结点入栈后,null入栈作为标记;
if(cur.left!=null)stk.push(cur.left);
}else{
// 栈顶元素为null,说明下一个是需要加入res的
cur = stk.poll();
res.add(cur.val);
}
}
return res;
}
}
//后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Deque<TreeNode> stack= new LinkedList<>();
if(root!=null)stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.peek();
if(cur!=null){
stack.poll();
stack.push(cur);
stack.push(null);
if(cur.right!=null)stack.push(cur.right);
if(cur.left!=null)stack.push(cur.left);
}else{
stack.poll();
cur=stack.peek();
stack.poll();
res.add(cur.val);
}
}
return res;
}
可以观察到这样设置的话,代码的区别就只在于
//1
stack.push(cur);
stack.push(null);
以及
//2
if(cur.right!=null)stack.push(cur.right);
//3
if(cur.left!=null)stack.push(cur.left);
这三句的顺序的不同。
二叉树遍历
LeetCode 144. 二叉树的前序遍历
2023.06.11 二刷
递归方法代码如下:
//递归版本
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
preOrder(res,root);
return res;
}
public void preOrder(List<Integer> res,TreeNode root){
if(root==null)return;
res.add(root.val);
preOrder(res,root.left);
preOrder(res,root.right);
}
}
迭代方法(非统一模板),代码如下:
//迭代版本
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res= new ArrayList<>();
Deque<TreeNode> stack=new LinkedList<>();
if(root!=null)stack.push(root);
//前序遍历顺序(根左右),用栈模拟要根先入栈再出栈,然后右结点入栈,左节点入栈,这样出栈顺序就符合
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
if(root.right!=null)stack.push(root.right);
if(root.left!=null)stack.push(root.left);
}
return res;
}
}
迭代法,统一模板:
思想:
迭代法需要使用栈,但用栈的话就无法解决遍历结点和处理结点的一致问题。
那么将访问到的结点放入栈中,处理过的结点也放进去,但是要对它进行标记,即处理的结点放入栈中之后,紧接着放入一个空指针入栈作为标记。后面栈顶元素碰到null,说明null的下一个栈顶元素就是访问过但还没处理的元素,后面就可以对null进行出栈,保存下一个栈顶元素,然后栈顶元素出栈,将保存的元素值加入res
代码如下:
//迭代统一版
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null)return res;
Deque<TreeNode> stack=new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.pop();//栈顶出栈,并让cur存储出栈元素
if(cur!=null){//出栈栈顶元素的非
// 判断出栈元素左右子树是否为空,不为空就入栈
if(cur.right!=null)stack.push(cur.right);
if(cur.left!=null)stack.push(cur.left);
//因为是前序遍历(中左右),入栈要按右左中的顺序
stack.push(cur);
stack.push(null);//在中结点入栈后,null入栈作为标记;
}else{//若栈顶元素为空,说明下一个栈顶元素是“中”结点
cur=stack.pop();;//null的下一个栈顶元素就是“中结点”,用cur暂存
res.add(cur.val);
}
}
return res;
}
}
LeetCode 145. 二叉树的后序遍历
2023.06.11 二刷
递归版本代码如下:
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new LinkedList<>();
postOrder(res,root);
return res;
}
public void postOrder(List<Integer> res,TreeNode root){
if(root==null)return;
postOrder(res,root.left);
postOrder(res,root.right);
res.add(root.val);
}
}
迭代版本,修改自前序遍历
前序遍历:中左右,调整代码入栈顺序->中右左,反转res->左右中(后续遍历)
代码如下:
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res =new Arraylist<>();
if(root==null)return res;
Deque<TreeNode> stack=new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
if(root.left!=null)stack.push(root.left);
if(root.right!=null)stack.push(root.right);
}
Collections.reverse(res);
return res;
}
}
迭代法,统一模板,代码如下:
//统一版本
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null)return res;
Deque<TreeNode> stack= new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.pop();
if(cur!=null){
stack.push(cur);
stack.push(null);
if(cur.right!=null)stack.push(cur.right);
if(cur.left!=null)stack.push(cur.left);
}else{
cur=stack.pop();
res.add(cur.val);
}
}
return res;
}
}
LeetCode 94. 二叉树的中序遍历
2023.06.11 二刷
递归版本,代码如下:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
inOrder(res,root);
return res;
}
public void inOrder(List<Integer> res,TreeNode root){
if(root==null)return;
inOrder(res,root.left);
res.add(root.val);
inOrder(res,root.right);
}
}
迭代法,非统一模板
中序遍历与前序和后序遍历不太一样
前序遍历的时候,遍历结点和处理结点是一起前后进行的,而中序遍历无法这样
代码如下:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res =new LinkedList<>();
if(root==null)return res;
Stack<TreeNode> stack =new Stack<>();
TreeNode cur=root;//用cur来遍历树
//当栈或当前遍历结点非空的时候
while(!stack.isEmpty()||cur!=null){
if(cur!=null){//当前遍历结点非空,就要入栈,然后往左子树遍历
stack.push(cur);
cur=cur.left;//左
}else{//当前遍历结点为空
cur=stack.peek();//栈顶元素就是要处理的数据,用cur保存
stack.pop();
res.add(cur.val);//中
cur=cur.right;//再往右子树遍历
}
}
return res;
}
}
迭代法,统一模板,代码如下:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res= new ArrayList<>();
if(root==null)return res;
Deque<TreeNode> stack=new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur=stack.pop();
if(cur!=null){
if(cur.right!=null)stack.push(cur.right);
stack.push(cur);
stack.push(null);
if(cur.left!=null)stack.push(cur.left);
}else{
cur=stack.pop();
res.add(cur.val);
}
}
return res;
}
}
n叉树的遍历
LeetCode 589. N 叉树的前序遍历
//递归法
class Solution {
List<Integer> res=new ArrayList<>();
public List<Integer> preorder(Node root) {
if(root==null)return res;
res.add(root.val);
for(Node child:root.children)preorder(child);
return res;
}
}
/*
前序遍历顺序是根-左-右,每层循环里面都是根结点先出栈,加入res,可以保证结果集中根会在孩子结点前;
还需要解决左右的顺序,栈先进后出的特性,要先让右孩子入栈,这样后面出栈可让左孩子先出栈加入res,保证结果集中的左-右顺序;结合起来就是根-左-右的顺序;
*/
//借助栈实现的迭代法
class Solution {
public List<Integer> preorder(Node root) {
List<Integer> res=new ArrayList<>();
if(root==null)return res;
Stack<Node>stack=new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
//因为栈先进后出,所以需要先让右边的孩子结点入栈
for(int i=root.children.size()-1;i>=0;--i)stack.push(root.children.get(i));
}
return res;
}
}
LeetCode 590. N 叉树的后序遍历
//递归法
class Solution {
List<Integer> res=new ArrayList<>();
public List<Integer> postorder(Node root) {
if(root==null)return res;
for(Node child:root.children)postorder(child);
res.add(root.val);
return res;
}
}
/*
与前序遍历迭代改后序遍历迭代类似,前序是根-左-右,后序是左-右-根;
只要在入栈时,让左孩子先于右孩子入栈,这样出栈就是右孩子先出,保证右-左顺序加入res;
而根总是先于孩子结点入栈,在孩子结点入栈的那层循环里,根结点值已经加入res;
这样可以保证结果集里是根-右-左,只要最后翻转res就可以变成左-右-根;
*/
//后序迭代法
class Solution {
public List<Integer> postorder(Node root) {
List<Integer> res=new ArrayList<>();
if(root==null)return res;
Stack<Node>stack=new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
root=stack.pop();
res.add(root.val);
for(int i=0;i<root.children.size();++i)
stack.push(root.children.get(i));
}
Collections.reverse(res);
return res;
}
}
二叉树的层序遍历
力扣 102.二叉树层序遍历
2023.12.13 三刷
层序遍历的主要思想需要借助队列先进先出的特性来存储遍历到的结点的左右孩子。
具体思想是:最开始将根节点加入队列,然后遍历队列,如果队列不为空,将队头结点出队,将队头结点值加入res中,并且将当前出队的结点的左右孩子加入队列;继续遍历队列,队列不为空的时候,重复出队头、加入res、左右孩子入队的操作;这样可以保证遍历二叉树的时候,永远都是在最上层的结点先被访问到(因为先出队的才能先被访问到)。
//借助队列实现层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();//res存储结果,注意题目的结果的格式(res是由许多resItem组成);
if(root==null)return res;//空树返回空结果
Queue<TreeNode> que=new LinkedList<>();//借助队列实现层序遍历,que继承LinkedList类(可以使用里面的方法)
que.offer(root);//入队操作(队尾入队),add方法出错会抛出异常,offer只是返回布尔值,所以用offer方法
//外层的while用来遍历整个队列(输出到res中)
while(!que.isEmpty()){
int len=que.size();//len用来记录当前遍历到的这一层有多少结点
List<Integer> resItem =new ArrayList<>();//每个resItem存这一层的结点值
//内层的while用来遍历每一层的结点(输出到resItem)
while(len-->0){//遍历这一层的结点,遍历完len就到0
TreeNode cur=que.poll();//poll获取队首元素并且出队
resItem.add(cur.val);//结点值加入这一层的结果中
if(cur.left!=null)que.offer(cur.left);//左右孩子入队
if(cur.right!=null)que.offer(cur.right);
}
res.add(resItem);//一层遍历结束,这一层的结点值加入最终结果中
}
return res;
}
}
力扣 107.二叉树的层序遍历II
2023.12.13 三刷
这题要求返回自底向上的层次遍历结果,只要在102题的基础上把res翻转就行了,在java里面可以用Collections.reverse(res),效果就相当于输出从底层到顶层的层序结果。
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
if(root==null)return res;
Queue<TreeNode> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
List<Integer> resItem= new ArrayList<>();
while(len-->0){
TreeNode cur=que.poll();
resItem.add(cur.val);
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
res.add(resItem);
}
Collections.reverse(res);
return res;
}
}
力扣 104. 二叉树的最大深度
2023.06.14 二刷
思路1:
这题要求输出给定二叉树的最大层数,同样可以用层序遍历的思想解决,只要在外层while循环里面加一个depth用来记录层数即可。
//1.层序遍历
class Solution {
public int maxDepth(TreeNode root) {
if(root==null)return 0;
int depth=0;//记录当前遍历的深度
Queue<TreeNode> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
depth++;//外层while遍历所有层
while(len-->0){//内层while循环用来遍历这一层的所有结点
TreeNode cur=que.poll();
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
}
return depth;
}
}
思路2. 递归
代码如下:
//递归法
class Solution {
public int maxDepth(TreeNode root) {
//若当前结点为空,则为叶子结点,高度为0;
if(root==null)return 0;
//当前结点最大深度为左右子树最大深度+1(+1是因为要算上当前结点的高度1)
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
}
力扣 111. 二叉树的最小深度
原题链接
这题要求的是最小的深度,只要在104基础上加一个判断就行。在内层while循环中,若当前结点没有左右孩子,说明它是叶子结点,这里就是最小深度。
class Solution {
public int minDepth(TreeNode root) {
if(root==null)return 0;
int depth=0;
Queue<TreeNode> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
depth++;
while(len-->0){
TreeNode cur=que.poll();
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
//如果当前结点无左右孩子,说明它是叶子结点,则此层有最小深度
if(cur.left==null&&cur.right==null)return depth;
}
}
return depth;//如果不会提前返回,说明会一直到最后一层
}
}
力扣 199. 二叉树的右视图
2023.12.14 三刷
这题要求输出每一层的最右边的结点值,只要在内层while循环里面判断是不是遍历到了这一层最后一个结点,如果是,把结点值加入res。
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res =new ArrayList<>();
if(root==null)return res;
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
while(len-->0){
TreeNode cur= que.poll();
if(len==0)res.add(cur.val);//当len=0时,说明这一层遍历到了最后一个结点
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
}
return res;
}
}
力扣 637. 二叉树的层平均值
这题要求输出每一层的结点值的平均值,直接在层序遍历过程中计算即可。
2023.06.15 二刷
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> res =new ArrayList<>();
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
double sum=0;//sum用于累加每一层的结点值的总和
for(int i=0;i<len;i++){//这里用while不太方便,因为后面还需要用到len的值,不好用while(len-->0)
TreeNode cur= que.poll();
sum+=cur.val;
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
res.add(sum/len);//该层遍历结束,求平均值
}
return res;
}
}
力扣 429. N 叉树的层序遍历
2023.06.16 二刷
要求输出n叉树的层序遍历结果,与二叉树层序思路一样。
class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> res =new ArrayList<>();
if(root==null)return res;;
Queue<Node> que=new LinkedList<>();//注意队列中的结点类型根据题意设置
que.offer(root);
while(!que.isEmpty()){
List<Integer> resItem=new ArrayList<>();
int len=que.size();
while(len-->0){
Node cur=que.poll();
resItem.add(cur.val);
//n叉树只要在内层while循环时,类似二叉树访问左右孩子,把自己孩子访问完就行
for(Node child:cur.children)que.offer(child);
}
res.add(resItem);
}
return res;
}
}
力扣 515. 在每个树行中找最大值
要求找出二叉树每一层中的最大值,只要在层序遍历过程中更新每层最大值,在一层遍历结束后将最大值加入res中即可。
class Solution {
public List<Integer> largestValues(TreeNode root) {
List<Integer> res =new ArrayList<>();
if(root==null)return res;
Queue<TreeNode> que= new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
int max=que.peek().val;//每一层遍历开始前,先把最大值设为队头结点的值
while(len-->0){
TreeNode cur=que.poll();
if(cur.val>max)max=cur.val;//如果遍历过程碰到更大值,就更新
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
res.add(max);
}
return res;
}
}
力扣 513. 找树左下角的值
2023.07.14 二刷
思路:
1.层序遍历:
每次先让右孩子先入队,这样最后一个出队的一定是最后一层的最左边的节点。
代码如下:
// 1.层序遍历
class Solution {
public int findBottomLeftValue(TreeNode root) {
TreeNode cur=new TreeNode(root.val);
Queue<TreeNode> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
cur=que.poll();
//这里先让右结点入队,这样下一层中也是右边的结点先出队
//可以保证最后一个出队的一定是最下面、最左边的结点
if(cur.right!=null)que.offer(cur.right);
if(cur.left!=null)que.offer(cur.left);
}
return cur.val;
}
}
2.深度优先遍历:(找深度最大的叶子节点)
-
确定递归参数与返回值:
– 参数:根节点,记录当前节点深度的depth;
– 返回值:用全局变量maxDepth记录最大深度,全局变量res记录深度最大的最左节点的值,无需返回值。 -
确定终止条件:
– 当前节点为null,返回上一层 -
单层递归逻辑
– 先判断当前节点是不是叶子节点,是的话就继续判断当前节点的深度是不是更大,是的话(说明目前当前节点就是深度最大的最左节点)就要更新最大深度,同时更新res;
– 如果当前节点不是叶子节点,直接递归其左右孩子,先左后右,保证优先更新左边节点的res;
代码如下:
// 2.深度优先
class Solution {
int maxDepth=-1;
int res=0;
public int findBottomLeftValue(TreeNode root) {
dfs(root,1);
return res;
}
public void dfs(TreeNode root,int depth){
if(root==null)return ;
// 当前节点是叶子节点时
if(root.left==null&&root.right==null){
// 看下当前节点深度是不是更大,更大就更新最大深度,并且更新res
if(depth>maxDepth){
maxDepth=depth;
res=root.val;
}
}
// 继续递归root的左右孩子
// (下一层深度+1,优先递归左孩子,可以保证深度一样的时候,res优先被最左边的更新)
dfs(root.left,depth+1);
dfs(root.right,depth+1);
}
}
力扣 116. 填充每个节点的下一个右侧节点指针
2023.06.17 三刷
要求将满二叉树每一层的结点用next从左到右串联;每个结点有val、next指针、left和right指针,初始next全置null;
解法1:
不考虑空间效率,可以用层序遍历的方法,空间复杂度O(n);
解法2:
也可以使用递归的方法,题目中说递归的调用栈不算,也可以粗略认为使用常数级空间;
//1.层序遍历方法(O(n)空间复杂度)
class Solution {
public Node connect(Node root) {
if(root==null)return root;
Queue<Node> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
while(len-->0){
Node cur=que.poll();
if(len>0)cur.next=que.peek();//len=0时,cur为该层最后一个结点(本身就指向null),不用管;所以只要处理len>0的情况就行
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
}
return root;
}
}
//2.1 递归方法
class Solution {
public Node connect(Node root) {
if(root==null)return root;
connectTwoNode(root.left,root.right);
return root;
}
public void connectTwoNode(Node leftNode,Node rightNode){
if(leftNode==null&&rightNode==null)return;
leftNode.next=rightNode;
connectTwoNode(leftNode.left,leftNode.right);
connectTwoNode(rightNode.left,rightNode.right);//连接右孩子的左右子结点
connectTwoNode(leftNode.right,rightNode.left);//连接左孩子的右子结点与右孩子的左子结点
}
}
// 2.2 递归方法(必须是完美二叉树才行),时间O(n),空间O(logn)
class Solution {
public Node connect(Node root) {
if(root==null)return root;
// 到最后一层停止连接(root.left=null代表到了最后一层)
if(root.left!=null){
root.left.next=root.right;
if(root.next!=null){
root.right.next=root.next.left;
}
}//每层递归只能把当前root的左右孩子相连,以及将root的右孩子与root.next的左孩子连
// 对root的左右孩子都需要重复进行相同的next连接操作
connect(root.left);
connect(root.right);
return root;
}
}
解法3(来自官方题解):
想办法在常数级空间复杂度下完成next的链接,这就需要借用上一层已经建立的next指针了,也就是第N层next链接建立后,在进行第N+1层next链接的时候,会用到第N层的next域;
使用这个方法要明白满二叉树中存在两种next链接,第一种是同一父节点下的两个子结点的next链接:
//第一种
node.left.next = node.right;
第二种是相邻的父节点间的子结点的链接,即第一个父节点的右孩子与第二个父节点的左孩子的链接:
//第二种
node.right.next = node.next.left
算法思路:
-
从根节点开始,由于第 0 层只有一个节点,所以不需要连接,直接为第 1 层节点建立 next 指针即可。该算法中需要注意的一点是:当我们为第 N 层节点建立 next 指针时,处于第 N−1 层。比如为第2层建立next指针的时候,遍历到的结点是位于第1层的。当第 N 层节点的 next 指针全部建立完成后,从N-1层移至第 N 层,建立第 N+1 层节点的 next 指针。
-
遍历某一层的节点时,这层节点的 next 指针已经建立。因此我们只需要知道这一层的最左节点,就可以按照链表方式遍历,不需要使用队列。
-
完成当前层的连接后,进入下一层重复操作,直到所有的节点全部连接。进入下一层后需要更新最左节点,然后从新的最左节点开始遍历该层所有节点。因为是完美二叉树,因此最左节点一定是当前层最左节点的左孩子。如果当前最左节点的左孩子不存在,说明已经到达该树的最后一层,完成了所有节点的连接。
所以完整代码如下:
//3. 使用已建立的next指针,时间O((n),空间O(1)
class Solution {
public Node connect(Node root) {
if(root==null)return root;
// 每层都从最左结点开始遍历该层,mostLeft标记每一层的最左结点
Node mostLeft=root;
// 当mostLeft的左孩子为空,说明已经到了最后一层
// 而最后一层已经在上一层中连接好next了,不用再连了
while(mostLeft.left!=null){
// 用cur遍历mostLeft所在的层(用这一层的next相连,相当于遍历链表)
Node cur=mostLeft;
while(cur!=null){
// ①cur的左右孩子相连
cur.left.next=cur.right;
// 需要确保cur后面还有结点,如果没有就不用
if(cur.next!=null){
// ②相邻的父节点间的子结点的链接
cur.right.next=cur.next.left;
}
cur=cur.next;//更新cur,用next走到该层的相邻结点
}//走出内层while循环,代表cur的下一层已经连好了
// 需要前往下一层进行连接,更新mostLeft
mostLeft=mostLeft.left;
}
return root;
}
}
力扣 117. 填充每个节点的下一个右侧节点指针 II
2023.06.17 二刷
相比于上一题,这题的二叉树形没有限制(可以不是满二叉树),在不考虑空间效率前提下,可以用上题的层序遍历的方法,代码完全一样。
简单省事型模板解法:
//层序遍历方法,空间复杂度O(n)
class Solution {
public Node connect(Node root) {
if(root==null)return root;
Queue<Node> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
while(len-->0){
Node cur=que.poll();
if(len>0)cur.next=que.peek();//len=0时,cur为该层最后一个结点(本身就指向null),不用管;所以只要处理len>0的情况就行
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
}
return root;
}
}
·常数级空间复杂度解法 : 空间复杂度O(1),来自【数据结构和算法–sdwwld】,即第一个题解:
// 2.常数级空间
class Solution {
public Node connect(Node root) {
if(root==null)return root;
//cur为当前遍历到的那一层的头结点(将该层视为链表,cur遍历链表)
Node cur=root;
while(cur!=null){
// 用于指向下一层的第一个结点,方便一层遍历完之后更新cur到下一层
Node dummy=new Node();
Node pre=dummy;//pre用于遍历串联下一层的结点,初始在dummy处
// 遍历当前层的链表
while(cur!=null){
// 首先看cur有没有左孩子,有的话就开始连cur的下一层的next指针
if(cur.left!=null){
pre.next=cur.left;
pre=pre.next;
}
// 再看cur有没有右孩子,有的话就连下一层的next指针
if(cur.right!=null){
pre.next=cur.right;
pre=pre.next;
}
//继续访问这一行的下一个节点
cur=cur.next;
}//从内层while出来,说明cur走到这一层的尾部
// 需要更新cur到下一层的第一个结点处,直到cur为空为止
cur=dummy.next;
}
return root;
}
}
二叉树遍历的其他题目
力扣 404. 左叶子之和
2023.07.14 二刷
思路:
求左叶子之和,平常基本都是通过当前节点左右孩子为空来判断当前节点是叶子结点,但是要判断为左叶子,需要当前节点的父节点也参与,设节点A,其左孩子不为空,并且左孩子的左右孩子都为空,那么节点A的左孩子就是左叶子节点,这时候就可以处理左叶子节点了。
一、使用递归方法:
1.递归函数的参数与返回值:
- 参数:根节点
- 返回值:以当前节点为根,下方的左叶子节点之和
2.终止条件:
- 当前节点为null时,返回0;
3.单层递归逻辑:
- 处理左子树:先判断当前节点左孩子是不是左叶子节点(左孩子不空,左孩子左右孩子为空),是的话记录值;不是就再对左孩子也进行递归判断
- 处理右子树:对右孩子进行递归,记录右子树的左叶子节点之和
- 返回左右子树的左叶子节点之和的和,作为当前节点的左叶子节点之和。
二、层序遍历
- 用层序遍历模板修改
- res记录左叶子节点之和,层序遍历中对cur的左孩子进行左叶子节点判断处理(是的话左叶子值加入res)即可。
两种方法的时空复杂度都是O(n),n为节点个数。
代码如下:
// 一、递归
public class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if(root == null) return 0;
int leftSum=0;
// 1.处理左子树
// 如果是左叶子节点(左孩子不空,左孩子左右孩子为空),记录左叶子节点值
if(root.left != null && root.left.left == null && root.left.right == null){
leftSum = root.left.val;
}else{
// 否则就要继续往左子树找
leftSum = sumOfLeftLeaves(root.left);
}
// 2.处理右子树
int rightSum=sumOfLeftLeaves(root.right);
// 3.返回以当前节点为根的树的左叶子节点之和
return leftSum + rightSum;
}
}
//二、迭代方法--层序遍历模板修改
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
int res=0;
Queue<TreeNode>que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
TreeNode cur=que.poll();
// 左孩子不空
if(cur.left!=null){
// 并且为叶子节点
if(cur.left.left==null&&cur.left.right==null)
res+=cur.left.val;
else
que.offer(cur.left);//不是叶子节点就还要往下一层遍历
}
if(cur.right!=null)
que.offer(cur.right);
}
return res;
}
}
力扣 513. 找树左下角的值
2023.07.14 二刷
思路:
1.层序遍历:
每次先让右孩子先入队,这样最后一个出队的一定是最后一层的最左边的节点。
代码如下:
// 1.层序遍历
class Solution {
public int findBottomLeftValue(TreeNode root) {
TreeNode cur=new TreeNode(root.val);
Queue<TreeNode> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
cur=que.poll();
//这里先让右结点入队,这样下一层中也是右边的结点先出队
//可以保证最后一个出队的一定是最下面、最左边的结点
if(cur.right!=null)que.offer(cur.right);
if(cur.left!=null)que.offer(cur.left);
}
return cur.val;
}
}
2.深度优先遍历:(找深度最大的叶子节点)
-
确定递归参数与返回值:
– 参数:根节点,记录当前节点深度的depth;
– 返回值:用全局变量maxDepth记录最大深度,全局变量res记录深度最大的最左节点的值,无需返回值。 -
确定终止条件:
– 当前节点为null,返回上一层 -
单层递归逻辑
– 先判断当前节点是不是叶子节点,是的话就继续判断当前节点的深度是不是更大,是的话(说明目前当前节点就是深度最大的最左节点)就要更新最大深度,同时更新res;
– 如果当前节点不是叶子节点,直接递归其左右孩子,先左后右,保证优先更新左边节点的res;
代码如下:
// 2.深度优先
class Solution {
int maxDepth=-1;
int res=0;
public int findBottomLeftValue(TreeNode root) {
dfs(root,1);
return res;
}
public void dfs(TreeNode root,int depth){
if(root==null)return ;
// 当前节点是叶子节点时
if(root.left==null&&root.right==null){
// 看下当前节点深度是不是更大,更大就更新最大深度,并且更新res
if(depth>maxDepth){
maxDepth=depth;
res=root.val;
}
}
// 继续递归root的左右孩子
// (下一层深度+1,优先递归左孩子,可以保证深度一样的时候,res优先被最左边的更新)
dfs(root.left,depth+1);
dfs(root.right,depth+1);
}
}
二叉树属性
对称二叉树
主要介绍力扣 101. 对称二叉树一题的递归和迭代解法。并且通过这题的思想还可以练习力扣 100. 相同的树与力扣 572. 另一棵树的子树
力扣 101. 对称二叉树
2023.12.13 三刷
递归解法
1.递归参数和返回值
因为需要比较是否对称,因此每次递归传入的参数应当是一对对称位置上的结点,返回值应当是bool类型
boolean compare(TreeNode leftNode,TreeNode rightNode)
2.终止条件和递归过程
比较两个结点:
①首先应该先判断是否为空,都为空则对称,返回true;
②能经过①走到②这一步,则两个结点不是全为空,有三种情况,只有左为空,只有右为空,或均不为空;前两种情况说明不对称,返回false,均不为空情况下,如果结点值不同,也不对称,返回false;
//首先判断空节点的三种情况
if(leftNode==null&&rightNode==null)return true;
else if(leftNode!=null&&rightNode==null)return false;
else if(leftNode==null&&rightNode!=null)return false;
//判断空节点之后判断值是否相同
else if(leftNode.val!=rightNode.val)return false;
③能走到这一步,说明两个结点对称了,需要进一步判断下一层的结点的对称情况,也就是下一层的外侧和内侧的两对结点(需要同时满足才是对称,这也决定了迭代法里面每次都是比较一对结点,循环末尾需要内外侧两对结点成对入队)
//排除空节点和值之后,肯定leftNode.val=rightNode.val,递归判断下一层的情况
else return compare(leftNode.left,rightNode.right)&&compare(leftNode.right,rightNode.left);//分别判断下一层外侧和内侧结点是否对称
整体代码如下:
//递归方法,时间O(n),空间O(n)
class Solution {
public boolean isSymmetric(TreeNode root) {
return compare(root.left,root.right);//这题结点树大于等于1,不用判断空节点
}
boolean compare(TreeNode leftNode,TreeNode rightNode){
//首先判断空节点的三种情况
if(leftNode==null&&rightNode==null)return true;
else if(leftNode!=null&&rightNode==null)return false;
else if(leftNode==null&&rightNode!=null)return false;
//判断空节点之后判断值是否相同
else if(leftNode.val!=rightNode.val)return false;
//排除空节点和值之后,肯定leftNode.val=rightNode.val,递归判断下一层的情况
else return compare(leftNode.left,rightNode.right)&&compare(leftNode.right,rightNode.left);//分别判断下一层外侧和内侧结点是否对称
}
}
迭代法
每次都是从队中取出一对结点进行比较,一层层比较下去,具体过程见代码注释。
//迭代方法,时间O(n),空间O(n)
class Solution {
public boolean isSymmetric(TreeNode root) {
Queue<TreeNode>que=new LinkedList<>();//用队列暂存一对对结点
que.offer(root.left);//入队出队一定都是成双成对的
que.offer(root.right);
TreeNode leftNode,rightNode;
while(!que.isEmpty()){
leftNode=que.poll();
rightNode=que.poll();
//首先判断结点是否为空,当两个结点都为空,无孩子,对称,直接进入下一次循环
if(leftNode==null&&rightNode==null)continue;
//能走到这里,说明两个结点不是全空,可能是其中一个为空,也可能是都不空(那就要判断值是不是相等)
if(leftNode==null||rightNode==null||leftNode.val!=rightNode.val)return false;
//能走到这里,说明这两个点对称了,要判断下一对结点(入队)
que.offer(leftNode.left);
que.offer(rightNode.right);
que.offer(leftNode.right);
que.offer(rightNode.left);
}
return true;//如果循环结束都没返回false,则说明全过程都是对称
}
}
理解了这题的做法,可以对另外三题进行练习:
力扣 100. 相同的树
2023.07.06 三刷
思路:
1.递归法:同101. 对称二叉树的解法,不过注意不是判断对称,而是判断两棵树是否相同
2.迭代法:每次都成对入队,出队也需要成对出队,这样每次比较的都是两棵树对应位置的结点
//递归(深度优先遍历)
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p==null&&q==null)return true;
else if(p==null||q==null||p.val!=q.val)return false;
//分别递归判断左子树和右子树
else return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);//注意这里是比较对应位置,而不是像上题一样的对称位置
}
}
//迭代法(广度优先遍历),用一个队列即可,每次出入队都成对
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
Queue<TreeNode>que=new LinkedList<>();
que.offer(p);que.offer(q);
while(!que.isEmpty()){
p=que.poll();q=que.poll();
if(p==null&&q==null)continue;
if(p==null||q==null||p.val!=q.val)return false;
que.offer(p.left);
que.offer(q.left);
que.offer(p.right);
que.offer(q.right);
}
return true;
}
}
力扣 572. 另一棵树的子树
2023.07.06 二刷
官网评论区说:
2022年1月字节面试题,要求写出第三种方案。并且还要完备,也即考虑hash冲突的问题。
也出现在华为面试题中。
1.递归解法:需要理解100题的递归方法,这题递归的思路就会很清晰。
使用100. 相同的树 中的isSameTree方法对以两个结点为根的树进行判断。
root的树从根节点开始进行递归遍历,设sub是子树,root是母树,然后: sub要么等于root,sub的左子树等于root;sub的右子树等于root。这三种情况只要有一种为true,则存在root的子树。
2.设计哈希函数+深度优先+解决冲突:
思路参考官方题解
代码如下:
//递归解法
class Solution {
//100题的递归函数,直接使用,p对应母树root的点,q对应子树subRoot的点
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p==null&&q==null)return true;
else if(p==null||q==null||p.val!=q.val)return false;
//分别递归判断母树和子树的左子树和右子树
else return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
}
//主函数
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if(root==null)return false;//母树遍历完都没有返回true,说明都没匹配上
//每层递归里面调用判断两树相等函数,判断是否相等,再调用主函数递归判断母树中当前结点的左右子树;
return isSameTree(root,subRoot)||isSubtree(root.left,subRoot)||isSubtree(root.right,subRoot);
}
}
//2.哈希+深度优先遍历解法
class Solution {
//随便两个素数P、Q,用在哈希函数里面计算结点哈希值
int P=7,Q=11,mod=100007;//mod用于防止哈希计算过程数值溢出
boolean ans=false;//ans标记子树是否在母树中出现过
int T=-99;//用于记录子树的哈希值,一开始设置为负数是为了避免碰巧等于子树哈希值,这样就可以让主函数里的T=dfs(subRoot)语句,给T赋正确的哈希值
//深度优先遍历,并且计算每个结点对应子树的哈希值
public int dfs(TreeNode root){
//根节点如果是空的,随便返回一个值(注意不能返回0,因为结点值为0也可能返回0,这样就没办法区分结点值为0和结点为空的情况,结果会出错)
if(root==null)return 1234;
//root.val%mod+mod先把当前遍历到的点值映射成非负,最后%mod防止溢出
int x=(root.val%mod+mod)%mod;
int L=dfs(root.left);//求左子树的哈希值
int R=dfs(root.right);//右子树的哈希值
//若在深度优先遍历母树过程中,发现母树某结点对应子树的哈希值与待匹配子树相同,说明子树在母树中出现了,ans标记置true
if(T==L||T==R)ans=true;
//返回当前root结点(这一层递归传入的结点)对应子树的哈希值
return (x+L*P+R*Q)%mod;
}
//主函数
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
T=dfs(subRoot);//求子树的哈希值
//判断子树哈希值和母树根节点哈希值相同,若相同则标记,不相同则在深度优先遍历母树过程中继续判断
if(T==dfs(root))ans=true;
return ans;//最终返回标记
}
}
剑指 Offer 26. 树的子结构
这题要注意与力扣 572. 另一棵树的子树区分,这题的子树只要结构可以对应就可以,没有要求从根节点到叶子结点完全对应。
2023.07.07 二刷
思路:
和572有一些类似,但是细节不同,因为572要求的子树是从母树那个结点往下的整棵子树和要比较的子树完全相同,而本题只要求母树的某个节点开始往下的一部分结构和要比较的子树一样就行,因此在深度优先遍历的时候,终止条件和572不同。
1.当B走到空,说明前面已经全部对比过了,没有在前面返回,能够把B走完,B就是A的子结构,返回true;
2.能够跳过第1个判断,说明B不是空,那么就要看A是不是空,如果A是空,说明B还没对比完,A就不够比了,这一部分肯定不是子结构,返回false;
3.再跳过第2个判断,说明A和B都不是空,如果A和B的值不相同,则也不是A的子结构,返false;
4.能走到4,最说明A和B值相同,那么还要继续分别对比A和B的左右孩子是否相同。
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
//约定空树不是任意一个树的子结构
if(A == null || B == null) return false;
// dfs(A, B) 当前节点B是否是A的子树,若不是,则同理判断当前节点的孩子节点
return dfs(A, B)||isSubStructure(A.left,B)||isSubStructure(A.right, B);
}
public boolean dfs(TreeNode A, TreeNode B){
//这里如果B为空,说明B已经访问完了,确定是A的子结构
if(B == null) return true;
//如果B不为空A为空,或者这两个节点值不同,说明B树不是A的子结构,直接返回false
if(A == null|| A.val != B.val ) return false;
// 若两个节点的值不同 则B不可能是A的子树 相同则比较两个节点的孩子节点是否相同
return dfs(A.left, B.left)&& dfs(A.right, B.right);
}
}
完全二叉树
力扣 222. 完全二叉树的节点个数
2023.07.11 二刷
思路:
1.递归;
2.层序遍历过程中统计;
3.利用完全二叉树性质;
思路1,2代码如下:
// 1.递归,时间O(n),空间O(logn)--递归栈空间
class Solution {
public int countNodes(TreeNode root) {
if(root==null)return 0;
return 1+countNodes(root.left)+countNodes(root.right);
}
}
// 2.层序,时间O(n),空间O(n)--队列长度
class Solution {
public int countNodes(TreeNode root) {
int count=0;
if(root==null)return count;
Queue<TreeNode> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
while(len-->0){
TreeNode cur=que.poll();
count++;
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
}
return count;
}
}
前两种解法比较常见,第三种解法利用到了完全二叉树的性质,解法思路来自精选题解,配图来自代码随想录公众号。
首先需要了解完全二叉树是什么样的,完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则最底层包含 1~ 2h-1 个节点。
想要计算完全二叉树的节点数,肯定是更希望它是一棵满二叉树,这样只需要知道树的高度就可以求出全部的节点数量。回顾一下满二叉的节点个数怎么计算:如果满二叉树的层数为h,则总节点数为:2h-1。
那么对于一棵二叉树而言,要想从中拆出满二叉树,可以通过其根节点的左右子树的高度来找满二叉树:
对root节点的左右子树进行高度统计,分别记为leftH和rightH,有以下两种结果:
-
leftH== rightH。这说明,左子树一定是满二叉树,因为节点已经填充到右子树了,左子树必定已经填满了(如下图)。所以左子树的节点总数可以直接得到,是2leftH - 1,加上当前这个root节点,则正好是2leftH。那么以root为根节点的完全二叉树节点总数就是2leftH+右子树节点总数,这样问题就退化成求右子树这棵完全二叉树的节点总数。
-
leftH!= rightH。说明此时最后一层节点没有填充到右子树部分,但倒数第二层一定是已经满了,右子树一定是满二叉树(如下图)。可以通过右子树高度直接得到右子树的节点个数。同理,右子树节点+root节点,总数为2rightH。以root为根节点的完全二叉树节点总数就是2rightH+左子树节点总数,这样问题就退化成求左子树这棵完全二叉树的节点总数。
代码如下:
// 3.利用完全二叉树性质,时间O(n),空间O(logn)
class Solution {
public int countNodes(TreeNode root) {
if(root==null)return 0;
int leftH=countHeight(root.left);
int rightH=countHeight(root.right);
// 左右子树高度相等,说明左子树是满二叉
if(leftH==rightH){
// 2^lefftH+右子树节点数
return (1<<leftH)+countNodes(root.right);
}else{
// 左右子树高度不等,说明右子树比左子树少1层,右子树一定是满二叉
return (1<<rightH)+countNodes(root.left);
}
}
// 计算完全二叉树高度
public int countHeight(TreeNode root){
int h=0;
while(root!=null){
h++;
root=root.left;
}
return h;
}
}
平衡二叉树
力扣 110. 平衡二叉树
本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
需要注意区分二叉树节点深度和高度:
深度:根节点出发,到该节点的最长简单路径边的条数;
高度:从该节点到叶子节点的最长简单路径边的条数;
求深度是从上往下查,用前序遍历;求高度从下往上,用后序遍历;此题需要比较的是每个节点的
2023.07.12 二刷
思路
需要注意区分二叉树节点深度和高度:
- 深度:根节点出发,到该节点的最长简单路径边的条数;
- 高度:从该节点到叶子节点的最长简单路径边的条数;
求深度是从上往下查,用前序遍历;求高度从下往上,用后序遍历;此题需要比较的是每个节点的高度,从下往上,采用后序遍历。
为什么这题求高度采用从下往上后序遍历的方式呢?
-
其实采用前序遍历也可以,但是需要构造一个获取当前节点最大深度的方法depth(root)–见【力扣 104. 二叉树的最大深度】。每遍历到一个节点,就获取当前节点左右子树最大深度,如果差值大于1就不是,小于等于1就继续用前序遍历子树。这样的过程中,会产生大量重复的遍历(反复调用depth方法产生的)。
-
采用后序遍历可以避免这样重复的计算,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回−1。如果存在一棵子树不平衡,则整个二叉树一定不平衡。
二叉树深度问题
力扣 104. 二叉树的最大深度
2023.06.14 二刷
求一棵二叉树的最大的深度,有递归和迭代法。
//递归法
class Solution {
public int maxDepth(TreeNode root) {
//若当前结点为空,则为叶子结点,高度为0;
if(root==null)return 0;
//当前结点最大深度为左右子树最大深度+1(+1是因为要算上当前结点的高度1)
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
}
/*
层序遍历比较适合求深度,因为层序遍历是一层层遍历下去,只要在每层里面给depth+1就行,层序遍历模板用得较为熟练,用起来比较方便。
*/
//迭代法(层序遍历模板)
class Solution {
public int maxDepth(TreeNode root) {
if(root==null)return 0;
Queue<TreeNode> que=new LinkedList<>();
int depth=0;//记录当前遍历的深度
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
depth++;//外层while遍历所有层
while(len-->0){//内层while循环用来遍历这一层的所有结点
TreeNode cur=que.poll();
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
}
return depth;
}
}
力扣 559. N 叉树的最大深度
原题链接
像上题一样,可以用递归和迭代方法。
2023.07.08 二刷
思路:同104. 二叉树的最大深度
1.递归:
对每一层的结点,都要设置一个depth,用来记录其所有子树的最大深度,对该层结点的所有子树进行遍历,更新最大的depth,那么这一层的这个结点,返回的1+depth就是该结点最大深度
2.层序遍历:
利用层序遍历模板即可
递归法中需要注意,不同于二叉树的求最大深度,n叉树有多个孩子结点,需要在访问当前结点的时候,遍历孩子结点,并求出下一层子结点中的最大深度,这需要使用depth记录下一层的子结点的最大深度;在最后返回值+1,也就是还需要算上当前遍历到的结点的1的高度。
//递归法
class Solution {
public int maxDepth(Node root) {
if(root==null)return 0;
int depth=0;
for(Node child:root.children){
depth=Math.max(maxDepth(child),depth);
}
return depth+1;
}
}
//迭代法(层次遍历模板)
class Solution {
public int maxDepth(Node root) {
if(root==null)return 0;
Queue<Node>que=new LinkedList<>();
que.offer(root);
int depth=0;
while(!que.isEmpty()){
int len=que.size();
depth++;
while(len-->0){
Node cur=que.poll();
for(Node child:cur.children)//多了遍历所有孩子的步骤
if(child!=null)que.offer(child);
}
}
return depth;
}
}
力扣 111. 二叉树的最小深度
2023.07.08 二刷
求最小深度需要注意,最小深度是从根节点到最近叶子节点的最短路径上的节点数量,如图:(为方便说明,采用代码随想录公众号的图片)
·因此,在递归的方法里,需要考虑左右子树分别为空的情况,排除这两种情况之后,还剩下左右子树都为空,和左右子树都存在的情况,则返回1+左右子树最小深度(若均非空,则可递归计算,若均为空,则相当于1+0)。这样的式子相当于省略了:
if(root.left==null&&root.right==null)return 1;
这一句的判断。
·在迭代方法里,由于是层序遍历从上往下的遍历,因此碰到的第一个叶子结点(左右子树都为空的结点)所在的那层深度就是最小深度。
思路:
-
1.递归:
分几种情况:
①当前结点为空,以当前结点为根的树最小深度为0;
②走到这说明当前结点root不为null,如果当前结点左右子树均为空,当前结点最小深度为1,返回;
③如果当前结点的左子树为空,右子树不为空,则最小深度为1(当前结点)+右子树最小深度;
④如果当前结点的左子树不为空,右子树为空,则最小深度为1(当前结点)+左子树最小深度;
⑤走到这说明左右孩子均不为空,则最小深度为1+左右子树最小深度; -
2.层序遍历:
层序遍历从上往下的遍历,碰到的第一个叶子结点(左右子树都为空的结点)所在的那层深度就是最小深度。
递归和迭代的代码如下:
//递归法
class Solution {
public int minDepth(TreeNode root) {
if(root==null)return 0;
//左子树空,右子树不空,最小深度为根节点到右子树的叶子结点的深度
if(root.left==null&&root.right!=null)return 1+minDepth(root.right);
//右子树空,左子树不空,最小深度为根节点到左子树的叶子结点的深度
if(root.left!=null&&root.right==null)return 1+minDepth(root.left);
//排除了左右子树分别为空的情况,还剩下左右子树都为空,和左右子树都存在的情况,这两种情况都可以用这个式子计算:当前遍历到的结点的最小深度为1+左/右子树最小的深度
return 1+Math.min(minDepth(root.left),minDepth(root.right));
}
}
//迭代法(层序遍历模板)
class Solution {
public int minDepth(TreeNode root) {
if(root==null)return 0;
int depth=0;
Queue<TreeNode> que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
depth++;
while(len-->0){
TreeNode cur=que.poll();
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
//如果当前结点无左右孩子,说明它是叶子结点,则此层有最小深度
if(cur.left==null&&cur.right==null)return depth;
}
}
return depth;//如果不会提前返回,说明会一直到最后一层
}
}
二叉树路径问题
LeetCode 257. 二叉树的所有路径(返回所有从根节点开始的路径)
2023.07.12 二刷
思路:
- 此题需要返回所有路径,而且是从根节点开始的,所以使用递归前序遍历方式;
- 因为路径在遍历过程中会动态变化,所以维护一个全局变量path,存储遍历过程中的节点值,并根据遍历情况对path内容进行增删。
- 存储结果的res也设置为全局变量,在递归的时候就不用传它的参;
遍历函数中:
1.当前节点不为空,则将节点值加入path;
2.判断当前节点是否为叶子节点,若是,则遍历path,将path值拼接,拼接后存入res;
3.接着遍历当前节点左右子树;
4.遍历完毕后,需要在最后将当前节点值从path中删除;
代码如下:
class Solution {
// 将结果集合res与维护路径的path设置为全局变量,就不用在递归中传递这两个参数
List<String> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<String> binaryTreePaths(TreeNode root) {
traversal(root);
return res;
}
// 前序遍历
public void traversal(TreeNode root){
// 当前结点为空,返回上一层
if(root==null)return ;
// 前序遍历--处理根节点
path.add(root.val);
// 当前节点为叶子结点时,就要处理路径,加入结果集
if(root.left==null&&root.right==null){
// path为Integer类型要转
StringBuilder sb = new StringBuilder();
// 遍历path,
for(int i=0;i<path.size();i++){
if(i!=0)sb.append("->");
sb.append(path.get(i));
}//path值都加入了sb
res.add(sb.toString());
}
// 前序遍历--遍历左右子树
traversal(root.left);
traversal(root.right);
// 遍历完毕,回到本层,需要回溯到上一层了,将本层添加的节点从path中删掉
path.remove(path.size()-1);
}
}
LeetCode 112. 路径总和(判断该树中是否存在 根节点到叶子节点 的路径)
2023.07.19 二刷
思路:
1.深度优先遍历:
- 深度优先遍历二叉树,向下一层递归时,targetSum-=当前节点的值root.val,当到达叶子节点的时候,判断targetSum是否等于当前的节点值,如果等于,说明找到了,否则尝试另外一条路径
- 维护一个全局变量res,初始为false,若遍历过程中判断存在该路径,修改res为true,最终返回res。
代码如下:
// 1.深度优先遍历,时间O(N),空间O(h),h最差为N,一般为logn
class Solution {
boolean res=false;
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null)return false;
dfs(root,targetSum);
return res;
}
public void dfs(TreeNode root,int targetSum){
if(root==null)return;
// 当前root为叶子节点,且targetSum减到root.val大小
if(root.left==null&&root.right==null&&targetSum==root.val){
res=true;
}
dfs(root.left,targetSum-root.val);
dfs(root.right,targetSum-root.val);
}
}
2.使用层序遍历的方式:
- 使用两个队列,分别存储将要遍历的节点,以及根节点到这些节点的路径和,记录从根节点到当前节点的路径和。
- 当遍历到叶节点,且到该节点的累积路径和为targetSum时,返回true。
代码如下:
// 2.层序遍历(双队列,不改变树的节点值),时间O(N),空间O(N)
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null)return false;
Queue<TreeNode> que1 = new LinkedList<>();//存节点
Queue<Integer> que2 = new LinkedList<>();//存根节点到该节点的路径累加和
que1.offer(root);
que2.offer(root.val);
while(!que1.isEmpty()){
int len=que1.size();
while(len-->0){
TreeNode cur=que1.poll();
int sum=que2.poll();//sum为cur节点对应的累加和
// 当cur为叶子结点,且对应累加和为targetSum,则存在路径
if(cur.left==null&&cur.right==null&&sum==targetSum){
return true;
}
// 向左子树继续累加
if(cur.left!=null){
que1.offer(cur.left);
que2.offer(sum+cur.left.val);//que2存储左孩子对应路径和
}
// 向右子树继续累加
if(cur.right!=null){
que1.offer(cur.right);
que2.offer(sum+cur.right.val);//que2存储右孩子对应路径和
}
}
}
return false;
}
}
也可以使用单队列,但是需要对树的节点值进行修改,代码如下:
// 层序遍历(单队列,会改变树的节点值),时间O(N),空间O(N)
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null)return false;
Queue<TreeNode>que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len = que.size();
while(len-->0){
TreeNode cur = que.poll();
//如果该节点是叶子节点
//同时到该节点的路径累加值等于sum,那么存在这条路径
if(cur.left==null&&cur.right==null&&cur.val==targetSum){
return true;
}
if(cur.left!=null){
que.offer(cur.left);
cur.left.val+=cur.val;
}
if(cur.right!=null){
que.offer(cur.right);
cur.right.val+=cur.val;
}
}
}
return false;
}
}
LeetCode 113. 路径总和 II(找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径)
2023.07.19 二刷
思路:参考112. 路径总和的深度优先遍历方法
- 维护两个全局变量,分别是存储结果的res,以及记录遍历节点路径的path;
- 核心就是在深度优先遍历过程中,根据遍历到的节点,改变path内容,并且每向下一层递归,传递的targetSum就要减去当前节点的val;
- 当前节点为叶子节点,且val和减完的targetSum相等,说明是一条符合条件的路径,添加进res;
- 注意这一层递归的最后,需要还原path,即在path中去掉当前的节点值。
代码如下:
class Solution {
List<List<Integer>> res=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
if(root==null)return res;
dfs(root,targetSum);
return res;
}
public void dfs(TreeNode root,int targetSum){
if(root==null)return;
path.add(root.val);
if(root.left==null&&root.right==null&&root.val==targetSum){
res.add(new ArrayList<>(path));
}
dfs(root.left,targetSum-root.val);
dfs(root.right,targetSum-root.val);
path.remove(path.size()-1);
}
}
LeetCode 437. 路径总和 III(求该二叉树里节点值之和等于 targetSum 的 路径 的数目)
这题和前面两题不太一样,要求的是从任意节点往下走,可以形成targerSum的路径的数目,按113. 路径总和 II的解法,肯定就是从根节点开始dfs,访问每一个节点 node,检测以 node为起始节点且向下延深的路径有多少种。递归遍历每一个节点的所有可能的路径,然后将这些路径数目加起来即为返回结果。
以下内容来自【彤哥来刷题啦】:
但是这样在遍历所有节点过程中,会有很多重复计算(节点间的val相加),可以考虑引入前缀和,比如,给定数据为:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8,对应的二叉树及其前缀和为:
有了前缀和,我们只要在每一条路径上求解:[两个节点] 各自的 [前缀和之差 ]等于 targetSum,如果符合这种情况,说明存在从【被减去节点】到【当前节点】累积和为targetSum的路径。
为了更快速的找到某个数值在路径中是否出现过,我们可以使用哈希表记录路径中每个数值出现的次数,比如,上图中哈希表中记录了 10 这个数值,当遍历到 18 时,发现哈希表中有 18 - 8 = 10,总次数加上 10 出现的次数即可。
另外,为了处理包含根节点的情况,我们需要在哈希表中存储一个 (0,1) 的键值对;
代码如下:
class Solution {
Map<Long,Integer> preMap = new HashMap<>();
int res=0;
public int pathSum(TreeNode root, int targetSum) {
// 防止包含根节点的时候找不到
preMap.put(0L,1);
Long preSum=0L;
dfs(root,preSum,targetSum);
return res;
}
public void dfs(TreeNode root,Long preSum,int targetSum){
if(root==null)return ;
// 记录到当前节点前缀和
preSum+=root.val;
res += preMap.getOrDefault(preSum-targetSum,0);
// 将该前缀和对应的出现次数记录在hashmap中
preMap.put(preSum,preMap.getOrDefault(preSum,0)+1);
// 继续向左右子树计算
dfs(root.left,preSum,targetSum);
dfs(root.right,preSum,targetSum);
// 递归结束,需要向上回溯,要扣除掉这个节点在hashmap中计入的value值
preMap.put(preSum,preMap.getOrDefault(preSum,0)-1);
}
}
LeetCode 543. 二叉树的直径(树中任意两个节点之间最长路径的长度,可以不经过根节点)
2023.12.13 二刷
思路:
直径是指树中任意两个节点之间最长路径的长度,路径可能经过也可能不经过根节点root,两节点之间路径的长度由它们之间【边数】表示。
- 经过找规律,发现这题里面的直径其实是一个结点从左边出发到叶结点的最长边数+从右边出发到叶结点的最长边数。
- 其实就是一个结点的左子树最大深度+右子树的最大深度。
- 这里需要注意:最大深度指的是–根节点到最远叶子节点的最长路径上的【节点数】,而这题要求的是这个结点左右两边到叶子的路径中【边数】之和的最大值。
- 正常来说,从一个结点往叶子结点走,节点数会比边数多1,不过一个结点的【左子树最大深度】,和这个结点到叶子结点的最长的路径【边数】是相同的,因为左子树到这个结点还有一个边(示例1中,1的左子树最大深度为2,1从左边出发到叶子结点最大边数为2,二者一样大)。
算法思想:
遍历每一个节点,以每一个节点为中心点分别计算左右子树最大深度l与r,这个结点的直径就是l+r
更新全局变量res=max(res,l+r)。
计算最大深度参考【104. 二叉树的最大深度】
代码如下:
class Solution {
int res=0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return res;
}
public int depth(TreeNode root){
if(root==null)return 0;
int l=depth(root.left);
int r=depth(root.right);
res=Math.max(res,l+r);
return Math.max(l,r)+1;
}
}
LeetCode 124. 二叉树中的最大路径和(2023/3/29 快手实习推荐算法一面)
2023.12.16 一刷
核心是注意当前节点的最大路径,与当前节点作为子节点时的贡献是两个不同的值
- 当前节点的最大路径(自己作为顶点): max(自己,自己+左边,自己+右边,自己 + 左边 + 右边)
- 当前节点作为子节点时的贡献(作为非顶点的路径一部分):max(自己,自己+左边,自己+右边)
后者相对前者,少了左右都存在的情况。因为作为子节点时,一条路如果同时包含左右,根就被包含了2次,不符合题目只出现一次的限制了。
以下内容来自【宫水三叶】:
设计 DFS 函数,传入当前节点 node,返回以该节点“往下”延伸所能取得的最大路径和。
即在 【仅使用当前节点】、【使用当前节点和左子树路径】和 【使用当前节点和右子树路径】三者中取最大值进行返回。
同时在 DFS 过程中,我们计算「以当前节点 node为路径最高点」时的最大路径和,并用此来更新全局变量 res。
具体的,在以节点 node为路径最高点时的最大路径和,首先包含当前节点本身(node.val),以及可选的左右节点路径和(dfs(node.left) 和 dfs(node.right))。当左右节点路径和不为负数时,说明能够对当前路径起到正向贡献作用,将其添加到路径中。
代码如下:
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
gainPathSum(root);
return res;
}
public int gainPathSum(TreeNode node){
if(node == null)return 0;
// 记录左右子树最大路径和,可能为负数,为负数就不考虑成为路径(直接置0)
int leftMaxPathSum = Math.max(gainPathSum(node.left),0);
int rightMaxPathSum = Math.max(gainPathSum(node.right),0);
// 计算当前节点开始的最大路径和
// 考虑了max(自己,自己+左边,自己+右边,自己 + 左边 + 右边)
int curMaxSum = node.val + leftMaxPathSum + rightMaxPathSum;
res = Math.max(res,curMaxSum);
// 返回值是当前节点作为子节点贡献的值
// 作为子节点的时候,max(自己,自己+左边,自己+右边)
return node.val+Math.max(leftMaxPathSum,rightMaxPathSum);
}
}
时间复杂度:O(N),其中 N是二叉树中的节点个数。对每个节点访问不超过 2 次。
空间复杂度:O(N),其中 N 是二叉树中的节点个数。空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。
LeetCode 687. 最长同值路径(还没做)
二叉树修改与构造
二叉树翻转
这里主要借用力扣 226. 翻转二叉树来回顾前面学到的二叉树的遍历方法,包括递归法、前序迭代法、迭代统一写法、层序遍历法。
力扣 226. 翻转二叉树(递归、迭代、迭代统一模板、层序)
2023.12.13 三刷
递归法(前中后序)翻转
前序和后序递归可以直接调换代码位置,如果用中序的递归,注意经过中间的翻转,左右孩子已经互换了,所以后面要翻转的应该还是root.left
//前序递归方法,修改一下交换代码和递归的代码的顺序就可以变成后序递归
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null)return root;//退出递归条件
//以下3行负责交换
TreeNode temp=root.left;
root.left=root.right;
root.right=temp;
//递归翻转
invertTree(root.left);
invertTree(root.right);
return root;
}
}
//中序递归翻转
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null)return root;
invertTree(root.left);
TreeNode temp=root.left;
root.left=root.right;
root.right=temp;
invertTree(root.left);//已经互换过了,所以还是left
return root;
}
}
迭代法翻转
//前序迭代版本
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null)return null;
Deque<TreeNode> stk= new LinkedList<>();
TreeNode node=new TreeNode();//暂存出栈结点
TreeNode temp=new TreeNode();//用于交换
stk.push(root);
while(!stk.isEmpty()){
node=stk.poll();
temp=node.left;
node.left=node.right;
node.right=temp;
if(node.right!=null)stk.push(node.right);
if(node.left!=null)stk.push(node.left);
}
return root;
}
}
//中序迭代版本
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null)return null;
Deque<TreeNode> stk= new LinkedList<>();
TreeNode node=new TreeNode();//暂存出栈结点
TreeNode temp=new TreeNode();//用于交换
stk.push(root);
while(!stk.isEmpty()){
node=stk.pop();
if(node.right!=null)stk.push(node.right);
temp=node.left;
node.left=node.right;
node.right=temp;
if(node.right!=null)stk.push(node.right);
}
return root;
}
}
//后序迭代版本
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null)return null;
Deque<TreeNode> stk= new LinkedList<>();
TreeNode node=new TreeNode();//暂存出栈结点
TreeNode temp=new TreeNode();//用于交换
stk.push(root);
while(!stk.isEmpty()){
node=stk.pop();
if(node.right!=null)stk.push(node.right);
if(node.left!=null)stk.push(node.left);
temp=node.left;
node.left=node.right;
node.right=temp;
}
return root;
}
}
迭代法(统一版本)
可以用一个模板将前中后序迭代的代码风格统一,这种模板的详细介绍在前面的文章:力扣刷题记录-二叉树的前中后序遍历(递归/迭代/迭代统一写法)里面有讲。
//迭代统一法的中序版本,只需修改if里的语句顺序就可改成前序、后序
class Solution {
public TreeNode invertTree(TreeNode root) {
Deque<TreeNode> stack=new LinkedList<>();
if(root!=null)stack.push(root);
TreeNode temp;
while(!stack.isEmpty()){
TreeNode cur=stack.pop();//cur获取栈顶元素
if(cur!=null){
if(cur.right!=null)stack.push(cur.right);
stack.push(cur);
stack.push(null);
if(cur.left!=null)stack.push(cur.left);
}else{
cur=stack.pop();
temp=cur.left;
cur.left=cur.right;
cur.right=temp;
}
}
return root;
}
}
层序遍历法翻转
//中序+层序
class Solution {
public TreeNode invertTree(TreeNode root) {
Queue<TreeNode> que=new LinkedList<>();
if(root!=null)que.offer(root);
TreeNode temp;
while(!que.isEmpty()){
TreeNode cur=que.poll();
if(cur.left!=null)que.offer(cur.left);
temp=cur.left;
cur.left=cur.right;
cur.right=temp;
if(cur.left!=null)que.offer(cur.left);
}
return root;
}
}
//前序+层序
class Solution {
public TreeNode invertTree(TreeNode root) {
Queue<TreeNode> que=new LinkedList<>();
if(root!=null)que.offer(root);
TreeNode temp;
while(!que.isEmpty()){
int len=que.size();
while(len-->0){
TreeNode cur=que.poll();
// 交换左右孩子
temp=cur.left;
cur.left=cur.right;
cur.right=temp;
// 左右孩子已经交换,先入队的就是右孩子(原来的左孩子)
if(cur.right!=null)que.offer(cur.right);
if(cur.left!=null)que.offer(cur.left);
}
}
return root;
}
}
//后序+层序
class Solution {
public TreeNode invertTree(TreeNode root) {
Queue<TreeNode> que=new LinkedList<>();
if(root!=null)que.offer(root);
TreeNode temp;
while(!que.isEmpty()){
//不用对每一层进行操作的话,就不用记录这一层有多少结点
TreeNode cur=que.poll();
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
temp=cur.left;
cur.left=cur.right;
cur.right=temp;
}
return root;
}
}
改造二叉树
力扣 114. 二叉树展开为链表
2023.12.14 二刷
思路:
public void flatten(TreeNode root)的作用是:输入节点 root,然后 root 为根的二叉树就会被拉平为一条链表。
对于一个节点 x,可以执行以下流程:
1、先利用 flatten(x.left) 和 flatten(x.right) 将 x 的左右子树拉平。
2、将原先的左子树(已拉平)作为x的右子树,且x左子树置为null;
3、将原先的右子树(已拉平)接到当前右子树的末端
图片来自labuladong:
代码如下:
class Solution {
public void flatten(TreeNode root) {
if(root==null)return ;
// 1.将x的左右子树分别拉平
flatten(root.left);
flatten(root.right);
TreeNode left = root.left;
TreeNode right = root.right;
// 2.将原先的左子树(已拉平)作为x的右子树,且x左子树置为null;
root.right = left;
root.left = null;
// 3.将原先的右子树(已拉平)接到当前右子树的末端
TreeNode cur = root;
while(cur.right!=null)cur = cur.right;
cur.right = right;
}
}
构造二叉树
力扣 106. 从中序与后序遍历序列构造二叉树
2023.12.14 三刷
思路:递归
大方向:
中序与后序数组分别用两个索引(左右边界)来限定各自子区间。每次递归都处理中序子区间与对应的后序子区间。不过需要注意区间的闭合情况,一般统一采用左闭右开。
1.首先判断后序区间是否还有元素(右边界-左边界<1),如果没有元素说明走到空节点了,直接返回null;
2.如果区间只有一个元素,则为叶子结点,直接返回即可
3.后序区间的最后一个元素为当前区间的根节点
4.在对应的中序区间内找到根节点
5.由中序区间根节点位置,再将该区间划分为左右子区间(左闭右开)
6.后序区间,参考中序子区间的长度进行划分
7.进行下一轮的递归
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
return myBuildTree(inorder,0,inorder.length,postorder,0,postorder.length);
}
public TreeNode myBuildTree(int[] inorder,int inLeft,int inRight,int[] postorder,int postLeft,int postRight){
// 1.后序的区间如果没有元素,代表节点为空
if(postRight-postLeft<1)return null;
// 2.后序区间只有一个元素,则返回该元素(由于左闭右开,选取左边界)
if(postRight-postLeft == 1)return new TreeNode(postorder[postLeft]);
// 3.后序区间的最后一个元素为当前区间的根节点
int rootValue = postorder[postRight-1];
TreeNode root = new TreeNode(rootValue);
// 4.在对应的中序区间内找到根节点对应的下标(左闭右开)
int rootIndex = 0;
for(int i=inLeft;i<inRight;i++){
if(inorder[i] == rootValue){
rootIndex = i;
}
}
// 5.6.7根据中序子区间大小划分后序子区间大小,确定好边界(左闭右开)
root.left = myBuildTree(inorder,inLeft,rootIndex,postorder,postLeft,postLeft+(rootIndex-inLeft));
root.right = myBuildTree(inorder,rootIndex+1,inRight,postorder,postLeft+(rootIndex-inLeft),postRight-1);// 去掉后序区间最后一个元素
return root;
}
}
力扣 105. 从前序与中序遍历序列构造二叉树
2023.12.14 三刷
与106. 从中序与后序遍历序列构造二叉树是一样的思路,只不过每次从前序数组中取根节点作为中序数组的分割点。
另外每次递归中都需要遍历中序区间去寻找根节点在中序数组中的下标,浪费时间,可以用一个hashmap存储中序数组值与下标对应关系,方便快速找到根节点下标。
class Solution {
//构造中序数组对应的哈希表,方便找到前序数组根节点值在中序数组中的索引
Map<Integer,Integer>inorderMap=new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i=0;i<inorder.length;i++)inorderMap.put(inorder[i],i);
return buildTree1(inorder,0,inorder.length,preorder,0,preorder.length);
}
public TreeNode buildTree1(int[] inorder,int inleft,int inright,int[] preorder,int preleft,int preright){
if(inright-inleft<1)return null;
if(inright-inleft==1)return new TreeNode(inorder[inleft]);
int rootValue=preorder[preleft];
TreeNode root=new TreeNode(rootValue);
int rootIndex=inorderMap.get(rootValue);
root.left=buildTree1(inorder,inleft,rootIndex,preorder,preleft+1,preleft+1+(rootIndex-inleft));
root.right=buildTree1(inorder,rootIndex+1,inright,preorder,preleft+1+(rootIndex-inleft),preright);
return root;
}
}
力扣 654. 最大二叉树
2023.09.05 二刷
思路:
此题类似105与106解题方法,不过更容易一些,每次递归中根据当前区间的最大值位置,将当前区间划分为左右子区间,然后再递归处理。注意区间为左闭右开。
1.先判断当前区间是否含有元素,如果不含,直接返回空节点;
2.再判断当前区间是否只有一个元素,如果只有一个,说明是叶子节点,返回新构造的叶子结点;
3.遍历当前区间,找到最大值,并记录最大值下标;
4.根据最大值的下标,将当前区间划分左区间与右区间,分别对应左子树、右子树
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return buildMaxBinaryTree(nums,0,nums.length);
}
public TreeNode buildMaxBinaryTree(int[] nums,int left,int right){
if(right-left < 1)return null;
if(right-left == 1)return new TreeNode(nums[left]);
int max = -1;
int maxIndex = -1;
for(int i=left;i<right;i++){
if(nums[i]>max){
max = nums[i];
maxIndex = i;
}
}
TreeNode root = new TreeNode(max);
root.left = buildMaxBinaryTree(nums,left,maxIndex);
root.right = buildMaxBinaryTree(nums,maxIndex+1,right);
return root;
}
}
力扣 617. 合并二叉树
2023.09.06 二刷
·递归的思路:
两个二叉树的对应节点可能存在以下三种情况,对于每种情况使用不同的合并方式。
-
如果两个二叉树的对应节点都为空,则合并后的二叉树的对应节点也为空;
-
如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空节点;
-
如果两个二叉树的对应节点都不为空,则合并后的二叉树的对应节点的值为两个二叉树的对应节点的值之和,此时需要显性合并两个节点。
对一个节点进行合并之后,还要对该节点的左右子树分别进行合并。这是一个递归的过程。
//递归法
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
//root1为空只要看root2的结点就行了,若root2也是空那直接返回空
if(root1==null)return root2;
if(root2==null)return root1;
//复用root1结点,直接对其值进行修改
root1.val+=root2.val;
root1.left=mergeTrees(root1.left,root2.left);
root1.right=mergeTrees(root1.right,root2.right);
return root1;
}
}
//迭代法(队列,层序遍历)
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1==null)return root2;
if(root2==null)return root1;
Queue<TreeNode> que=new LinkedList<>();
que.offer(root1);
que.offer(root2);
while(!que.isEmpty()){
//先进先出
TreeNode node1=que.poll();
TreeNode node2=que.poll();
//复用树1的结点(会修改树1的结点值)
node1.val+=node2.val;
//对当前遍历到的两个结点的左孩子进行判断
//若均不为空,需要等待下一次合并,先入队
if(node1.left!=null&&node2.left!=null){
que.offer(node1.left);
que.offer(node2.left);
}
//若node2左孩子不为空,说明node1左孩子空
//直接把node2左子树赋给node1左孩子
//后续无需再对该结点的孩子进行操作,因为该结点后续的链接关系已经确定
else if(node2.left!=null)
node1.left=node2.left;
//不用考虑node1左孩子不空,node2左孩子空的情况
//因为本来就是复用树1的结点,这种情况直接用原来的结点就行
//对当前遍历到的两个结点的右孩子进行判断,思路同上
if(node1.right!=null&&node2.right!=null){
que.offer(node1.right);
que.offer(node2.right);
}else if(node2.right!=null)
node1.right=node2.right;
}
return root1;
}
}
二叉搜索树属性
力扣 700. 二叉搜索树中的搜索
0223.09.06 三刷
思路:
递归和迭代均利用二叉搜索树特性。
//递归法(空间效率较差)
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
//碰到空节点了或者找到目标节点,返回这个结点
if(root==null||root.val==val)return root;
if(val<root.val)return searchBST(root.left,val);
if(val>root.val)return searchBST(root.right,val);
return null;
}
}
//迭代法
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
while(root!=null){
if(root.val==val)return root;
else if(root.val>val)root=root.left;
//只剩下root.val<val的情况
else root=root.right;
}
return null;
}
}
力扣 230. 二叉搜索树中第K小的元素
2023.12.15 二刷
思路:在中序遍历的同时统计当前遍历到的结点的位序,若当前遍历到的节点值=k,记录答案并返回
代码如下:
class Solution {
int res=0,rank=0;
public int kthSmallest(TreeNode root, int k) {
inorder(root,k);
return res;
}
public void inorder(TreeNode root,int k){
if(root == null)return;
inorder(root.left,k);
// 开始处理中间节点
rank++;
if(rank ==k){
res = root.val;
return;
}
inorder(root.right,k);
}
}
题目有额外要求:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
以下解法来自力扣官方题解:
前面的方法之所以需要中序遍历前 k 个元素,是因为不知道子树的结点数量,不得不通过遍历子树的方式来获知。
因此,可以记录以每个结点为根结点的子树的结点数,并在查找第 k 小的值时,使用如下方法搜索:
令 node 等于根结点,开始搜索。
对当前结点 node进行如下操作:
- 如果 node 的左子树的结点数 left 小于 k−1,则第 k 小的元素一定在 node的右子树中,令 node 等于其的右子结点,k 等于 k−left−1,并继续搜索;
- 如果 node的左子树的结点数 left 等于 k−1,则第 kkk 小的元素即为 node,结束搜索并返回 node 即可;
- 如果 node 的左子树的结点数 left 大于 k−1,则第 k 小的元素一定在 node 的左子树中,令 node等于其左子结点,并继续搜索。
在实现中,我们既可以将以每个结点为根结点的子树的结点数存储在结点中,也可以将其记录在哈希表中。
记录在节点中:
class TreeNode {
int val;
// 以该节点为根的树的节点总数
int size;
TreeNode left;
TreeNode right;
}
有了 size 字段,外加 BST 节点左小右大的性质,对于每个节点 node 就可以通过 node.left 推导出 node 的排名,从而做到我们刚才说到的对数级算法。
当然,size 字段需要在增删元素的时候需要被正确维护,力扣提供的 TreeNode 是没有 size 这个字段的,所以我们这道题就只能利用 BST 中序遍历的特性实现了。
哈希表的代码比较长:
class Solution {
public int kthSmallest(TreeNode root, int k) {
MyBst bst = new MyBst(root);
return bst.kthSmallest(k);
}
}
class MyBst {
TreeNode root;
Map<TreeNode, Integer> nodeNum;
public MyBst(TreeNode root) {
this.root = root;
this.nodeNum = new HashMap<TreeNode, Integer>();
countNodeNum(root);
}
// 返回二叉搜索树中第k小的元素
public int kthSmallest(int k) {
TreeNode node = root;
while (node != null) {
int left = getNodeNum(node.left);
if (left < k - 1) {
node = node.right;
k -= left + 1;
} else if (left == k - 1) {
break;
} else {
node = node.left;
}
}
return node.val;
}
// 统计以node为根结点的子树的结点数
private int countNodeNum(TreeNode node) {
if (node == null) {
return 0;
}
nodeNum.put(node, 1 + countNodeNum(node.left) + countNodeNum(node.right));
return nodeNum.get(node);
}
// 获取以node为根结点的子树的结点数
private int getNodeNum(TreeNode node) {
return nodeNum.getOrDefault(node, 0);
}
}
力扣 98. 验证二叉搜索树
2023.12.15 四刷
·递归思路:
题目中二叉搜索树性质:
结点左子树全都小于当前结点值,右子树全都大于当前节点值,其左右子树递归该定义。
这启示我们设计一个递归函数 myJudegeBST(root, min, max) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (min,max) 的范围内(注意是开区间)。如果 root 节点的值 val 不在(min,max)的范围内说明不满足条件直接返回false,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。
那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 max改为 root.val,即调用 myJudegeBST(root.left, min, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 myJudegeBST(root.right, root.val, max)。
class Solution {
public boolean isValidBST(TreeNode root) {
//测试用例中有int最小值,因此用long
return myJudegeBST(root,Long.MIN_VALUE,Long.MAX_VALUE);
}
public boolean myJudegeBST(TreeNode cur,long min,long max){
//递归判断,空树也是二叉搜索树
if(cur==null)return true;
//左右边界为开区间,因此若是相等,则不符合区间范围
//即当前结点值必须在开区间(min,max)内
if(cur.val<=min||cur.val>=max)return false;
//递归判断左右子树,并更新相应区间
return myJudegeBST(cur.left,min,cur.val)&&myJudegeBST(cur.right,cur.val,max);
}
}
迭代法
//迭代法(中序遍历)
//二叉搜索树的中序迭代,每次处理中间结点的时候,当前值总应该大于前一个结点值
//若小于等于则错,否则继续遍历,若遍历结束未出错,则是二叉搜索树
class Solution {
public boolean isValidBST(TreeNode root) {
//空树也是二叉搜索树
if(root==null)return true;
Deque<TreeNode>stk=new LinkedList<>();
TreeNode pre=null;
while(root!=null||!stk.isEmpty()){
//一直往左下走
if(root!=null){
stk.push(root);
root=root.left;
}else{//走到空节点了,此时栈顶元素就是“中间结点”
//处理中间结点
TreeNode cur=stk.poll();
//pre!=null防止null.val情况出现
if(pre!=null&&pre.val>=cur.val)return false;
//向后遍历
pre=cur;
root=cur.right;//右孩子
}
}
return true;
}
}
// 利用二叉搜索树的中序遍历严格有序特性
class Solution {
TreeNode prev = null;// 用于记录中序遍历过程中的前一个节点
public boolean isValidBST(TreeNode root) {
if(root == null)return true;
// 中序遍历,先往左子树走
boolean lFlag = isValidBST(root.left);
// 开始处理“中”
// 先保证前一个节点不是空的,再保证当前节点比前一个节点大,如果更小,则不符合搜索树规则
if(prev != null && root.val <= prev.val)return false;
prev = root;// 更新prev节点
// 处理“右”
boolean rFlag = isValidBST(root.right);
return lFlag && rFlag;
}
}
力扣 530. 二叉搜索树的最小绝对差
2023.09.16 二刷
思路:由于二叉搜索树的性质,其中序遍历的结果一定是升序的,所以最小的绝对差一定是在遍历过程中前后相邻的两个结点的差值。只要在中序遍历中记录前序结点的值,在遍历当前结点时进行差值计算即可比较最小绝对差。
//递归法(中序)
class Solution {
TreeNode pre=null;//记录前一个结点
int mindiff=10001;//记录最小绝对差
//只能新创一个中序递归函数,因为中序递归碰到空节点要返回上一层,int型没办法返回
public int getMinimumDifference(TreeNode root) {
inorder(root);
return mindiff;
}
public void inorder(TreeNode root){
if(root==null)return;
inorder(root.left);
//中序处理
if(pre!=null)mindiff=Math.min(mindiff,root.val-pre.val);
pre=root;//pre向后移动
inorder(root.right);
}
}
//迭代法(中序遍历)
class Solution {
public int getMinimumDifference(TreeNode root) {
Stack<TreeNode>stack=new Stack<>();
TreeNode pre=null;//记录中序遍历过程当前结点的前序结点
int mindiff=10001;//记录最小值
while(root!=null||!stack.isEmpty()){
if(root!=null){
stack.push(root);
root=root.left;
}else{
TreeNode cur=stack.pop();
//二叉搜索树的中序遍历的当前结点一定比前一个结点值大
if(pre!=null){
mindiff=Math.min(mindiff,cur.val-pre.val);
}
pre=cur;
root=cur.right;//向后走
}
}
return mindiff;
}
}
力扣 501. 二叉搜索树中的众数
2023.09.29 二刷
思路:
这题根据前几题的铺垫,很容易想到中序遍历二叉树,建立Map<key,value>的关系,key为遍历的结点值,value为结点值出现的次数;然后在map中找到value最大的,即为所求众数(众数可能有多个);但是这种方法的空间复杂度较高,因为需要额外的哈希表。
该题可以利用二叉搜索树特性,对该树进行中序遍历,这题的中序遍历结果是一个非递减的有序序列,重复出现的数在有序序列中是连续的,根据这个特性,可以实时更新记录当前的众数:
pre记录前一个遍历到的结点,count记录当前数字重复的次数,maxCount记录已扫描过到数中出现次数最多的数字。
更新函数(update)的内容:
·对count的操作:
1.若为第一个结点(pre=null),count=1;
2.若当前结点值与上一个结点值不同(pre.val!=cur.val),置count=1;
3.除以上两种情况外,剩下的情况就是上一个数和当前数相同,count++;
·对maxCount的操作:
1.若count=maxCount,说明当前遍历到的数和众数出现次数一样多,加入resList中(resList.add(cur.val);
2.若count>maxCount,说明当前遍历到的数是新的众数,需要清除原来resList中的数(resList.clear()),把新众数加入resList,并且把maxCount置为新众数的数量count(maxCount=count);
//递归法
class Solution {
List<Integer>resList=new ArrayList<>();
//java会给int的参数赋初值0
int count,maxCount;
TreeNode pre=null;
//注意需要返回int类型的
public int[] findMode(TreeNode root) {
inorder(root);
//java8的方法,可以将ArrayList<Integer>转成int[];
//return resList.stream().mapToInt(Integer::intValue).toArray();
//常规做法,挨个赋值
int[] res=new int[resList.size()];
for(int i=0;i<resList.size();i++)res[i]=resList.get(i);
return res;
}
//中序递归遍历
public void inorder(TreeNode root){
if(root==null)return;
inorder(root.left);
update(root);//处理结点
inorder(root.right);
}
//处理函数,更新众数
public void update(TreeNode cur){
//pre=null,即cur为第一个遍历到的结点
//cur.val!=pre.val,即遇到新的数
if(pre==null||cur.val!=pre.val)count=1;
else ++count;//若与前一个数相同则计数+1
//当前遍历的数出现次数与前面记录的最大次数相同,则该数也是众数
if(count==maxCount)resList.add(cur.val);
//当前遍历的数出现次数大于原先的记录,新众数出现,原来记录的众数失效
//清除resList,改成加入新的众数,并更新最大出现次数
else if(count>maxCount){
resList.clear();
resList.add(cur.val);
maxCount=count;
}
pre=cur;//最后要把pre后移到当前结点
}
}
//迭代法(与上一题差不多的写法)
class Solution {
public int[] findMode(TreeNode root){
List<Integer>resList=new ArrayList<>();
int count=0,maxCount=0;
TreeNode pre=null;
Stack<TreeNode>stack=new Stack<>();
while(root!=null||!stack.isEmpty()){
if(root!=null){
stack.push(root);
root=root.left;
}else{
//只是为了方便理解设置了一个cur,可以全用root代替
TreeNode cur=stack.pop();
if(pre==null||cur.val!=pre.val)count=1;
else ++count;
if(count==maxCount)resList.add(cur.val);
else if(count>maxCount){
resList.clear();
resList.add(cur.val);
maxCount=count;
}
pre=cur;//最后要把pre后移到当前结点
root=cur.right;
}
}
int[] res=new int[resList.size()];
for(int i=0;i<resList.size();i++)res[i]=resList.get(i);
return res;
}
}
力扣 538. 把二叉搜索树转换为累加树
这题同力扣1038题,要求把二叉搜索树每个结点值修改成原来树中大于等于该结点值的累加和。二叉搜索树中序遍历是从小到大的递增序列,只要逆中序遍历,遍历序列就会是从大到小的顺序,那么只要遍历到当前结点的时候,将其值加上前面遍历到的结点值的累加和即可。
class Solution {
int sum=0;
public TreeNode convertBST(TreeNode root) {
dfs(root);
return root;
}
public void dfs(TreeNode root){
if(root==null)return;
dfs(root.right);
sum+=root.val;
root.val=sum;
dfs(root.left);
}
}
二叉搜索树修改与构造
力扣 701. 二叉搜索树中的插入操作
递归:
//递归法
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root==null)return new TreeNode(val);
//需要用root.left和root.right接住返回的结点以及关系
if(root.val>val)root.left= insertIntoBST(root.left,val);
else if(root.val<val)root.right= insertIntoBST(root.right,val);
return root;
}
}
//迭代法
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root==null)return new TreeNode(val);
TreeNode pre=null,cur=root;
//遍历到符合条件的空节点为止(pre记录为空的前一个结点)
while(cur!=null){
pre=cur;
if(cur.val>val)cur=cur.left;
else cur=cur.right;
}
//确定是pre左孩子还是右孩子
if(pre.val>val)pre.left=new TreeNode(val);
else pre.right=new TreeNode(val);
return root;
}
}
力扣 450. 删除二叉搜索树中的节点
原题链接
删除结点应该分五种情况:
1.没找到删除结点,返回null;
2.删除节点为叶子结点,直接删除;
3.删除节点左孩子空,右孩子不空,则右孩子补位到删除结点处;
4.删除结点右孩子空,左孩子不空,左孩子补位;
5.左右孩子都不空,则按搜索树特性,删除结点的左孩子成为右孩子的最左下孩子(中序后继),删除结点的右孩子补位删除结点,如下图;
代码如下:
//递归法
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if(root==null)return null;
if(root.val>key)root.left=deleteNode(root.left,key);
else if(root.val<key)root.right=deleteNode(root.right,key);
else{//找到要删除节点
if(root.left==null&&root.right==null)return null;
else if(root.left==null)return root.right;
else if(root.right==null)return root.left;
else{//左右孩子都不为空(找中序后继)
TreeNode cur=root.right;
//找到删除结点右子树最左下结点
while(cur.left!=null)cur=cur.left;
//把删除节点左孩子移到右子树最左下结点的左孩子处
cur.left=root.left;
//被删除结点的右孩子补位
root=root.right;
}
return root;
}
return root;
}
}
力扣 669. 修剪二叉搜索树
递归法:
主要思想是:①若当前结点值小于low,说明当前结点在范围左边,应该递归当前结点的右子树,并且返回右子树里面符合条件的头结点。②若当前结点值大于high,说明当前结点值在范围右边,要递归当前结点的左子树,并且返回左子树里面符合条件的头结点。③剩下的就是当前结点值在low和high之间,那么就将下一层处理好的左右子树头结点返回给当前结点的左右孩子。
//递归法
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
if(root==null)return null;
if(root.val<low)return trimBST(root.right,low,high);
if(root.val>high)return trimBST(root.left,low,high);
//到这都没return说明root.val在low和high之间
//向下递归,用当前层的左右孩子接住返回的结点
root.left=trimBST(root.left,low,high);
root.right=trimBST(root.right,low,high);
return root;
}
}
这题递归法代码有点不好理解是如何修剪二叉树的。接下来利用图示来帮助理解(图片来自代码随想录公众号):在这棵树上修剪low=1,high=3
第一层递归(3):root.val=3的时候,是在区间内,跳过前三个if判断,用root.left和root.right接住下一层递归返回的头结点。
第二层递归(0):root.val=0<1,进入第三层递归(root.val=2),返回第三层递归返回的头结点
第二层递归(4):root.val=4>3,进入第三层递归(root==null),直接返回null;
第三层递归(2):root.val=2,在范围内,用左右孩子接住下一层(第四层)递归的返回结点(root.left和root.right)
第四层递归(1):root.val=1,走到最后return root;回到第三层递归(root.val=2),这样第三层递归root.left=1,root.right=null;第三层再return root;回到第二层递归(root.val=0),这一层的返回值就是第三层的结点2;而这个结点被第一层的root.left接住了,就变成第一层结点3的左孩子是结点2;最终返回的就是最后的根结点3;
关键之处就在于第二层递归到0结点时,它返回的不是0结点,而是利用特性返回0结点右子树中符合条件的头结点2,这一步就相当于修剪掉了0结点,并且在更上层的3结点处,用3结点的左孩子接住了返回的2结点。
迭代法:
主要思想:①从根节点处出发,走到符合区间范围的结点处,作为根节点(即root.val要在low和high之间)。②处理根节点的左孩子,修剪掉左子树中小于low的结点。③处理根节点右孩子,修剪掉右子树中大于high的结点。
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
//①从根节点出发找到在区间范围内的结点作为新的根节点
while(root!=null&&(root.val<low||root.val>high)){
if(root.val<low)root=root.right;
if(root.val>high)root=root.left;
}
TreeNode cur= root;
//处理左子树中小于low的情况
while(cur!=null){
//必须用while,不能用if
//当前结点左孩子结点不符合区间,可能符合的情况在左孩子结点的右子树里
//将左孩子结点的右子树头结点赋给左孩子,相当于修剪掉了原来的左孩子结点
while(cur.left!=null&&cur.left.val<low){
cur.left=cur.left.right;
}
cur=cur.left;
}
cur=root;
//处理右子树,同理
while(cur!=null){
while(cur.right!=null&& cur.right.val>high){
cur.right=cur.right.left;
}
cur=cur.right;
}
return root;
}
}
力扣 108. 将有序数组转换为二叉搜索树
2023.12.15 二刷
思路:
将严格递增的有序数组构造成绝对平衡的二叉搜索树,最重要的就是找分割点。因为数组的严格递增特性,只要找到位于中间位置的分割点,再划分好左右区间范围就很容易了。思路可以参考力扣 106.从中序与后序遍历序列构造二叉树。本题的构造过程比106题更简单。
每层递归中找到当前区间的中间结点作为这层递归的根结点,再用这个根结点划分出下一个左右区间,用根结点的左右孩子接住下一层递归返回的左右区间的结点即可。
class Solution {
public TreeNode sortedArrayToBST (int[] nums) {
TreeNode root = buildBST(nums,0,nums.length);
return root;
}
public TreeNode buildBST(int[] nums,int l,int r){
if(r-l<1)return null;
if(r-l == 1)return new TreeNode(nums[l]);
int mid = l+((r-l)>>1);
TreeNode root = new TreeNode(nums[mid]);
root.left = buildBST(nums,l,mid);
root.right = buildBST(nums,mid+1,r);
return root;
}
}
公共祖先问题
力扣 236. 二叉树的最近公共祖先
2023.09.29 二刷
这题是找距离给定两个结点最近的公共祖先,百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例:
比较好想到的是在二叉树中找到p、q两个结点,再向上走找到公共祖先。那么如何可以确定自己找到了公共祖先?只要在自底向上的过程中,发现左右子树各自出现结点p、q中的一个,那么这个结点就是最近的公共祖先了,而后序遍历可以契合这样的自底向上的过程。
递归三部曲:
1.确定递归函数参数和返回值
·参数就是题目给出的函数中带的root、p、q;
·返回值:按题意需要返回一个结点(公共祖先);
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){
}
此外,对于递归函数什么时候需要返回值,什么时候不需要也需要分情况讨论。
- 需要搜索整棵二叉树且不用处理递归返回值,递归函数就不用返回值。(力扣113.路径总和ii)
- 需要搜索整棵二叉树且需要处理返回值,递归函数就要返回值。(本题:力扣 236.二叉树的最近共公共祖先)
- 需要搜索一条符合条件的路径,递归函数就一定需要返回值。(力扣 112.路径总和)
其中,搜索一条边的写法:
if(递归函数(root.left))return;
if(递归函数(root.right))return;
搜索整棵树的写法:
TreeNode left=递归函数(root.left);
TreeNode right=递归函数(root.right);
对left与right进行处理;
在这题里面,需要对整棵树进行搜索。
2.确定终止条件
·找到p或q,或者遇到空节点就返回结点。
if(root==p||root==q||root==null)return root;
3.确定单层递归逻辑
·由1里面的介绍,这题需要搜索整棵树,用left和right记录返回的结点,然后对left和right进行处理。
①因为是从下往上递归返回的,所以如果left和right都不为空,就直接返回root,此时root就是最近公共祖先。
②若left和right中只有一个为空,则返回不为空的那个,因为在自底向上递归返回的过程中,搜寻到的结点一定在不为空的子树里;
③剩下的情况就是le和right都为空,那直接返回null就行;
TreeNode left=lowestCommonAncestor(root.left,p,q);
TreeNode right=lowestCommonAncestor(root.right,p,q);
if(left!=null&&right!=null)return root;
else if(left==null&&right!=null)return right;
else if(left!=null&&right==null) return left;
else return null;
完整代码:
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){
if(root==p||root==q||root==null)return root;
TreeNode left=lowestCommonAncestor(root.left,p,q);
TreeNode right=lowestCommonAncestor(root.right,p,q);
if(left!=null&&right!=null)return root;
else if(left==null&&right!=null)return right;
else if(left!=null&&right==null) return left;
else return null;
}
}
力扣 235. 二叉搜索树的最近公共祖先
原题链接
2023.12.14 二刷
这题相比于上一题,可以利用二叉搜索树的特性,要找p、q的公共祖先,只要保证在从上到下的递归过程中,返回第一个符合p.val<=root.val<=q.val的结点即可,第一个符合条件的结点就是最近公共祖先。
从根节点开始遍历;
- 如果当前节点的值大于 p 和 q 的值,说明 p 和 q 应该在当前节点的左子树,因此将当前节点移动到它的左子节点;
- 如果当前节点的值小于 p 和 q 的值,说明 p 和 q 应该在当前节点的右子树,因此将当前节点移动到它的右子节点;
- 如果当前节点的值不满足上述两条要求,那么说明当前节点就是「分岔点」。此时,p 和 q 要么在当前节点的不同的子树中,要么其中一个就是当前节点。
//递归法
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root.val<p.val&&root.val<q.val)return lowestCommonAncestor(root.right,p,q);
else if(root.val>p.val&&root.val>q.val)return lowestCommonAncestor(root.left,p,q);
//剩下一种情况就是root.val夹在p、q的值之间,这时直接返回root即可
else return root;
}
}
//迭代法(利用二叉搜索树的简单迭代)
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root!=null){
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;
}
return root;//走到最后都没有返回,说明走到null了,直接返回null也行
}
}