代码随想录 二叉树模块小结(2)

这部分主要记录搜索树的题型和写题方法。

1、二叉搜索树的属性

2、二叉搜索树的修改与构造

目录

一、二叉搜索树的属性

1.2、是否是二叉搜索树

思路一

思路二

1.3、二叉搜索树的搜索

1.4、二叉搜索树的最小绝对差

1.5、二叉搜索树的众数

二、二叉搜索树的修改与构造

2.2、二叉搜索树的插入

2.3、二叉搜索树的删除

2.4、二叉搜索树的修剪

2.5、二叉搜索树的构造


一、二叉搜索树的属性

1.2、是否是二叉搜索树

对应题目98. 验证二叉搜索树 - 力扣(LeetCode)

思路一

搜索树的特点就是根节点的值大于左子树的所有值,小于右子树的所有值。

中序有序

那么判断一颗树是否是二叉搜索树就比较好想了,直接中序遍历树,看看是否是递增有序的。

遍历过程中不断保存最小值,一旦当前节点的值小于最小值,说明中序不有序,则一定不是搜索树。

代码如下

class Solution {
    long min = Long.MIN_VALUE;  // 一定要用long类型
    public boolean isValidBST(TreeNode root) {
        return isValidBST1(root);
    }

    public boolean isValidBST1(TreeNode root) {
        if (root == null) return true;
        boolean left =  isValidBST1(root.left);
        if (root.val > min) min = root.val;
        else return false;
        boolean right =  isValidBST1(root.right);
        return left && right;
    }
}

思路二

第二种思路是使用前序遍历,在遍历左右子树的过程中不断更新最大值和最小值的边界,一旦当前节点越界,则说明不是二叉搜索树。

代码如下

class Solution {
    public boolean isValidBST(TreeNode root) {
        long min = Long.MIN_VALUE;   // 一定要用long类型
        long max = Long.MAX_VALUE;
        return isValidBST(root, min, max);
    }

    public boolean isValidBST(TreeNode root, long min, long max) {
        if (root == null) return true;
        // 越界判断 要加= ,因为重复也是不行的
        if (root.val <= min || root.val >= max) return false;
        // 向左遍历就更新右边界,向右遍历就更新左边界
        // 左右子树都要满足条件 才能是搜索树
        return isValidBST(root.left, min, root.val) && isValidBST(root.right, root.val, max);
    }
}

1.3、二叉搜索树的搜索

对应题目700. 二叉搜索树中的搜索 - 力扣(LeetCode)

二叉搜索树的搜索因为其有序性,会比一般二叉树遍历要更高效。

当目标值比当前节点值小,说明在左子树,就往左遍历;当目标值比当前节点值大,说明在右子树,就往右遍历。

这样可以一定程度上剪枝,不用遍历整棵树。

代码如下

class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if (root == null) return root;
        if (root.val == val) return root;
        if (root.val > val) return searchBST(root.left, val);  // 向左
        if (root.val < val) return searchBST(root.right, val);  // 向右
        return null;
    }
}

1.4、二叉搜索树的最小绝对差

对应题目530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)

这道题可以直接中序遍历树,得到的数组是有序的,直接在数组上找最小绝对差。

但最佳的办法是直接在树上进行搜索,思路与在数组上操作是一样的。

每次遍历需要保存前一个节点来进行计算。

保存前一个节点的方法也很简单,直接在中处理逻辑完之后保存当前节点就行。

本题的中处理逻辑是更新最小的绝对差,因为最小绝对差一定是出现在中序遍历时相邻的节点上

if (pre != null) diff = Math.min(diff, root.val - pre.val);
pre = root;  // 保存上一个节点

代码如下

可以保存前一个节点,也可以直接保存前一个节点的数值。

class Solution {
    int diff = Integer.MAX_VALUE;  // 记录最小差
    //int preN = -1;   // 记录上一节点的数值
    TreeNode pre = null;   // 记录上一节点的指针
    public int getMinimumDifference(TreeNode root) {
        traversal(root);
        return diff;
    }

    public void traversal(TreeNode root) {
        if (root == null) return;
        // 中序遍历
        traversal(root.left);
        // if (preN != -1 && root.val - preN < diff) diff = root.val - preN; 
        // preN = root.val;
        if (pre != null) diff = Math.min(diff, root.val - pre.val);
        pre = root;
        traversal(root.right);
    }
}

1.5、二叉搜索树的众数

对应题目501. 二叉搜索树中的众数 - 力扣(LeetCode)

这道题,有一个比较直接的做法,就是用一个map保存各节点数值的出现次数,然后根据频次排序,最后取首个或多个数。

代码如下

class Solution {
    HashMap<Integer, Integer> map = new HashMap();  // 保存频次
    public int[] findMode(TreeNode root) {
        traversal(root);  // 记录各数的频率
        Set<Map.Entry<Integer,Integer>> entries = map.entrySet();
        // 使用优先队列排序
        PriorityQueue<Map.Entry<Integer, Integer>> que = new PriorityQueue<>((o1, o2) -> o2.getValue() - o1.getValue());
        for (Map.Entry<Integer, Integer> e : entries) {
            que.add(e);
        }
        ArrayList<Integer> list = new ArrayList(); // 保存结果
        Map.Entry<Integer, Integer> e1 = que.remove();   // 最多的那个
        list.add(e1.getKey());
        // 看看后面还有没有次数一样多的
        while (!que.isEmpty() && que.peek().getValue() == e1.getValue()) { 
            list.add(que.remove().getKey());
        }
        int[] res = new int[list.size()];
        for (int i = 0; i < list.size(); i++) res[i] = list.get(i);
        return res;
    }

    public void traversal(TreeNode root) {
        if (root == null) return;
        traversal(root.left);
        map.put(root.val, map.getOrDefault(root.val, 0) + 1);
        traversal(root.right);
    }
}

但也可以直接在遍历树的时候进行统计。

因为重复的数字只会出现在中序遍历时的相邻节点上,因此使用中序遍历,保存一个全局最大频率maxCount,并且在遍历时不断计数count,同时需要保存前一个节点的指针pre。

一旦当前节点值与上一个节点的值相同,进行计数count++;否则就重置为1,意味着进入下一个数的计数。

那么什么时候更新maxCount?当count > maxCount时,意味着要更新maxCount了,同时要清空结果集,加入当前的节点数值,因为前面添加过的众数已经作废。

如果有多个数的频次重复,即count = maxCount,那么就把当前节点数值加入结果集里。

代码如下

class Solution {
    int count = 0;
    int maxCount = 0;
    TreeNode pre = null;
    ArrayList<Integer> resList = new ArrayList();
    public int[] findMode(TreeNode root) {
        traversal(root);
        // 转成数组
        int[] res = new int[resList.size()];
        for (int i = 0; i < resList.size(); i++) res[i] = resList.get(i);
        return res;
    }

    public void traversal(TreeNode root) {
        if (root == null) return;

        traversal(root.left);
        // 中处理逻辑
        if (pre != null && root.val != pre.val) count = 1;
        else count++;
        // 加入结果集时机
        // 若当前count比maxCount大 或等于maxCount 说明count肯定是最大的 要加入了
        // 同时如果count比maxCount大 说明之前加如结果集的都是作废的 要清空再加入
        // 相等的话就不用清空 继续加
        if (count > maxCount) {
            maxCount = count;
            resList.clear();
            resList.add(root.val);
        }else if (count == maxCount) {
            resList.add(root.val);
        }
        pre = root;   // 记录前一个节点
        traversal(root.right);
    }
}

二、二叉搜索树的修改与构造

2.2、二叉搜索树的插入

对应题目701. 二叉搜索树中的插入操作 - 力扣(LeetCode)

搜索树在插入的时候可能是要对树进行变换的,因为要维持树的有序性。

但是其实完全可以不用变换。

二叉树前序搜索时,会根据数值大小来判断遍历左子树还是右子树以达到剪枝的目的。

那么一旦遇到空节点,直接将新节点插入就可以了。

这样是一定可以保证新的树是有序的,那么就不需要对树进行重构变换了。

代码如下

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);
        else root.left = insertIntoBST(root.left, val);
        return root;
    }
}

2.3、二叉搜索树的删除

对应题目450. 删除二叉搜索树中的节点 - 力扣(LeetCode)

删除相对添加来说就会难一些,因为要考虑多种情况,不同的情况可能是要进行树变换的。

那么当遇到要删除的节点时,想要删除该节点,需要考虑以下三种情况:

1、该节点左右孩子都为空,那么直接删除该节点就行。

2、该节点左右孩子只有一个为空,那么就以不为空的孩子作为根节点返回。

3、该节点左右孩子都不为空,将左孩子作为右孩子的最左边的叶子节点的左节点,将右孩子返回

第三种情况可能有点绕,其实分开看,右孩子的最左边的叶子节点,它一定是以右孩子为根节点的树中,数值最小的,那么将左孩子作为它的左节点,是一定可以保证有序的,因为左孩子小于右孩子为根的所有节点。

那么三种情况分析完了,这道题也差不多可以解出来。

本题使用前序最直观。

代码如下

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) return root;
        if (root.val == key) {   // 找到要删除的节点
            // 三种情况 都为空 其中一个为空 都不为空
            if (root.left == null) return root.right;
            else if (root.right == null) return root.left;
            else {   // 左右孩子都不为空
                TreeNode node = traversal(root.right);  // 获取右孩子最左下的值
                node.left = root.left;   // 把root的左孩子放到右孩子的最左下边
                return root.right;
            }
        }else if (root.val < key) root.right = deleteNode(root.right, key);
        else root.left =  deleteNode(root.left, key);
        return root;
    }

    // 找最最左下角的节点 不一定最底层 一定最左
    public TreeNode traversal(TreeNode node) {
        if (node.left == null) return node;
        return traversal(node.left);
    }
}

2.4、二叉搜索树的修剪

对应题目669. 修剪二叉搜索树 - 力扣(LeetCode)

修剪其实也就是删除节点,可以直接套用LC_450删除节点的思路,不过一定要使用后序自底向上遍历,如果使用前序,删除根节点时会直接返回,导致漏删一些节点。

代码如下

class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        if (root == null) return root;
        // 一定要自底向上 如果要删根节点 可能会漏删
        root.right = trimBST(root.right, low, high);
        root.left =  trimBST(root.left, low, high);
        if (root.val < low || root.val > high) {   // 找到要删除的节点
            // 四种情况 都为空 其中一个为空 都不为空
            if (root.left == null) return root.right;
            else if (root.right == null) return root.left;
            else {   // 左右孩子都不为空
                TreeNode node = traversal(root.right);  // 获取右孩子最左下的值
                node.left = root.left;   // 把root的左孩子放到右孩子的最左下边
                return root.right;
            }
        }
        
        return root;
    }

    // 找最最左下角的节点 不一定最底层 一定最左
    public TreeNode traversal(TreeNode node) {
        if (node.left == null) return node;
        return traversal(node.left);
    }
}

但其实也可以换种思路,因为不是只删除一个节点。

一旦当前节点需要删除,可以直接返回删除后的左孩子或右孩子。

如果root.val > high,那么就向左递归,返回满足条件的左孩子。

如果root.val < low,那么就向右递归,返回满足条件的右孩子。

如果root.val在范围内,那么就递归获取左右子树。

代码如下

class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        if(root == null) return root;
        // 换种思路
        // 如果比low小 则直接往右找 比high大 往左找
        // 这样就相当于删掉当前节点了
        if (root.val < low) return trimBST(root.right, low, high);
        else if (root.val > high) return trimBST(root.left, low, high);
        root.left = trimBST(root.left, low, high);
        root.right = trimBST(root.right, low, high);
        return root;
    }
}

2.5、二叉搜索树的构造

对应题目108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)

可以回顾根据数组构造二叉树的过程,

题目为:106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

 记录的笔记:(116条消息) 代码随想录 二叉树模块小结(1)_散谎的博客-CSDN博客

大致的思路就是找分割点,分割左右数组,再递归创建左右子树。

本大题不需要担心是否平衡的问题,如果从数组中间取索引作为分割点,那么最后的树一定是平衡的。

代码如下

class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        return traversal(nums, 0, nums.length);
    }

    public TreeNode traversal(int[] nums, int start, int end) {
        if (start >= end) return null;
        int index = (end - start) / 2 + start;  // 分割边界  要加上start 因为是找下标 很关键
        TreeNode root = new TreeNode(nums[index]);  // 根节点
        if (end - start == 1) return root;  // 根节点 直接返回

        // 分割左右子数组  左闭右开
        int leftStart = start;
        int leftEnd = index;   // 选过了该节点
        int rightStart = index + 1;
        int rightEnd = end;

        root.left = traversal(nums, leftStart, leftEnd);
        root.right = traversal(nums, rightStart, rightEnd);
        return root;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值