左小右大——把"有序"藏在树的形状里,查找一路砍半
想象一本会自己归位的电话簿:新名字进来时,比大小——比当前页码小就翻到左页、大就翻到右页,一直翻到空位坐下。久而久之,整本簿子自动变得"左页全比右页小"。想找某人?每次比一下当前页,直接砍掉一半的页——这正是 GESP5 里二分查找的思想,只不过它长在了一棵树上。
这棵"左小右大"的树叫二叉排序树(Binary Search Tree,BST)。它天生自带三件超能力:查找快(砍半)、插入有序(自动归位)、中序遍历就是升序名单。
把 5、3、8、1、4、7、9 按"比大小走左/右"的规则插入,得到这棵 BST。右边演示查找 7 的全过程——只比了 2 次!
二叉排序树(BST):对每个节点,它左子树里所有节点的值都小于它,右子树里所有节点的值都大于它。注意是"整棵子树",不只是直接孩子——左孩子的所有后代都必须比它小。
推论:沿任何路径,从根往下,值要么一路"比父亲小"(走左)、要么"比父亲大"(走右),不可能先左后右乱跳(比如从根走左到 3、再想跳到 3 的右子树里放 6?不行!6 虽然大于 3,但必须小于 5 才能待在 5 的左子树里)。
插入规则:从根开始比大小,比当前节点小走左、大走右,直到走到一个空位,坐下。依次插入 5、3、8、1、4、7、9:
结果正好是上图那棵完全二叉树(7 个节点、3 层)——因为插入顺序"乱中有序",树长得又矮又壮,查找才快。
查找 = 二分查找换上了树的衣服:比较当前节点,大了走右、小了走左、相等结束。走一步,"还可能的范围"就砍掉一半——因为左子树全小、右子树全大,没走的那半边一个都不用看。
高度决定最坏比较次数:最多比较次数 ≈ 树高 h。n 个节点的"又矮又壮"的二叉树,高度约 log₂n:100 个节点约 7 次、1000 个约 10 次、100 万个约 20 次——所以平均时间复杂度 O(log n)。对比:线性扫描 100 万个要 100 万次!
回忆 GESP5 第 2 课:中序遍历 = 左 → 根 → 右。对普通树,中序只是三种顺序之一;对 BST,中序有魔法:左子树全小 → 根 → 右子树全大,递归下去,输出的序列一定是升序!
对示例树走一遍:左子树(3 为根)先中序 → 1 3 4;再根 5;再右子树(8 为根)中序 → 7 8 9。输出 1 3 4 5 7 8 9——乱序插入的 7 个数,中序遍历直接变成升序名单,这就是"用 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 前的比较次数。③ 重复值怎么处理要听题目(忽略 / 存计数 / 放右子树)。