如果你已经懂了二叉树的基础遍历,可能会问:“二叉树除了遍历,还能解决什么实际问题?”—— 答案是 “高效的排序和查询”。比如要从 10 万条用户 ID 中找某条记录,数组遍历要 10 万次,而二叉搜索树(Binary Search Tree,简称 BST) 只要 20 次左右。
BST 不是新结构,而是 “加了规则的二叉树”—— 核心规则只有一条:左子树所有节点的值 < 根节点的值 < 右子树所有节点的值(左小右大)。这条规则让它天生具备 “自动排序” 和 “快速查询” 的能力,成为数据库索引、字典检索的基础。
一、先搞懂 BST 的核心:一条规则,让二叉树 “变聪明”
BST 是二叉树的 “优等生”,只比普通二叉树多了 “左小右大” 这一条规则,但正是这条规则,让它从 “普通树” 变成 “能高效干活的树”。
1. 用 “字典查字” 理解 BST 的规则
你查字典时,会按拼音 / 部首找:比如查 “查” 字,先翻到 “C” 开头的部分,再找 “cha” 的音节 —— 这就是 “逐步缩小范围”。BST 的 “左小右大” 规则同理:
要找比当前节点小的值,只需要看左子树(不用看右子树);
要找比当前节点大的值,只需要看右子树(不用看左子树);
等于当前节点的值,就是要找的目标。
2. 看一个真实的 BST 结构(直观感受规则)
下面这棵树就是标准的 BST,每个节点都满足 “左小右大”:
5(根)
/ \\
3 7(3<5<7)
/ \ / \\
2 4 6 8(2<3<4,6<7<8)
检查节点 3:左子树 2<3,右子树 4>3,符合规则;
检查节点 7:左子树 6<7,右子树 8>7,符合规则;
所有节点都满足 “左小右大”,这就是 BST 的核心。
二、BST 的灵魂优势:2 个 “天生技能”,秒杀数组和链表
普通二叉树只是 “存数据的容器”,而 BST 凭借 “左小右大” 规则,自带两个实用技能,这也是它被广泛使用的原因。
1. 技能 1:查询快如闪电(平均 O (logn))
查询的核心是 “每次排除一半数据”,就像查字典不翻无关的页码。以找 “6” 为例,步骤如下:
从根 5 开始:6>5→只看右子树(排除左子树 2、3、4,一半数据没了);
到右子树的根 7:6<7→只看左子树(排除右子树 8,又排除一半);
到 7 的左孩子 6:正好等于目标,找到!
总共 3 步,而如果是数组存 [2,3,4,5,6,7,8],找 6 需要遍历 6 次;如果是链表,也需要 6 次。
效率对比(数据量越大,BST 优势越明显):
| 数据量 | BST 查询次数(平均) | 数组遍历次数 | 链表遍历次数 |
|---|---|---|---|
| 100 | 7(log₂100≈7) | 100 | 100 |
| 10000 | 14(log₂10000≈14) | 10000 | 10000 |
| 100000 | 17(log₂100000≈17) | 100000 | 100000 |
2. 技能 2:中序遍历 = 自动排序(升序序列)
普通二叉树的中序遍历是 “左→根→右”,而 BST 的中序遍历会直接输出严格升序序列—— 因为 “左小右大” 的规则,左子树最小,根中间,右子树最大。
以上面的 BST 为例,中序遍历结果是:2 → 3 → 4 → 5 → 6 → 7 → 8(完美升序)。
这意味着:你不用写排序算法,只要把数据插入 BST,再中序遍历,就能得到有序数据 —— 这就是 BST “自动排序” 的魔力。
三、BST 的 3 个核心操作:查询、插入、删除(代码 + 步骤拆解)
BST 的操作都围绕 “左小右大” 规则,以下用 Java、JavaScript、C# 实现,代码可直接复制运行,以 “5-3-7-2-4-6-8” 这棵 BST 为例。
1. 第一步:定义 BST 节点(和普通二叉树节点一样)
BST 的节点结构和普通二叉树完全相同,只是操作逻辑不同:
// Java版本(JS、C#结构一致,语法稍改)
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
2. 核心操作 1:查询(找目标值)
逻辑:按 “左小右大” 逐步缩小范围,直到找到或为空。
Java 代码
// 查询目标值是否存在,存在返回该节点,不存在返回null
public static TreeNode search(TreeNode root, int target) {
if (root == null) {
return null; // 树空或没找到,返回null
}
if (target == root.val) {
return root; // 找到目标,返回节点
} else if (target < root.val) {
return search(root.left, target); // 比当前小,查左子树
} else {
return search(root.right, target); // 比当前大,查右子树
}
}
// 调用示例(找6)
TreeNode bstRoot = buildBST(); // 先构建“5-3-7-2-4-6-8”这棵BST
TreeNode targetNode = search(bstRoot, 6);
System.out.println(targetNode != null ? "找到:" + targetNode.val : "没找到"); // 输出“找到:6”
关键提醒:
递归终止条件:要么找到目标,要么节点为空(没找到);
非递归实现(避免栈溢出):用 while 循环替代递归,逻辑完全一致。
3. 核心操作 2:插入(自动维持 “左小右大”,实现自动排序)
逻辑:按 “左小右大” 找到空位置,插入新节点(新节点默认是叶子)。
重点:插入后 BST 的规则不变,且中序遍历仍为有序序列。
插入步骤(以插入 “9” 为例)
从根 5 开始:9>5→查右子树;
到 7:9>7→查右子树;
到 8:9>8→查右子树(8 的右子树为空);
把 9 作为 8 的右孩子插入,插入后 BST 仍满足 “左小右大”。
Java 代码
// 插入新值,返回插入后的根节点(递归实现)
public static TreeNode insert(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val); // 找到空位置,插入新节点
}
if (val < root.val) {
// 比当前小,插入左子树
root.left = insert(root.left, val);
} else if (val > root.val) {
// 比当前大,插入右子树
root.right = insert(root.right, val);
}
// 注意:val等于当前节点值时,默认不插入(避免重复,可根据需求修改)
return root;
}
// 构建BST示例(插入5、3、7、2、4、6、8)
public static TreeNode buildBST() {
TreeNode root = null;
int\[] values = {5, 3, 7, 2, 4, 6, 8};
for (int val : values) {
root = insert(root, val); // 逐个插入,自动维持BST规则
}
return root;
}
自动排序验证:
插入上述数组后,中序遍历 BST:
// 中序遍历(和第一篇一致)
public static void inorder(TreeNode root) {
if (root == null) return;
inorder(root.left);
System.out.print(root.val + " ");
inorder(root.right);
}
// 调用:inorder(bstRoot) → 输出:2 3 4 5 6 7 8(升序)
4. 核心操作 3:删除(最复杂,分 3 种情况)
删除是 BST 的难点,但只要记住 “删除后仍满足左小右大” 的原则,分 3 种情况处理即可。
情况 1:删除的是 “叶子节点”(无孩子)
逻辑:直接删除,把父节点的对应孩子指针设为 null。
示例:删除 BST 中的 “2”(叶子节点):
找到 2(3 的左孩子);
把 3 的 left 设为 null,删除完成。
情况 2:删除的是 “只有一个孩子的节点”
逻辑:用孩子节点替代被删除节点的位置(“子承父业”)。
示例:删除 BST 中的 “3”(只有右孩子 4):
找到 3(5 的左孩子,右孩子是 4);
把 5 的 left 设为 4(用 4 替代 3 的位置),删除完成。
情况 3:删除的是 “有两个孩子的节点”(最复杂)
逻辑:用 “右子树的最小节点”(或左子树的最大节点)替代被删除节点,再删除那个最小节点。
原因:右子树的最小节点是 “比被删除节点大的最小数”,替代后能维持左小右大规则。
示例:删除 BST 中的 “5”(有左孩子 3、右孩子 7):
找到 5 的右子树(根 7),找右子树的最小节点(6,7 的左孩子);
用 6 的 val 替代 5 的 val(把 5 变成 6);
删除右子树中的 6(6 是叶子节点,按情况 1 处理);
删除后 BST 仍满足左小右大:
6(原5的位置被6替代)
/ \\
3 7(原7的左孩子6被删除)
/ \ \\
2 4 8
Java 代码(删除完整实现)
// 删除目标值,返回删除后的根节点
public static TreeNode delete(TreeNode root, int target) {
if (root == null) {
return null; // 没找到目标,返回原树
}
// 1. 找到要删除的节点
if (target < root.val) {
root.left = delete(root.left, target); // 删左子树的节点
} else if (target > root.val) {
root.right = delete(root.right, target); // 删右子树的节点
} else {
// 2. 找到目标节点,分3种情况删除
// 情况1:叶子节点或只有一个孩子
if (root.left == null) {
return root.right; // 左空,用右孩子替代
}
if (root.right == null) {
return root.left; // 右空,用左孩子替代
}
// 情况2:有两个孩子,找右子树的最小节点
TreeNode minNode = findRightMin(root.right);
// 用最小节点的值替代当前节点
root.val = minNode.val;
// 删除右子树中的最小节点
root.right = delete(root.right, minNode.val);
}
return root;
}
// 找右子树的最小节点(最左边的节点)
private static TreeNode findRightMin(TreeNode node) {
while (node.left != null) {
node = node.left; // 一直往左走,直到左孩子为空
}
return node;
}
// 调用示例(删除5)
bstRoot = delete(bstRoot, 5);
inorder(bstRoot); // 输出:2 3 4 6 7 8(仍为升序)
四、BST 的致命缺陷:有序插入会退化成 “链表”
BST 的优势建立在 “树是平衡的” 基础上,但如果插入的数据是有序的(比如 1、2、3、4、5),BST 会退化成一条链表 —— 此时 “左小右大” 规则仍满足,但查询效率从 O (logn) 暴跌到 O (n),和普通链表没区别。
退化过程(插入 1→2→3→4→5)
插入 1:根是 1;
插入 2:2>1→作为 1 的右孩子;
插入 3:3>2→作为 2 的右孩子;
插入 4:4>3→作为 3 的右孩子;
插入 5:5>4→作为 4 的右孩子;
退化后的结构(链表):
1 → 2 → 3 → 4 → 5(每个节点只有右孩子)
退化的问题:查询效率暴跌
找 5 时,需要从 1 遍历到 5(5 次),和数组 / 链表一样,完全失去 BST 的优势。
这就是为什么实际项目中很少用 “纯 BST”,而是用它的改进版 —— 红黑树、AVL 树(解决平衡问题)。
五、小实战:用 BST 实现 “简单字典”(查单词 + 排序)
结合 BST 的 “快速查询” 和 “自动排序” 优势,实现一个能 “查单词” 和 “按字母排序输出” 的字典:
Java 代码(单词字典)
class WordNode {
String word; // 单词(关键词,按字母序比较)
String meaning; // 释义
WordNode left;
WordNode right;
public WordNode(String word, String meaning) {
this.word = word;
this.meaning = meaning;
this.left = null;
this.right = null;
}
}
public class BSTDictionary {
// 插入单词(按单词字母序比较,a\<b\<c...)
public static WordNode insertWord(WordNode root, String word, String meaning) {
if (root == null) {
return new WordNode(word, meaning);
}
// 按字符串字典序比较(Java的compareTo方法)
if (word.compareTo(root.word) < 0) {
root.left = insertWord(root.left, word, meaning);
} else if (word.compareTo(root.word) > 0) {
root.right = insertWord(root.right, word, meaning);
} else {
root.meaning = meaning; // 单词已存在,更新释义
}
return root;
}
// 查询单词
public static String searchWord(WordNode root, String word) {
if (root == null) {
return "单词不存在";
}
if (word.compareTo(root.word) == 0) {
return root.meaning; // 找到,返回释义
} else if (word.compareTo(root.word) < 0) {
return searchWord(root.left, word);
} else {
return searchWord(root.right, word);
}
}
// 中序遍历:按字母序输出所有单词(自动排序)
public static void printSortedWords(WordNode root) {
if (root == null) return;
printSortedWords(root.left);
System.out.println(root.word + ":" + root.meaning);
printSortedWords(root.right);
}
// 测试
public static void main(String\[] args) {
WordNode dictRoot = null;
// 插入单词
dictRoot = insertWord(dictRoot, "apple", "苹果");
dictRoot = insertWord(dictRoot, "banana", "香蕉");
dictRoot = insertWord(dictRoot, "cat", "猫");
dictRoot = insertWord(dictRoot, "dog", "狗");
// 查询单词
System.out.println(searchWord(dictRoot, "banana")); // 输出“香蕉”
System.out.println(searchWord(dictRoot, "egg")); // 输出“单词不存在”
// 按字母序输出所有单词
System.out.println("\n按字母序排序的单词:");
printSortedWords(dictRoot);
// 输出:
// apple:苹果
// banana:香蕉
// cat:猫
// dog:狗
}
}
最后:BST 的核心总结
BST 的核心是 “左小右大”:这条规则让它天生会查询(O (logn))、会排序(中序遍历升序);
操作围绕 “维持规则”:插入找空位置,删除分 3 种情况,核心是不破坏左小右大;
致命缺陷是 “失衡退化”:有序插入会变成链表,效率暴跌 —— 这就是下一篇要讲的 “红黑树” 需要解决的问题。
下一篇咱们会学习 “红黑树”:它是 BST 的 “平衡版”,能自动修复失衡,让查询、插入、删除效率始终稳定在 O (logn),是工业级项目的首选。