LeetCode算法小抄--二叉搜索树

简介: LeetCode算法小抄--二叉搜索树

二叉搜索树(BST)

二叉查找树:又被称为二叉搜索树(Binary Search Tree,简写 BST)其特点如下

1、对于 BST 的每一个节点 node,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。

2、对于 BST 的每一个节点 node,它的左侧子树和右侧子树都是 BST。

3、”中序遍历“可以让结点有序(升序)

直接基于 BST 的数据结构有 AVL 树,红黑树等等,拥有了自平衡性质,可以提供 logN 级别的增删查改效率;还有 B+ 树,线段树等结构都是基于 BST 的思想来设计的

寻找第 K 小的元素

230. 二叉搜索树中第K小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

一个直接的思路就是升序排序,然后找第 k 个元素呗。BST 的中序遍历其实就是升序排序的结果,找第 k 个元素肯定不是什么难事。

class Solution {
    public int kthSmallest(TreeNode root, int k) {
        // 利用 BST 的中序遍历特性
        traverse(root, k);
        return res;
    }
    // 记录结果
    int res = 0;
    // 记录当前元素的排名
    int rank = 0;
    private void traverse(TreeNode root, int k){
        if(root == null) return;
        traverse(root.left, k);
        /* 中序遍历代码位置 */
        rank++;
        if(k == rank){
            // 找到第 k 小的元素
            res = root.val;
            return;
        }
        traverse(root.right, k);
    }
}

二叉搜索树的转化

538. 把二叉搜索树转换为累加树

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

提醒一下,二叉搜索树满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。
  • 节点的右子树仅包含键 大于 节点键的节点。
  • 左右子树也必须是二叉搜索树。
  • 对于一个节点来说,确实右子树都是比它大的元素,但问题是它的父节点也可能是比它大的元素呀?这个没法确定的,我们又没有触达父节点的指针,所以二叉树的通用思路在这里用不了。

其实,正确的解法很简单,还是利用 BST 的中序遍历特性

如果我想降序打印节点的值怎么办?

很简单,只要把递归顺序改一下就行了:

void traverse(TreeNode root) {
    if (root == null) return;
    // 先递归遍历右子树
    traverse(root.right);
    // 中序遍历代码位置
    print(root.val);
    // 后递归遍历左子树
    traverse(root.left);
}

这段代码可以降序打印 BST 节点的值,如果维护一个外部累加变量 sum,然后把 sum 赋值给 BST 中的每一个节点,不就将 BST 转化成累加树了吗

class Solution {
    public TreeNode bstToGst(TreeNode root) {
        traverse(root);
        return root;
    }
    // 记录累加和
    int sum = 0;
    private void traverse(TreeNode root) {
        if (root == null) {
            return;
        }
        traverse(root.right);
        // 维护累加和
        sum += root.val;
        // 将 BST 转化成累加树
        root.val = sum;
        traverse(root.left);
    }
}

这道题就解决了,核心还是 BST 的中序遍历特性,只不过我们修改了递归顺序,降序遍历 BST 的元素值,从而契合题目累加树的要求。

1038. 从二叉搜索树到更大和树

给定一个二叉搜索树 root (BST),请将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。

提醒一下, 二叉搜索树 满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。
  • 节点的右子树仅包含键 大于 节点键的节点。
  • 左右子树也必须是二叉搜索树。
class Solution {
    public TreeNode bstToGst(TreeNode root) {
        traverse(root);
        return root;
    }
    // 记录累加和
    int sum = 0;
    private void traverse(TreeNode root) {
        if (root == null) {
            return;
        }
        traverse(root.right);
        // 维护累加和
        sum += root.val;
        // 将 BST 转化成累加树
        root.val = sum;
        traverse(root.left);
    }
}

判断 BST 的合法性

98. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。
boolean isValidBST(TreeNode root) {
    if (root == null) return true;
    // root 的左边应该更小
    if (root.left != null && root.left.val >= root.val)
        return false;
    // root 的右边应该更大
    if (root.right != null && root.right.val <= root.val)
        return false;
    return isValidBST(root.left)
        && isValidBST(root.right);
}

坑:

这个算法出现了错误,BST 的每个节点应该要小于右边子树的所有节点,下面这个二叉树显然不是 BST,因为节点 10 的右子树中有一个节点 6,但是我们的算法会把它判定为合法 BST:


9b8f936bd40446e38a5c4f65427365d1.png


出现问题的原因在于,对于每一个节点 root,代码值检查了它的左右孩子节点是否符合左小右大的原则;但是根据 BST 的定义,root 的整个左子树都要小于 root.val,整个右子树都要大于 root.val

问题是,对于某一个节点 root,他只能管得了自己的左右子节点,怎么把 root 的约束传递给左右子树呢?

class Solution {
    public boolean isValidBST(TreeNode root) {
        return isValidBST(root, null, null);
    }
    /* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */
    private boolean isValidBST(TreeNode root, TreeNode min, TreeNode max){
        // base case
        if(root == null) return true;
        // 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
        if(min != null && root.val < min.val) return false;
        if(max != null && root.val > max.val) return false;
        // 限定左子树的最大值是 root.val,右子树的最小值是 root.val
        return isValidBST(root.left, min, root) && isValidBST(root.right, root, max);
    }
}

通过使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点,这也是二叉树算法的一个小技巧吧。

在 BST 中搜索元素

700. 二叉搜索树中的搜索

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null

class Solution {
    //在一棵普通的二叉树中寻找,可以这样写
    public TreeNode searchBST(TreeNode root, int val) {
        if(root == null) return null;
        if(root.val == val) return root;
        // 当前节点没找到就递归地去左右子树寻找
        TreeNode left = searchBST(root.left, val);
        TreeNode right = searchBST(root.right, val);
        return left != null? left : right;
    }
}

这样写完全正确,但这段代码相当于穷举了所有节点,适用于所有二叉树。那么应该如何充分利用 BST 的特殊性,把「左小右大」的特性用上?

很简单,其实不需要递归地搜索两边,类似二分查找思想,根据 targetroot.val 的大小比较,就能排除一边。我们把上面的思路稍稍改动

class Solution {
    //在一棵普通的二叉树中寻找,可以这样写代码
    public TreeNode searchBST(TreeNode root, int val) {
        if(root == null) return null;
        if(root.val == val) return root;
        if(val < root.val){
           return searchBST(root.left, val);
        }
        if(val > root.val){
           return searchBST(root.right, val);
        }
        return root;
    }
}

在 BST 中插入一个数

对数据结构的操作无非遍历 + 访问,遍历就是「找」,访问就是「改」一旦涉及「改」,就类似二叉树的构造问题,函数要返回 TreeNode 类型,并且要对递归调用的返回值进行接收

TreeNode insertIntoBST(TreeNode root, int val) {
    // 找到空位置插入新节点
    if (root == null) return new TreeNode(val);
    // if (root.val == val)
    //     BST 中一般不会插入已存在元素
    if (root.val < val) 
        root.right = insertIntoBST(root.right, val);
    if (root.val > val) 
        root.left = insertIntoBST(root.left, val);
    return root;
}

701. 二叉搜索树中的插入操作

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        // 找到空位置插入新节点
        if(root == null) return new TreeNode(val);
        if(val > root.val) root.right = insertIntoBST(root.right, val);
        if(val < root.val) root.left = insertIntoBST(root.left, val);
        return root;
    }
}

在 BST 中删除一个数

跟插入操作类似,先「找」再「改」

TreeNode deleteNode(TreeNode root, int key) {
    if (root.val == key) {
        // 找到啦,进行删除
    } else if (root.val > key) {
        // 去左子树找
        root.left = deleteNode(root.left, key);
    } else if (root.val < key) {
        // 去右子树找
        root.right = deleteNode(root.right, key);
    }
    return root;
}

找到目标节点了,比方说是节点 A,如何删除这个节点,这是难点。因为删除节点的同时不能破坏 BST 的性质。有三种情况

情况 1A 恰好是末端节点,两个子节点都为空,就直接删除。


445ddf8c1c264c70beef0994cc727efc.png


if (root.left == null && root.right == null)
    return null;

情况 2A 只有一个非空子节点,那么它要让这个孩子接替自己的位置



0f91b53646ed4f2bbacc5f62ca8a6d31.png

// 排除了情况 1 之后
if (root.left == null) return root.right;
if (root.right == null) return root.left;

情况 3A 有两个子节点,麻烦了,为了不破坏 BST 的性质,A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。我们以第二种方式讲解。

ea3f8c0533074ceea366619c42842ab1.png


if (root.left != null && root.right != null) {
    // 找到右子树的最小节点
    TreeNode minNode = getMin(root.right);
    // 把 root 改成 minNode
    root.val = minNode.val;
    // 转而去删除 minNode
    root.right = deleteNode(root.right, minNode.val);
}

完整版

TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;
    if (root.val == key) {
        // 这两个 if 把情况 1 和 2 都正确处理了
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        // 处理情况 3
        // 获得右子树最小的节点
        TreeNode minNode = getMin(root.right);
        // 删除右子树最小的节点
        root.right = deleteNode(root.right, minNode.val);
        // 用右子树最小的节点替换 root 节点
        minNode.left = root.left;
        minNode.right = root.right;
        root = minNode;
    } else if (root.val > key) {
        root.left = deleteNode(root.left, key);
    } else if (root.val < key) {
        root.right = deleteNode(root.right, key);
    }
    return root;
}
TreeNode getMin(TreeNode node) {
    // BST 最左边的就是最小的
    while (node.left != null) node = node.left;
    return node;
}

注意一下,上述代码在处理情况 3 时通过一系列略微复杂的链表操作交换 rootminNode 两个节点:

// 处理情况 3
// 获得右子树最小的节点
TreeNode minNode = getMin(root.right);
// 删除右子树最小的节点
root.right = deleteNode(root.right, minNode.val);
// 用右子树最小的节点替换 root 节点
minNode.left = root.left;
minNode.right = root.right;
root = minNode;

替换 root 节点为什么这么麻烦,直接改 val 字段不就行了?看起来还更简洁易懂

// 处理情况 3
// 获得右子树最小的节点
TreeNode minNode = getMin(root.right);
// 删除右子树最小的节点
root.right = deleteNode(root.right, minNode.val);
// 用右子树最小的节点替换 root 节点
root.val = minNode.val;

仅对于这道算法题来说是可以的,但这样操作并不完美,我们一般不会通过修改节点内部的值来交换节点。因为在实际应用中,BST 节点内部的数据域是用户自定义的,可以非常复杂,而 BST 作为数据结构(一个工具人),其操作应该和内部存储的数据域解耦,所以我们更倾向于使用指针操作来交换节点,根本没必要关心内部数据


450. 删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。


一般来说,删除节点可分为两个步骤:


首先找到需要删除的节点;

如果找到了,删除它。

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if(root == null) return null;
        if(root.val == key){
            if(root.left == null) return root.right;
            if(root.right == null) return root.left;
            // 获得右子树最小的节点
            TreeNode minNode = getMin(root.right);
            // 删除右子树最小的节点
            root.right = deleteNode(root.right, minNode.val);
            // 用右子树最小的节点替换 root 节点
            root.val = minNode.val;
        } else if(key > root.val){
            root.right = deleteNode(root.right, key);
        }else if(key < root.val){
            root.left = deleteNode(root.left, key);
        }
        return root;
    }
    private TreeNode getMin(TreeNode node){
        // BST 最左边的就是最小的
        while(node.left != null){
            node = node.left;
        }
        return node;
    }
}

构造BST

96. 不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

// 超出时间限制
class Solution {
    public int numTrees(int n) {
        return count(1, n);
    }
    /* 计算闭区间 [lo, hi] 组成的 BST 个数 */
    private int count(int lo, int hi) {
        // base case
        if (lo > hi) return 1;
        int res = 0;
        for (int i = lo; i <= hi; i++) {
            // i 的值作为根节点 root
            int left = count(lo, i - 1);
            int right = count(i + 1, hi);
            // 左右子树的组合数乘积是 BST 的总数
            res += left * right;
        }
        return res;
    }    
}
// 优化算法,备忘录模式
class Solution {
    // 备忘录
    int[][] memo;
    public int numTrees(int n) {
        memo = new int[n+1][n+1];
        return count(1, n);
    }
    private int count(int lo, int hi) {
        if(lo > hi) return 1;
        // 查备忘录
        if(memo[lo][hi] != 0){
            return memo[lo][hi];
        }
        int res = 0;
        for(int mid = lo; mid <= hi; mid++) {
            int left = count(lo, mid-1);
            int right = count(mid+1, hi);
            res += left * right;
        }
        // 将结果存入备忘录
        memo[lo][hi] = res;        
        return res;
    }
}

95. 不同的二叉搜索树 II

给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。



30b76acc7ec34b7fa71624e9f6d0b6f1.png

class Solution {
    public List<TreeNode> generateTrees(int n) {
        if(n==0) return new LinkedList<>();
        // 构造闭区间 [1, n] 组成的 BST 
        return build(1,n);
    }
    /* 构造闭区间 [lo, hi] 组成的 BST */
    private List<TreeNode> build(int lo, int hi){
        List<TreeNode> res = new LinkedList<>();
        // base case
        if(lo > hi){
            res.add(null);
            return res;
        }
        // 1、穷举 root 节点的所有可能
        for (int i = lo; i <= hi; i++) {
            // 2、递归构造出左右子树的所有合法 BST
            List<TreeNode> leftTree = build(lo, i - 1);
            List<TreeNode> rightTree = build(i + 1, hi);
            // 3、给 root 节点穷举所有左右子树的组合
            for(TreeNode left : leftTree){
                for(TreeNode right : rightTree){
                    // i 作为根节点 root 的值
                    TreeNode root = new TreeNode(i);
                    root.left = left;
                    root.right = right;
                    res.add(root);
                }
            }
        }
        return res;
    }
}


相关文章
|
3天前
|
存储 算法
Leetcode 30天高效刷数据结构和算法 Day1 两数之和 —— 无序数组
给定一个无序整数数组和目标值,找出数组中和为目标值的两个数的下标。要求不重复且可按任意顺序返回。示例:输入nums = [2,7,11,15], target = 9,输出[0,1]。暴力解法时间复杂度O(n²),优化解法利用哈希表实现,时间复杂度O(n)。
16 0
|
20天前
|
算法
代码随想录算法训练营第六十天 | LeetCode 84. 柱状图中最大的矩形
代码随想录算法训练营第六十天 | LeetCode 84. 柱状图中最大的矩形
20 3
|
20天前
|
存储 算法
代码随想录算法训练营第五十九天 | LeetCode 739. 每日温度、496. 下一个更大元素 I
代码随想录算法训练营第五十九天 | LeetCode 739. 每日温度、496. 下一个更大元素 I
22 1
|
20天前
|
算法
代码随想录算法训练营第五十七天 | LeetCode 739. 每日温度、496. 下一个更大元素 I
代码随想录算法训练营第五十七天 | LeetCode 739. 每日温度、496. 下一个更大元素 I
17 3
|
20天前
|
算法
代码随想录算法训练营第五十六天 | LeetCode 647. 回文子串、516. 最长回文子序列、动态规划总结
代码随想录算法训练营第五十六天 | LeetCode 647. 回文子串、516. 最长回文子序列、动态规划总结
34 1
|
20天前
|
算法
代码随想录算法训练营第五十五天 | LeetCode 583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结
代码随想录算法训练营第五十五天 | LeetCode 583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结
24 1
|
22天前
|
算法 API DataX
二叉树(下)+Leetcode每日一题——“数据结构与算法”“对称二叉树”“另一棵树的子树”“二叉树的前中后序遍历”
二叉树(下)+Leetcode每日一题——“数据结构与算法”“对称二叉树”“另一棵树的子树”“二叉树的前中后序遍历”
|
1天前
|
算法
【免费】基于ADMM算法的多微网电能交互分布式运行策略(matlab代码)
【免费】基于ADMM算法的多微网电能交互分布式运行策略(matlab代码)
|
1天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于有序抖动块截断编码的水印嵌入和提取算法matlab仿真
这是一个关于数字图像水印嵌入的算法介绍。使用MATLAB2022a,该算法基于DOTC,结合抖动和量化误差隐藏,确保水印的鲁棒性和隐蔽性。图像被分为N*N块,根据水印信号进行二值化处理,通过调整重建电平的奇偶性嵌入水印。水印提取是嵌入过程的逆操作,通过重建电平恢复隐藏的水印比特。提供的代码片段展示了从块处理、水印嵌入到噪声攻击模拟及水印提取的过程,还包括PSNR和NC的计算,用于评估水印在不同噪声水平下的性能。
|
1天前
|
机器学习/深度学习 算法 数据挖掘
基于改进ISODATA算法的负荷场景曲线聚类(matlab代码)
基于改进ISODATA算法的负荷场景曲线聚类(matlab代码)

热门文章

最新文章