代码随想录刷题day21丨669. 修剪二叉搜索树,108.将有序数组转换为二叉搜索树,538.把二叉搜索树转换为累加树,二叉树总结
1.题目
1.1修剪二叉搜索树
-
题目链接:669. 修剪二叉搜索树 - 力扣(LeetCode)
-
视频讲解:你修剪的方式不对,我来给你纠正一下!| LeetCode:669. 修剪二叉搜索树_哔哩哔哩_bilibili
-
文档讲解:https://programmercarl.com/0669.%E4%BF%AE%E5%89%AA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.html
-
解题思路:递归法
-
确定递归函数的参数以及返回值
-
这里我们为什么需要返回值呢?
因为是要遍历整棵树,做修改,其实不需要返回值也可以,我们也可以完成修剪(其实就是从二叉树中移除节点)的操作。
但是有返回值,更方便,可以通过递归函数的返回值来移除节点。
-
-
确定终止条件
- 修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。
-
确定单层递归的逻辑
- 如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。
- 如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。
- 接下来要将下一层处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right。
- 最后返回root节点
-
-
代码:
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public TreeNode trimBST(TreeNode root, int low, int high) { if(root == null){ return null; } if(root.val < low){ TreeNode right = trimBST(root.right,low,high); return right; } if(root.val > high){ TreeNode left = trimBST(root.left,low,high); return left; } root.left = trimBST(root.left,low,high); root.right = trimBST(root.right,low,high); return root; } }
-
总结:
- 错误的想法就是:递归处理,然后遇到
root->val < low || root->val > high
的时候直接return NULL
- 错误的想法就是:递归处理,然后遇到
1.2将有序数组转换为二叉搜索树
-
题目链接:108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)
-
视频讲解:构造平衡二叉搜索树!| LeetCode:108.将有序数组转换为二叉搜索树_哔哩哔哩_bilibili
-
文档讲解:https://programmercarl.com/0108.%E5%B0%86%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E8%BD%AC%E6%8D%A2%E4%B8%BA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.html
-
解题思路:递归
- 从数组中间位置取值作为节点元素
- 本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
- 分割点就是数组中间位置的节点。
- 如果数组长度为偶数,中间节点有两个,取哪一个?
- 取哪一个都可以,只不过构成了不同的平衡二叉搜索树。
- 左下标 left 和右下标 right
- 这里注意,我这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间,这又涉及到循环不变量。
-
代码:
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public TreeNode sortedArrayToBST(int[] nums) { TreeNode root = traversal(nums,0,nums.length -1); return root; } public TreeNode traversal(int[] nums,int left,int right){ //左闭右闭 if(left > right){ return null; } int mid = (left + right)/2; TreeNode root = new TreeNode(nums[mid]); root.left = traversal(nums,left,mid -1); root.right = traversal(nums,mid+1,right); return root; } }
-
总结:
- 在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。
- 迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下标,一个队列放右区间下标。
1.3把二叉搜索树转换为累加树
-
题目链接:538. 把二叉搜索树转换为累加树 - 力扣(LeetCode)
-
视频讲解:普大喜奔!二叉树章节已全部更完啦!| LeetCode:538.把二叉搜索树转换为累加树_哔哩哔哩_bilibili
-
文档讲解:https://programmercarl.com/0538.%E6%8A%8A%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E8%BD%AC%E6%8D%A2%E4%B8%BA%E7%B4%AF%E5%8A%A0%E6%A0%91.html
-
解题思路:递归(右中左)+ 双指针
- 从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了。
- 需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。
- 需要定义一个全局变量pre,用来保存cur节点的前一个节点的数值,定义为int型就可以了。
-
代码:
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { int pre = 0; public TreeNode convertBST(TreeNode root) { traversal(root); return root; } public void traversal(TreeNode cur){ if(cur == null){ return; } //右 traversal(cur.right); //中 cur.val += pre; pre = cur.val; //左 traversal(cur.left); } }
-
总结:
- 注意要右中左来遍历二叉树, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。
2.二叉树总结
-
看到递归,就去想:返回值、参数是什么?终止条件是什么?单层逻辑是什么?
-
递归与迭代究竟谁优谁劣呢?
-
从时间复杂度上其实迭代法和递归法差不多(在不考虑函数调用开销和函数调用产生的堆栈开销),但是空间复杂度上,递归开销会大一些,因为递归需要系统堆栈存参数返回值等等。
-
递归更容易让程序员理解,但收敛不好,容易栈溢出。
-
这么说吧,递归是方便了程序员,难为了机器(各种保存参数,各种进栈出栈)。
-
在实际项目开发的过程中我们是要尽量避免递归!因为项目代码参数、调用关系都比较复杂,不容易控制递归深度,甚至会栈溢出
-
一定要掌握前中后序一种迭代的写法,并不因为某种场景的题目一定要用迭代,而是现场面试的时候,面试官看你顺畅的写出了递归,一般会进一步考察能不能写出相应的迭代。
-
层序遍历遍历相对容易一些,只要掌握基本写法(也就是框架模板),剩下的就是在二叉树每一行遍历的时候做做逻辑修改。
-
翻转二叉树的递归用中序遍历是不行的,因为使用递归的中序遍历,某些节点的左右孩子会翻转两次。
-
根节点的高度就是二叉树的最大深度
-
使用前序(中左右)的遍历顺序,这才是真正求深度的逻辑!
-
求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。
-
迭代法中究竟什么时候用队列,什么时候用栈?
- 如果是模拟前中后序遍历就用栈,如果是适合层序遍历就用队列,当然还是其他情况,那么就是 先用队列试试行不行,不行就用栈
-
回溯隐藏在traversal(cur->left, path + “->”, result);中的 path + “->”。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。
-
求二叉树的各种最值,就想应该采用什么样的遍历顺序,确定了遍历循序,其实就和数组求最值一样容易了
-
递归函数究竟什么时候需要返回值,什么时候不要返回值?
- 一般情况下:如果需要搜索整棵二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。
- 特别是有些时候 递归函数的返回值是bool类型,一些同学会疑惑为啥要加这个,其实就是为了找到一条边立刻返回。
- 其实还有一种就是后序遍历需要根据左右递归的返回值推出中间节点的状态,这种需要有返回值
-
构造二叉树有三个注意的点:
- 分割时候,坚持区间不变量原则,左闭右开,或者左闭右闭。
- 分割的时候,注意后序 或者 前序已经有一个节点作为中间节点了,不能继续使用了。
- 如何使用切割后的后序数组来切合中序数组?利用中序数组大小一定是和后序数组的大小相同这一特点来进行切割。
-
为什么前序和后序不能唯一构成一棵二叉树?
- 因为没有中序遍历就无法确定左右部分,也就无法分割
-
注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。
-
递归函数什么时候加if,什么时候不加if ?
- 其实就是控制空节点(空指针)是否进入递归,是不同的代码实现方式,都是可以的。
- 一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。
-
如何 一起遍历两棵二叉树?
- 迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。
-
大多二叉搜索树的题目,其实都离不开中序遍历,因为这样就是有序的。
-
二叉搜索树的特性?
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
-
我们在验证二叉搜索树的时候,有两个陷阱:
- 陷阱一
不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了,而是左子树都小于中间节点,右子树都大于中间节点。
- 陷阱二
在一个有序序列求最值的时候,不要定义一个全局变量,然后遍历序列更新全局变量求最值。因为最值可能就是int 或者 longlong的最小值。
推荐要通过前一个数值(pre)和后一个数值比较(cur),得出最值。
在二叉树中通过两个前后指针作比较,会经常用到。
-
二叉搜索树和中序遍历是好朋友!
-
平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?
- 是的,是二叉搜索树和平衡二叉树的结合。
-
平衡二叉树与完全二叉树的区别在于底层节点的位置?
- 是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。
-
堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?
- 堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树。
-