目录
二叉搜索树属性
力扣 700. 二叉搜索树中的搜索
//递归法(空间效率较差)
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;
}
}
力扣 98. 验证二叉搜索树
原题链接
·递归思路:
题目中二叉搜索树性质:
结点左子树全都小于当前结点值,右子树全都大于当前节点值,其左右子树递归该定义。
这启示我们设计一个递归函数 edgeJudge(root, min, max) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (min,max) 的范围内(注意是开区间)。如果 root 节点的值 val 不在(min,max)的范围内说明不满足条件直接返回false,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。
那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 max改为 root.val,即调用 edgeJudge(root.left, min, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 edgeJudge(root.right, root.val, max)。
class Solution {
public boolean isValidBST(TreeNode root) {
//测试用例中有int最小值,因此用long
return edgeJudge(root,Long.MIN_VALUE,Long.MAX_VALUE);
}
public boolean edgeJudge(TreeNode cur,long min,long max){
//递归判断,空树也是二叉搜索树
if(cur==null)return true;
//左右边界为开区间,因此若是相等,则不符合区间范围
//即当前结点值必须在开区间(min,max)内
if(cur.val<=min||cur.val>=max)return false;
//递归判断左右子树,并更新相应区间
return edgeJudge(cur.left,min,cur.val)&&edgeJudge(cur.right,cur.val,max);
}
}
迭代法
//迭代法(中序遍历)
//二叉搜索树的中序迭代,每次处理中间结点的时候,当前值总应该大于前一个结点值
//若小于等于则错,否则继续遍历,若遍历结束未出错,则是二叉搜索树
class Solution {
public boolean isValidBST(TreeNode root) {
//空树也是二叉搜索树
if(root==null)return true;
Stack<TreeNode>stk=new Stack<>();
TreeNode pre=null;
while(root!=null||!stk.isEmpty()){
//一直往左下走
if(root!=null){
stk.push(root);
root=root.left;
}else{//走到空节点了,此时栈顶元素就是“中间结点”
//处理中间结点
TreeNode cur=stk.pop();
//pre!=null防止null.val情况出现
if(pre!=null&&pre.val>=cur.val)return false;
//向后遍历
pre=cur;
root=cur.right;//右孩子
}
}
return true;
}
}
力扣 530. 二叉搜索树的最小绝对差
思路:由于二叉搜索树的性质,其中序遍历的结果一定是升序的,所以最小的绝对差一定是在遍历过程中前后相邻的两个结点的差值。只要在中序遍历中记录前序结点的值,在遍历当前结点时进行差值计算即可比较最小绝对差。
//递归法(中序)
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. 二叉搜索树中的众数
原题链接
思路:
这题根据前几题的铺垫,很容易想到中序遍历二叉树,建立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. 将有序数组转换为二叉搜索树
原题链接
思路:
将严格递增的有序数组构造成绝对平衡的二叉搜索树,最重要的就是找分割点。因为数组的严格递增特性,只要找到位于中间位置的分割点,再划分好左右区间范围就很容易了。思路可以参考力扣 106.从中序与后序遍历序列构造二叉树。本题的构造过程比106题更简单。
每层递归中找到当前区间的中间结点作为这层递归的根结点,再用这个根结点划分出下一个左右区间,用根结点的左右孩子接住下一层递归返回的左右区间的结点即可。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
TreeNode root=buildBST(nums,0,nums.length-1);
return root;
}
public TreeNode buildBST(int[] nums,int l,int r){
if(l>r)return null;//超界判断
int mid=l+((r-l)>>1);//防int溢出
TreeNode root=new TreeNode(nums[mid]);
//[l,r]左闭右闭
root.left=buildBST(nums,l,mid-1);
root.right=buildBST(nums,mid+1,r);
return root;
}
}
公共祖先问题
力扣 236. 二叉树的最近公共祖先
原题链接
这题是找距离给定两个结点最近的公共祖先,百度百科中最近公共祖先的定义为:“对于有根树 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. 二叉搜索树的最近公共祖先
原题链接
这题相比于上一题,可以利用二叉搜索树的特性,要找p、q的公共祖先,只要保证在从上到下的递归过程中,返回第一个符合p.val<=root.val<=q.val的结点即可,第一个符合条件的结点就是最近公共祖先。
//递归法
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也行
}
}