【二叉树3--二叉搜索树】BST基本操作+相应题目+扩展

本文介绍了二叉搜索树(BST)的基础概念,包括其定义、性质和操作。重点讲述了如何检查BST的合法性,搜索、插入和删除节点的算法,并探讨了在BST中寻找第k小元素的高效方法。同时,文章通过实例解析了删除节点的三种情况,并讨论了如何在BST中恢复次序错误。最后,提供了修剪BST以保持特定范围的节点的解决方案。
摘要由CSDN通过智能技术生成

前言/目录

前文已经写了【二叉树基本框架&常规题目】+ 【多种方式序列化和反序列化二叉树方法】
本篇文章将记录和写一下二叉树中常考的二叉搜索树的基础、题目和扩展题目(101)。

定义及操作集锦

首先,BST 的特性大家应该都很熟悉了:

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

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

二叉搜索树并不算复杂,但我觉得它构建起了数据结构领域的半壁江山,直接基于 BST 的数据结构有 AVL 树,红黑树等等,拥有了自平衡性质,可以提供 logN 级别的增删查改效率;还有 B+ 树,线段树等结构都是基于 BST 的思想来设计的。

关于BST的构建合法性检查、搜索或查找一个数是否存在、BST插入一个数、BST删除一个数
【构建、改查、增、删】


struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};


/**
 * @Description: 检测BST的构造是否合法
 * @param {TreeNode} *root
 * @return {*}
 * @notes: 
 */
boolean isValidBST(TreeNode* root){
    return isValidBST(root, nullptr, nullptr);
}
boolean isValidBST(TreeNode* root,TreeNode* min, TreeNode* max){
    // 辅助函数 —— 检测是否构成合法的BST
    if(root == nullptr) return true;
        // 注意当前节点要和所有的左右节点比较!
    if(min != nullptr && root->val <= min->val) return false;
    if(max != nullptr && root->val >= max->val) return false;

    // 其它交给递归
    return isValidBST(root->left, min, root) &&
            isValidBST(root->right, root, max);  // 相当于在后面给所有节点添加了一个 min 和 max的边界约束。
                                                 // 约束了root 的左子树节点值不超过root的值、右子树节点值不小于root的值。
}

/**
 * @Description: 搜索、改查
 * @param {*}
 * @return {*}
 * @notes: 
 */
boolean isInBST(TreeNode* root, int target){
    // 当前节点该做的事
    if(root == nullptr) return false;
    if(root->val == target) return true;

    // 递归框架.
    if(target > root->val) return isInBST(root->right, target);
    if(target < root->val) return isInBST(root->left, target);
}

/**
 * @Description: 增(插入)
 * @param {*}
 * @return {*}
 * @notes: 
 */
TreeNode* insertIntoBST(TreeNode* root, int val){
    // 新增节点,所以要返回构建的节点
    if(root == nullptr) return new TreeNode(val);
    
    if(val == root->val) return root; // 重复了不添加新的
    else if(val > root->val){
        root->right = insertIntoBST(root->right, val);
    }else if(val < root->val){
        root->left = insertIntoBST(root->left, val);
    }
    return  root;
}


/**
 * @Description: 最复杂和难的  
 * @param {*} 注意关键:按照有几个子节点进行分类和 构建伪代码框架——分为三类; 且注意:两个节点要找到最小的进行互换之后、删除最小的节点即可。
 * @return {*}
 * @notes: 
 */
TreeNode* deleteNode(TreeNode* root, int key){
    // 首先搜索,找到了分情况删除; 未找到递归查找。
    if(root == nullptr ) return nullptr;
    if(root->val == key){
        // 分情况进行删除结点;三种情况。
            // 一
        if(root->left == nullptr && root->right == nullptr) return nullptr;
            // 二 (可以和上一种合起来
        if(root->left == nullptr) return root->right;
        if(root->right == nullptr) return root->left;
            // 第三种情况:
            // 先找到最小的节点赋值, 然后递归删除最后的节点即可。
        TreeNode* cur_min = getMin(root->right); // 这里决定了选择右边最小的节点进行交换
                // 声明这样做不正规,但是正确;(本应该是在指针级别 进行操作的;因为不同结构体内的成员不同
        root->val = cur_min->val;
        // return deleteNode(root->right, cur_min->val);  // 错了
        root->right = deleteNode(root->right, cur_min->val); //【这样才对,否则 就覆盖了root了!】

    }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* root){
    // 辅助函数——辅助第三种情况交换节点 并删除
    // 要么右边最小节点; 要么左边最大节点
    // 这里选择右边最小节点 [只能上一层 主函数进行选择]
    while(root->left != nullptr) root = root->left;
    return root;
}

性质

从做算法题的角度来看 BST,除了它的定义,还有一个重要的性质:BST 的中序遍历结果是有序的(升序)。

也就是说,如果输入一棵 BST,以下代码可以将 BST 中每个节点的值升序打印出来:

void traverse(TreeNode root) {
    if (root == null) return;
    traverse(root.left);
    // 中序遍历代码位置
    print(root.val);
    traverse(root.right);
}

但是注意区分——普通搜索遍历 和 构建Tree的遍历:

/**
 * @Description: 搜索、改查
 * @param {*}
 * @return {*}
 * @notes: 
 */
boolean isInBST(TreeNode* root, int target){
    // 当前节点该做的事
    if(root == nullptr) return false;
    if(root->val == target) return true;

    // 递归框架.
    if(target > root->val) return isInBST(root->right, target);
    if(target < root->val) return isInBST(root->left, target);
}

/**
 * @Description: 增(插入)
 * @param {*}
 * @return {*}
 * @notes: 
 */
TreeNode* insertIntoBST(TreeNode* root, int val){
    // 新增节点,所以要返回构建的节点
    if(root == nullptr) return new TreeNode(val);
    
    if(val == root->val) return root; // 重复了不添加新的
    else if(val > root->val){
        root->right = insertIntoBST(root->right, val);
    }else if(val < root->val){
        root->left = insertIntoBST(root->left, val);
    }
    return  root;
}

刷题1 LeetCode230 & 538(1038)

描述

那么根据这个性质,我们来做两道算法题。(比较简单不再赘述,只谈一些【提升之处】)
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

T230 BST寻找第k小的值中:

这道题就做完了,不过呢,还是要多说几句,因为这个解法并不是最高效的解法,而是仅仅适用于这道题。

dong’s 旧文 高效计算数据流的中位数 中就提过今天的这个问题:

如果让你实现一个在二叉搜索树中通过排名计算对应元素的方法select(int k),你会怎么设计?

如果按照我们刚才说的方法,利用「BST 中序遍历就是升序排序结果」这个性质,每次寻找第k小的元素都要中序遍历一次,最坏的时间复杂度是O(N),N是 BST 的节点个数。

要知道 BST 性质是非常牛逼的,像红黑树这种改良的自平衡 BST,增删查改都是O(logN)的复杂度,让你算一个第k小元素,时间复杂度竟然要O(N),有点低效了。

所以说,计算第k小元素,最好的算法肯定也是对数级别的复杂度,不过这个依赖于 BST 节点记录的信息有多少。

我们想一下 BST 的操作为什么这么高效?就拿搜索某一个元素来说,BST 能够在对数时间找到该元素的根本原因还是在 BST 的定义里,左子树小右子树大嘛,所以每个节点都可以通过对比自身的值判断去左子树还是右子树搜索目标值,从而避免了全树遍历,达到对数级复杂度。

那么回到这个问题,想找到第k小的元素,或者说找到排名为k的元素,如果想达到对数级复杂度,关键也在于每个节点得知道他自己排第几。

比如说你让我查找排名为k的元素,当前节点知道自己排名第m,那么我可以比较m和k的大小:

1、如果m == k,显然就是找到了第k个元素,返回当前节点就行了。

2、如果k < m,那说明排名第k的元素在左子树,所以可以去左子树搜索第k个元素。

3、如果k > m,那说明排名第k的元素在右子树,所以可以去右子树搜索第k - m - 1个元素。

这样就可以将时间复杂度降到O(logN)了。

那么,如何让每一个节点知道自己的排名呢?

这就是我们之前说的,需要在二叉树节点中维护额外信息。每个节点需要记录,以自己为根的这棵二叉树有多少个节点。

也就是说,我们TreeNode中的字段应该如下:

class TreeNode {
    int val;
    // 以该节点为根的树的节点总数
    int size;
    TreeNode left;
    TreeNode right;
}

有了size字段,外加 BST 节点左小右大的性质,对于每个节点node就可以通过node.left推导出node的排名,从而做到我们刚才说到的对数级算法。

当然,size字段需要在增删元素的时候需要被正确维护,力扣提供的TreeNode是没有size这个字段的,所以我们这道题就只能利用 BST 中序遍历的特性实现了,但是我们上面说到的优化思路是 BST 的常见操作,还是有必要理解的。


刷题2(450\701\700\98)

450.删除二叉搜索树中的节点(Medium)
701.二叉搜索树中的插入操作(Medium)
700.二叉搜索树中的搜索(Easy)
98.验证二叉搜索树(Medium)

参见开头的 基础操作集锦。【重点关注- 构建(98) + 删除(450)】

构建合法性

一、判断 BST 的合法性
这里是有坑的哦,我们按照刚才的思路,每个节点自己要做的事不就是比较自己和左右孩子吗?看起来应该这样写代码:

boolean isValidBST(TreeNode root) {
    if (root == null) return true;
    if (root.left != null && root.val <= root.left.val)
        return false;
    if (root.right != null && root.val >= root.right.val)
        return false;

    return isValidBST(root.left)
        && isValidBST(root.right);
}

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

在这里插入图片描述

出现问题的原因在于,对于每一个节点root,代码值检查了它的左右孩子节点是否符合左小右大的原则;但是根据 BST 的定义,root的整个左子树都要小于root.val,整个右子树都要大于root.val。【注意这里 强调左右子树反过来对 root;即: root 对左右子树整体的 影响—— 所以需要整体上可以添加 参数传递。】

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

请看正确的代码:


class Solution {
public:
    /**
     * @Description: 总的思路:使得中间的值大于所有左边 小于左右右边。
     * @param {*} 新函数--有最小节点和最大节点 ; 
     * @return {*}
     * @notes: 
     */
    bool isValidBST(TreeNode* root) {
        // if(root == nullptr) return true;
        return isValidBST(root, nullptr, nullptr);
    }
    // 辅助函数判断是否 大于和小于
    /**
     * @Description: 变相后序:使得左子树限定在不大于root,  右子树限定在不小于root
     * @param {*}  即:左子树 小于root<max>, 右子树 大于root<min>;
     * @return {*}
     * @notes: 
     */
    bool isValidBST(TreeNode* root, TreeNode* min, TreeNode* max){
        if(root == nullptr ) return true;
        // 三种--- 使得当前节点-不超过右子树min[就是root节点啊!]  大于左子树max[就是root节点啊]
        if(min!=nullptr && root->val <= min->val) return false;
        if(max!=nullptr && root->val >= max->val) return false;

        return isValidBST(root->left, min, root) 
                && isValidBST(root->right, root, max);

    }
    /**
     * @Description: 站在了 中间节点角度,上边是站在了 左右子树角度。
     * @param {*}
     * @return {*}
     * @notes: 
     */
    bool isValidBST(TreeNode* root, TreeNode* min, TreeNode* max){
        if(root == nullptr) return true;
        // 左搜---右边最小的
        bool left = isValidBST(root->left, min, root);
        bool right = isValidBST(root->right, root, max);

        if(max!=nullptr && root->val >= max->val) return false;
        if(min!=nullptr && root->val <= min->val) return false;
        return left && right;
};

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

删除

这个问题稍微复杂,跟插入操作类似,先「找」再「改」,先把框架写出来再说:

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 的性质。有三种情况,用图片来说明。

情况 1:A恰好是末端节点,两个子节点都为空,那么它可以当场去世了。

在这里插入图片描述

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

情况 2:A只有一个非空子节点,那么它要让这个孩子接替自己的位置。
在这里插入图片描述

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

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

在这里插入图片描述

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.val = minNode.val;
        root.right = deleteNode(root.right, minNode.val);
    } 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;
} 

删除操作就完成了。注意一下,这个删除操作并不完美,因为我们一般不会通过root.val = minNode.val修改节点内部的值来交换节点,而是通过一系列略微复杂的链表操作交换root和minNode两个节点。

因为具体应用中,val域可能会是一个复杂的数据结构,修改起来非常麻烦;而链表操作无非改一改指针,而不会去碰内部数据。

不过这里我们暂时忽略这个细节,旨在突出 BST 基本操作的共性,以及借助框架逐层细化问题的思维方式。
我的解法:

class Solution {
public:
    /**
     * @Description: 删除节点三种情况:①叶子节点直接删除 ②单独节点删除并返回有的 一个节点 ③中间节点找到最小值 替换并递归删除
     * @param {*}
     * @return {*}
     * @notes: 
     */
    TreeNode* deleteNode(TreeNode* root, int key) {
        if(root == nullptr) return nullptr;

        // 如果是当前值 —— 再分三种情况。
        if(root->val == key) {
            // 1
            if(root->left==nullptr && root->right == nullptr) return nullptr;
            // 2
            if(root->left == nullptr) return root->right; // 上述简化了,1&2 可共用,删除1 ;也可以!
            if(root->right == nullptr) return root->left;
            // 3 ; 找到右边最小的节点替换,并递归删除叶子结点
            int tmp = root->val;
            root->val = getMin(root->right, tmp);

            root->right = deleteNode(root->right, tmp);
        }
        if(key < root->val) root->left = deleteNode(root->left, key);
        else if(key > root->val) root->right = deleteNode(root->right, key);

        return root;
    }
    // 辅助函数寻找当前bst的最小值,最左边!
    int getMin(TreeNode* root, int val){
        while(root->left != nullptr) root = root->left;
        int tmp = root->val;
        root->val = val;
        return tmp;
    }
};

【必】(区别构建)拓展题目–递归&指针引用 拓展

题目:
在这里插入图片描述

我的题解:


class Solution
{
public:
    /**
     * @Description: 找到并 交换恢复BST;且 只有一处错误
     * @param {*}
     * @return {*}
     * @notes: 
     */
    void recoverTree(TreeNode *root)
    {
        TreeNode *max= nullptr;
        TreeNode *min= nullptr;
        TreeNode *pre= nullptr;
        bool found  = false;
        findRecover(root, pre, max, min, found);
        cout << max->val << endl;
        cout << min->val << endl;
        swap(max->val, min->val);
    }
    /**
     * @Description: 
     * @param {bool} isFound
     * @return {*}
     * @notes: 
     */
    void findRecover(TreeNode *root, TreeNode* &pre, TreeNode* &max, TreeNode* &min, bool &isFound)
    {
        if (root == nullptr)
            return;

        findRecover(root->left, pre, max, min, isFound);
        cout << "isFound is :" << isFound << endl;
        
        // 中序遍历
        if (pre!=nullptr && !isFound && root->val <= pre->val)
        {
            isFound = true; // 发现过一次错乱。
            max = pre;
            min = root;
            // pre = root;
            // return ;
            cout << max -> val << endl;
            cout << min->val << endl;
        }
        else if (pre!=nullptr && isFound && root->val <= pre->val)
        { // 第二次发现错乱
            min = root;
            cout << max -> val << "hahah"<<endl;
            cout << min->val << endl;
        }
        pre = root;

        findRecover(root->right, pre, max, min, isFound);
    }
};

关键之处:

  1. 首先知晓要用到 中序遍历递增的特点;
  2. 设置一个prev指针记录当前操作节点的前一个节点,如果当前节点 小于前一个节点,则需要调整并记录【注意:是全局的记录!】
  3. 小技巧:如果遍历过程中只出现了一次 次序错误,这说明是这两个相邻接点需要被交换;否则出现了两次 次序错误,那就需要继续记录单个 (TreeNode*) min 并最终在主函数 交换这两个节点。

【必】(区别删除)拓展题目–剪枝 好好利用性质!和递归!而不是一味删除!!!

题目:
在这里插入图片描述
我的题解:

class Solution {
public:
    /**
     * @Description: 用性质做!!!
     * @param {*}
     * @return {*}
     * @notes: 
     */
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if(root == nullptr) return nullptr;
        // 前序!
        if(root->val > high)  return trimBST(root->left, low, high);
        if(root->val < low) return trimBST(root->right, low, high);
        
        root->left = trimBST(root->left, low, high);
        root->right = trimBST(root->right, low, high);
        return root;
    }

    TreeNode* trimBST1(TreeNode* root, int low, int high) {
        if(root == nullptr) return nullptr;
        root->left = trimBST(root->left, low, high);
        // 中序
        if(root->val < low) {
            // 三种情况删除当前节点
            return trimBST(root->right, low, high);
        }else if(root->val > high){
            return trimBST(root->left, low, high);
        }
        root->right = trimBST(root->right, low, high);
        return root;
    }
};


关键之处:
理解并明白 使用性质!—— 利用二叉查找树的大小关系,我们可以很容易地利用递归进行树的处理。

总结

通过这篇文章,我们总结出了如下几个技巧:

1、二叉树算法设计的总路线:把当前节点要做的事做好,其它的抛给递归框架–返回值我操心——不用当前的节点操心。

2、如果当前节点会对下面的子节点有整体影响,可以通过辅助函数增长参数列表,借助参数传递信息。

2.1 分为上述`BST合法性 非全局引用的使用` 是从上到下的;属于 **递归框架操心**2.2 分为上述`99题恢复BST` —— 是全局引用不随递归改变;**也属于我操心**

3、在二叉树递归框架之上,扩展出一套 BST 代码框架

void BST(TreeNode root, int target) {
    if (root.val == target)
        // 找到目标,做点什么
    if (root.val < target) 
        BST(root.right, target);
    if (root.val > target)
        BST(root.left, target);
}

4、学会了——构建、改查、增、删。【即:合法性、搜索、插入、删除。】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值