【数据结构】【学习笔记】二叉搜索树

我们可以把二叉查找树当作是二分查找算法思想的树形结构体现(二分查找复习)。
在查找数据或寻找适合添加数据的位置时,只要将其和现有的数据比较大小,就可以根据比较结果得知该往哪边移动了。

⚙️定义

二叉搜索树有两个性质:

  1. 每个结点的值均大于其左子树上任意一个结点的值;
  2. 每个结点的值均小于其右子树上任意一个结点的值。

🔎特性

比较的次数取决于树的高度。所以如果结点数为n,而且树的形状又较为均衡的话,比较大小和移动的次数最多就是 \(\log_2 n\) 。因此,时间复杂度为 \(O(\log n)\)

但是,如果树的形状朝单侧纵向延伸,树就会变得很高,此时时间复杂度也就变成了 \(O(n)\)

退化场景:连续顺序插入 [1, 2, 3, 4, 5] 时,BST 会退化为倾斜链表。
演进方向:为解决这一瓶颈,后续演进出了 AVL 树(严格平衡)与 红黑树(近似平衡,C++ std::map / C# SortedDictionary 的底层原理)。

中序遍历(In-order Traversal)结果必定严格递增。

这是做算法题(如验证 BST、求 BST 第 \(k\) 小元素、转换为双向链表等)时频次最高的考点。

🤓实现

添加数据

步骤如下

先从二叉搜索树的顶端结点开始寻找添加数字的位置,将想要添加的数与该结点中的值进行比较:
- 若添加数小于结点值,则左移;
- 若添加数大于结点值,则右移;

删除结点

步骤如下

  • 当删除的结点没有子结点,直接删除即可;
  • 当需要删除的结点只有一个子结点:
    1. 先删除目标结点;
    2. 把子结点移动到被删除结点即可。
  • 当需要删除的结点有两个子结点:
    1. 先删除目标结点;
    2. 找到右子树的最小值(后继节点)或左子树的最大值(前驱节点);
    3. 将该节点的值复制/覆盖到当前要删除的节点;
    4. 转为递归删除那个后继/前驱节点(由于它是极值点,最多只有 1 个子节点,直接退化为前两种简单情况)。

查找结点

步骤如下

  1. 从二叉搜索树的顶端结点开始往下查找;
  2. 把查找数和结点值进行比较:
    • 小于该结点值则往左搜索;
    • 大于该结点值则往右搜索;

❔题目

1. 二叉搜索树中的搜索

解题思路

根据上文实现中的描述,搜索只要注意以下这两点即可:

  • 小于该结点值则往左搜索;
  • 大于该结点值则往右搜索;

实现

function searchBST(root: TreeNode | null, val: number): TreeNode | null {
    while(root !== null){
        if(root.val === val){
            return root;
        }else if(root.val < val){
            root = root.right;
        }else{
            root = root.left;
        }
    }
    return null;
};
public 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.right;
            } else {
                root = root.left;
            }
        }
        return null;
    }
}
class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        while (root != nullptr) {
            if (root->val == val) {
                return root;
            } else if (root->val < val) {
                root = root->right;
            } else {
                root = root->left;
            }
        }
        return nullptr;
    }
};

复杂度分析

  • 时间复杂度\(O(H)\)
    其中 \(H\) 为二叉搜索树的高度。平均情况下树是平衡的,复杂度为 \(O(\log N)\);最坏情况下树退化为单链表,复杂度为 \(O(N)\)
  • 空间复杂度\(O(1)\)
    采用迭代写法,只需常数级额外空间(若使用递归写法,空间复杂度为递归栈开销 \(O(H)\))。

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

解题思路

同样的,可以查看上文知道步骤如下:

  • 若添加数小于结点值,则左移;
  • 若添加数大于结点值,则右移;

实现

function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null {
    // 递归基:到达空位置,创建新节点返回
    if (!root) return new TreeNode(val);

    if (val < root.val) {
        root.left = insertIntoBST(root.left, val);   // 插入左子树并更新 left 指针
    } else {
        root.right = insertIntoBST(root.right, val);  // 插入右子树并更新 right 指针
    }

    return root;
}
public class Solution {
    public TreeNode InsertIntoBST(TreeNode root, int val) {
        // 递归基:到达空位置,创建新节点返回
        if (root == null) return new TreeNode(val);

        if (val < root.val) {
            root.left = InsertIntoBST(root.left, val);   // 插入左子树并更新 left 指针
        } else {
            root.right = InsertIntoBST(root.right, val);  // 插入右子树并更新 right 指针
        }

        return root;
    }
}
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        TreeNode* newNode = new TreeNode(val);
        if (!root) return newNode;
        TreeNode* cur = root;
        while(cur != nullptr){
            if(val < cur->val){
                if(cur->left == nullptr){
                    cur->left = newNode;
                    break;
                }else{
                    cur = cur->left;
                }
            }else{
                if(cur->right == nullptr){
                    cur->right = newNode;
                    break;
                }else{
                    cur = cur->right;
                }
            }
        }
        return root;
    }
};
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (!root) return new TreeNode(val); // 找到目标位置/原树为空,创建新节点返回
        
        if (val < root->val) {
            root->left = insertIntoBST(root->left, val);  // 递归插入左子树并接住返回值
        } else {
            root->right = insertIntoBST(root->right, val); // 递归插入右子树并接住返回值
        }
        return root;
    }
};

复杂度分析

  • 时间复杂度\(O(H)\),其中 \(H\) 为树的高度。
    平均复杂度为 \(O(\log N)\),最坏退化为单链表时为 \(O(N)\)
  • 空间复杂度
    • 迭代法:\(O(1)\),只需常数级指针。
    • 递归法:\(O(H)\),递归栈调用开销。

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

解题思路

直接引用上文:

步骤如下

  • 当删除的结点没有子结点,直接删除即可;
  • 当需要删除的结点只有一个子结点:
    1. 先删除目标结点;
    2. 把子结点移动到被删除结点即可。
  • 当需要删除的结点有两个子结点:
    1. 先删除目标结点;
    2. 找到右子树的最小值(后继节点)或左子树的最大值(前驱节点);
    3. 将该节点的值复制/覆盖到当前要删除的节点;
    4. 转为递归删除那个后继/前驱节点(由于它是极值点,最多只有 1 个子节> 点,直接退化为前两种简单情况)。

使用递归法实现:

  1. 寻找目标节点:根据 BST 性质,小了去左子树,大了去右子树。
  2. 处理 4 种删除情况:
    • 情况 1:没有子节点(叶节点)\(\rightarrow\) 直接返回 null。
    • 情况 2:只有右孩子 \(\rightarrow\) 返回右孩子替换当前节点。
    • 情况 3:只有左孩子 \(\rightarrow\) 返回左孩子替换当前节点。
    • 情况 4:左右孩子都在 \(\rightarrow\) 找到右子树中的最小节点(后继节点),把它的值覆盖到当前节点,然后递归删除右子树中的那个后继节点。

实现

function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
    if (!root) return null;

    // 1. 寻找目标节点
    if (key < root.val) {
        root.left = deleteNode(root.left, key);
    } else if (key > root.val) {
        root.right = deleteNode(root.right, key);
    } else {
        // 2. 找到目标节点
        if (!root.left) return root.right;
        if (!root.right) return root.left;

        // 找到右子树最小值
        let minNode = root.right;
        while (minNode.left) {
            minNode = minNode.left;
        }

        // 值覆盖
        root.val = minNode.val;

        // 递归删除右子树后继节点
        root.right = deleteNode(root.right, minNode.val);
    }

    return root;
}
public class Solution {
    public TreeNode DeleteNode(TreeNode root, int key) {
        if (root == null) return null;

        // 1. 寻找要删除的节点
        if (key < root.val) {
            // 去左子树删除
            root.left = DeleteNode(root.left, key);
        } else if (key > root.val) {
            // 去右子树删除
            root.right = DeleteNode(root.right, key);
        } else {
            // 2. 找到目标节点,分类讨论删除
            
            // 情况 1 & 2:左子树为空,直接返回右子树
            if (root.left == null) return root.right;
            // 情况 3:右子树为空,直接返回左子树
            if (root.right == null) return root.left;

            // 情况 4:左右子树都不为空
            // 找到右子树的最小值(后继节点)
            TreeNode minNode = root.right;
            while (minNode.left != null) {
                minNode = minNode.left;
            }

            // 用后继节点的值覆盖当前节点
            root.val = minNode.val;

            // 转为递归删除右子树中的那个后继节点
            root.right = DeleteNode(root.right, minNode.val);
        }

        return root;
    }
}
class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (!root) return nullptr;

        // 1. 寻找要删除的节点
        if (key < root->val) {
            root->left = deleteNode(root->left, key);
        } else if (key > root->val) {
            root->right = deleteNode(root->right, key);
        } else {
            // 2. 找到目标节点
            if (!root->left) return root->right;
            if (!root->right) return root->left;

            // 左右子树都不为空:找右子树最小值
            TreeNode* minNode = root->right;
            while (minNode->left) {
                minNode = minNode->left;
            }

            // 值覆盖
            root->val = minNode->val;

            // 递归销毁右子树的后继节点
            root->right = deleteNode(root->right, minNode->val);
        }

        return root;
    }
};

复杂度分析

  • 时间复杂度\(O(H)\),其中 \(H\) 为树的高度。
    平均复杂度为 \(O(\log N)\),最坏退化为单链表时为 \(O(N)\)
  • 空间复杂度\(O(H)\)
    递归调用的栈空间开销(最坏情况下为 \(O(N)\))。

引用

[1] 力扣探索模式
[2] 宫崎修一,石田保辉. 我的第一本算法书 [M]. 微信读书版


注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

posted @ 2026-08-27 13:56  SEHOD  阅读(6)  评论(0)    收藏  举报