Lesson 0405 · 二叉搜索树 BST

④ 数据结构与算法 — 第 5 课

上节课:二叉树 + 四种遍历。
这节课:给二叉树加一条规则,让查找从 O(n) 降到 O(log n)。这条规则就是——小的放左边,大的放右边。
三句话速览
  1. BST 的规矩:左子树全小,右子树全大。
  2. 查找每步只走一边,平衡时 O(log n)。
  3. 中序一定有序;删两个孩子用后继顶替。

1. BST 的规则

二叉搜索树(Binary Search Tree,BST)满足:

        8
       / \
      3   10
     / \    \
    1   6    14
       / \   /
      4   7 13

看根 8:左边所有数都比 8 小,右边所有数都比 8 大。每个子树也满足这个规则。注意是整棵子树都满足,不只是直接孩子——比如 7 比 3 大,但它在 3 的右子树里,依然小于 8。

2. 查找:每次砍掉一半

想找 7:

bool search(Node* root, int x) {
    if (root == nullptr) return false;   // 走到空,没找到
    if (root->val == x) return true;     // 找到

    if (x < root->val) return search(root->left, x);
    else               return search(root->right, x);
}

每次只往一边走,理想情况下树比较平衡,高度约为 log n,所以查找是 O(log n)。这里的「砍掉一半」不是真删,而是每比较一次就排除掉一整棵子树。

3. 插入:找到空位放进去

Node* insert(Node* root, int x) {
    if (root == nullptr) {
        return new Node(x);              // 空位:新节点放这里
    }

    if (x < root->val) {
        root->left = insert(root->left, x);
    } else if (x > root->val) {
        root->right = insert(root->right, x);
    }
    // x == root->val 时这里什么都没做,表示不插入重复值

    return root;
}

插入 5 到上面的树:5 < 8 往左 → 5 > 3 往右 → 5 < 6 往左 → 5 > 4 往右 → 空位,插入。

注意 root->left = insert(root->left, x); 这个赋值:递归返回的可能是新节点,必须接回父节点,否则新节点就丢了。

4. 中序遍历 = 从小到大

BST 最重要的性质:中序遍历的结果一定有序。

void inorder(Node* root) {
    if (root == nullptr) return;
    inorder(root->left);
    cout << root->val << " ";
    inorder(root->right);
}

上面那棵树中序输出:

1 3 4 6 7 8 10 13 14

这也是为什么 set 遍历出来是有序的——它内部就是平衡 BST。

把同一棵 BST 的四种遍历按「第几个被访问」排开(表头 1–9 是访问次序,不是节点编号):

遍历123456789
前序831647101413
中序134678101314
后序147631314108
层序831016144713

看第二行:中序就是严格递增的 1 3 4 6 7 8 10 13 14。前序第一个是根 8,后序最后一个是根 8,层序按 8 → 3、10 → 1、6、14 → 4、7、13 一层层铺开。四种顺序,同一棵树,数字一个不差。

5. 删除:分三种情况

情况例子做法
叶子节点删 7直接删掉
只有一个孩子删 10(只有右孩子 14)用孩子顶替自己的位置
有两个孩子删 8用右子树最小值顶替,再删那个最小值
Node* findMin(Node* root) {
    while (root != nullptr && root->left != nullptr) {
        root = root->left;
    }
    return root;
}

Node* remove(Node* root, int x) {
    if (root == nullptr) return nullptr;

    if (x < root->val) {
        root->left = remove(root->left, x);
    } else if (x > root->val) {
        root->right = remove(root->right, x);
    } else {
        // 找到要删的节点
        if (root->left == nullptr) {
            Node* rightChild = root->right;
            delete root;
            return rightChild;
        }
        if (root->right == nullptr) {
            Node* leftChild = root->left;
            delete root;
            return leftChild;
        }

        // 有两个孩子:找右子树最小值(后继)
        Node* successor = findMin(root->right);
        root->val = successor->val;
        root->right = remove(root->right, successor->val);
    }

    return root;
}

拿删 8 走一遍:8 有两个孩子,右子树最小值是 10(10 没有左孩子),把 10 写到根的位置;然后到右子树里删掉原来那个 10,此时 10 只有一个右孩子 14,直接让 14 顶上来。整棵树仍然满足「左小右大」。

为什么用右子树最小值?
它是右子树里最小的,比左子树所有数都大,同时又比右子树其他数都小,放到根的位置正好维持 BST 规则。

6. BST 的复杂度与退化

情况树形高度查找
平衡左右差不多O(log n)O(log n)
退化像链表一样O(n)O(n)

如果你按 1 2 3 4 5 顺序插入,BST 会一路往右,退化成链表:

1
 \
  2
   \
    3
     \
      4

所以真正好用的 BST 需要「平衡」,例如 AVL 树、红黑树。C++ 的 set、map 底层通常就是红黑树。

7. 完整示例

#include <iostream>
using namespace std;

struct Node {
    int val;
    Node* left;
    Node* right;
    Node(int v) : val(v), left(nullptr), right(nullptr) {}
};

// ==== 插入 ====
Node* insert(Node* root, int x) {
    if (root == nullptr) return new Node(x);
    if (x < root->val) root->left = insert(root->left, x);
    else if (x > root->val) root->right = insert(root->right, x);
    return root;
}

// ==== 查找 ====
bool search(Node* root, int x) {
    if (root == nullptr) return false;
    if (root->val == x) return true;
    if (x < root->val) return search(root->left, x);
    return search(root->right, x);
}

// ==== 中序遍历 ====
void inorder(Node* root) {
    if (root == nullptr) return;
    inorder(root->left);
    cout << root->val << " ";
    inorder(root->right);
}

// ==== main ====
int main() {
    Node* root = nullptr;
    int a[] = {8, 3, 10, 1, 6, 14, 4, 7, 13};

    for (int x : a) root = insert(root, x);

    inorder(root);  cout << endl;     // 1 3 4 6 7 8 10 13 14
    cout << search(root, 7) << endl;  // 1
    cout << search(root, 9) << endl;  // 0

    return 0;
}

8. 坑点清单

9. 小测

Q1. BST 中,左子树的值与根相比应该?
Q2. BST 的哪种遍历结果一定有序?
Q3. BST 删除有两个孩子的节点时,通常用什么顶替?
Q4. 按 1 2 3 4 5 顺序插入 BST,树会变成?

10. 动手练习

题目1:手写 BST

按顺序插入 8 3 10 1 6 14 4 7 13,然后:
- 中序输出,确认有序
- 查找 7 和 9
- 删除 10(只有一个孩子)
- 删除 8(有两个孩子)
- 再中序输出
题目2:洛谷 P5076 普通二叉树

在 BST 上实现插入、查排名、查第 k 小、查前驱/后继。
这题偏难,做之前先把上面的删除写熟。也可以先用 set 做 P2234 营业额统计。
卡住时的自查顺序
  1. 先画出树,标上每个节点的左右指针;每插一个数就沿路径走一遍。
  2. 查找 / 插入:比较的是 x 和 root->val,往左还是往右别写反。
  3. 删除叶子 / 单孩子:先存孩子指针,再 delete,最后把存下来的接回去。
  4. 删除两个孩子:先 findMin(root->right),改值后再递归删后继。
  5. 边界:空树、删根、删不存在的值、连续插入有序数据看会不会退化。

11. 本课小结

下一课预告
0406 堆与优先队列 —— 换个思路:不要求全部有序,只要随时能拿到最小值。堆用完全二叉树 + 一条父子规矩,把「取极值」做到 O(1)、插入删除做到 O(log n),代码比 BST 短得多。

← 上一课:树与二叉树基础 · 课程对照表

书籍 《算法 (第4版)》第 3.2 节(二叉查找树)