🔍

二叉排序树(BST)

左小右大——把"有序"藏在树的形状里,查找一路砍半

💡 打个比方

想象一本会自己归位的电话簿:新名字进来时,比大小——比当前页码小就翻到左页、大就翻到右页,一直翻到空位坐下。久而久之,整本簿子自动变得"左页全比右页小"。想找某人?每次比一下当前页,直接砍掉一半的页——这正是 GESP5 里二分查找的思想,只不过它长在了一棵树上。

这棵"左小右大"的树叫二叉排序树(Binary Search Tree,BST)。它天生自带三件超能力:查找快(砍半)、插入有序(自动归位)、中序遍历就是升序名单。

二叉排序树:对每个节点,左子树所有值 < 节点值 < 右子树所有值。查找 = 比大小走左/右(类似二分,平均 O(log n));中序遍历 = 升序序列。

🖼️ 大图看明白

把 5、3、8、1、4、7、9 按"比大小走左/右"的规则插入,得到这棵 BST。右边演示查找 7 的全过程——只比了 2 次!

5 3 8 1 4 7 9 左子树:1 3 4(都 < 5) 右子树:7 8 9(都 > 5) 中序遍历(左→根→右)= 1 3 4 5 7 8 9,自动升序! 查找 7:每步砍掉一半 第 1 步:7 和 5 比 → 7 > 5,走右 左边一半(1、3、4、5)全部排除 第 2 步:7 和 8 比 → 7 < 8,走左 8 和 9 排除 第 3 步:7 == 7,找到! 路径 5 → 8 → 7,共 3 次比较(8 个节点最多 ⌈log₂8⌉=3 次) 查找 9 呢?5 → 8 → 9,3 次比较。 查找 2 呢?5 → 3 → 1,3 次比较后到叶子发现没有。

🧩 子任务 1:定义——左小根大右

🎯 小目标:背下 BST 性质,会判断一棵树是不是 BST

二叉排序树(BST):对每个节点,它左子树里所有节点的值都小于它,右子树里所有节点的值都大于它。注意是"整棵子树",不只是直接孩子——左孩子的所有后代都必须比它小。

推论:沿任何路径,从根往下,值要么一路"比父亲小"(走左)、要么"比父亲大"(走右),不可能先左后右乱跳(比如从根走左到 3、再想跳到 3 的右子树里放 6?不行!6 虽然大于 3,但必须小于 5 才能待在 5 的左子树里)。

✅ 小检查:5 的左子树(根 3)的右孩子能放 6 吗?为什么?
看看答案
不能。6 在 5 的左子树里,必须 < 5;虽然 6 > 3 满足"比 3 大走右",但违反了对 5 的约束。BST 看的是整条祖先链。

🧩 子任务 2:插入——新数来了往哪坐?

🎯 小目标:会手工把一串数插入 BST,并警惕"排好队进树"的退化

插入规则:从根开始比大小,比当前节点小走左、大走右,直到走到一个空位,坐下。依次插入 5、3、8、1、4、7、9:

  1. 5:树空,5 当根。
  2. 3:3 < 5 → 左,空位,坐下(5 的左孩子)。
  3. 8:8 > 5 → 右,空位,坐下(5 的右孩子)。
  4. 1:1 < 5 → 左;1 < 3 → 左,空位,坐下(3 的左孩子)。
  5. 4:4 < 5 → 左;4 > 3 → 右,空位,坐下(3 的右孩子)。
  6. 7:7 > 5 → 右;7 < 8 → 左,空位,坐下(8 的左孩子)。
  7. 9:9 > 5 → 右;9 > 8 → 右,空位,坐下(8 的右孩子)。

结果正好是上图那棵完全二叉树(7 个节点、3 层)——因为插入顺序"乱中有序",树长得又矮又壮,查找才快。

退化警告:如果按升序 1、2、3、4、5 插入,每个新数都比现有所有数大,一路走右,树变成一条右斜链(像链表):查找 5 要 5 次,O(n)!BST 的"砍半"只在树矮时成立。数据随机时平均高度 O(log n);有序数据要特殊处理(高级课学的平衡树)。
✅ 小检查:按 3、1、2 的顺序插入,树长什么样?查找 2 要几次比较?
看看答案
3 当根;1 < 3 坐左;2 < 3 走左、2 > 1 坐 1 的右孩子。树:3 为根,左孩子 1,1 的右孩子 2。查 2:3 → 1 → 2,3 次比较(树只有 3 个节点,最多 3 次)。

🧩 子任务 3:查找——每步砍一半

🎯 小目标:会手工查找,能说清 BST 查找为什么是 O(log n)

查找 = 二分查找换上了树的衣服:比较当前节点,大了走右、小了走左、相等结束。走一步,"还可能的范围"就砍掉一半——因为左子树全小、右子树全大,没走的那半边一个都不用看。

高度决定最坏比较次数:最多比较次数 ≈ 树高 h。n 个节点的"又矮又壮"的二叉树,高度约 log₂n:100 个节点约 7 次、1000 个约 10 次、100 万个约 20 次——所以平均时间复杂度 O(log n)。对比:线性扫描 100 万个要 100 万次!

和 GESP5 二分查找的关系:二分在有序数组里砍半,BST 在有序树里砍半,思想完全一样。区别:数组插入/删除要挪一整片(O(n)),BST 插入/删除只动局部(平均 O(log n))——BST 是"会动的有序表"。
✅ 小检查:1000 个节点、长得健康的 BST,最坏要比较多少次?(提示:2¹⁰ = 1024)
看看答案
高度约 ⌈log₂1000⌉ = 10(2¹⁰ = 1024 ≥ 1000),所以最多 10 次比较。数据量翻 1000 倍,比较次数只多 10 次——这就是 O(log n) 的威力。

🧩 子任务 4:中序遍历 = 升序名单(超能力)

🎯 小目标:理解"BST 的中序遍历天然有序",会用它输出排序结果

回忆 GESP5 第 2 课:中序遍历 = 左 → 根 → 右。对普通树,中序只是三种顺序之一;对 BST,中序有魔法:左子树全小 → 根 → 右子树全大,递归下去,输出的序列一定是升序!

对示例树走一遍:左子树(3 为根)先中序 → 1 3 4;再根 5;再右子树(8 为根)中序 → 7 8 9。输出 1 3 4 5 7 8 9——乱序插入的 7 个数,中序遍历直接变成升序名单,这就是"用 BST 排序"。

考点:① BST 的中序遍历 = 升序(单选必考)。② 给一棵树,判断它是不是 BST:中序一遍,看序列是否严格上升,是则是、否则否。③ BST 查找 = 二叉树搜索算法的考点落点,平均 O(log n)。
✅ 小检查:一棵树的中序输出是 2 5 7 9 11,这棵树最少/最多可能有多高?
看看答案
5 个节点。最矮 = 完全形,高 3(1+2+2);最高 = 一条链,高 5。中序有序只说明它是 BST,不说明它矮——形状由插入顺序决定。

💻 完整代码

插入(递归归位)+ 中序遍历(输出升序)+ 查找(数比较次数),一个文件三件超能力。

// BST:插入 / 中序输出 / 查找(计比较次数)
#include <bits/stdc++.h>
using namespace std;

struct Node {
    int val;
    Node *l, *r;
    Node(int v) : val(v), l(nullptr), r(nullptr) {}
};

void insert(Node *&p, int x) {      // 注意:参数是"引用的引用"
    if (!p) { p = new Node(x); return; }
    if (x < p->val)      insert(p->l, x);   // 小走左
    else if (x > p->val) insert(p->r, x);   // 大走右;相等忽略
}

int search(Node *p, int x) {          // 返回比较次数(找到或走到头)
    int cnt = 0;
    while (p) {
        cnt++;
        if (x == p->val) return cnt;
        p = (x < p->val) ? p->l : p->r;
    }
    return cnt;
}

void inorder(Node *p) {               // BST 中序 = 升序
    if (!p) return;
    inorder(p->l);
    cout << p->val << " ";
    inorder(p->r);
}

int main() {
    Node *root = nullptr;
    int nums[] = {5, 3, 8, 1, 4, 7, 9};
    for (int x : nums) insert(root, x);

    cout << "inorder: ";
    inorder(root);   // 输出:1 3 4 5 7 8 9
    cout << endl;
    cout << "search 7: " << search(root, 7) << " 次比较" << endl;  // 3
    cout << "search 2: " << search(root, 2) << " 次比较" << endl;  // 3(未找到)
    return 0;
}
易错点:① insert 的参数要写 Node *&p(引用的引用)——新节点要在父节点的孩子指针上安家,只传 Node *p 的话新地址传不回去。② 查找用 while 不用递归更简单;"未找到"时 cnt 是走到 nullptr 前的比较次数。③ 重复值怎么处理要听题目(忽略 / 存计数 / 放右子树)。

✋ 你来试试

第 1 题:依次插入 4、2、6、1、3、5、7,写出中序遍历结果。
1 2 3 4 5 6 7。无论插入顺序如何,BST 的中序永远是升序(这棵树恰好是 4 为根、2/6 为左右孩子的完全形)。
第 2 题:按 1、2、3、4、5 升序插入后,查找 5 要几次比较?查找 1 呢?
树退化成右斜链 1→2→3→4→5。查 5:5 次(最坏);查 1:1 次(根就是 1)。平均 3 次 = O(n),砍半失效了。
第 3 题:某二叉树的中序遍历是 2 4 6 8,能断定它是 BST 吗?为什么?
能。中序严格上升意味着:对每个节点,它左子树输出的数全在它前面且更小、右子树全在后面且更大——BST 性质对每个节点都成立。
🎯 一句话记住:BST = 左小根大右;查找砍半 O(log n),中序必升序;升序数据插进去会退化成链。