一、什么是二叉搜索树?是“二叉”还是“搜索”?
先别被名字吓到,我们拆开看:
- 二叉:每个节点最多有两个孩子(左儿子 & 右儿子)。
- 搜索:查找贼快,比遍历数组快多了。
- 树:不是种出来的那种🌳,是数据结构里的“家族族谱”。
合起来就是:一个有序的、能高效查找的二叉树。
✅ 核心规则就一条:
对任意节点来说:
左子树的所有值 < 当前节点 < 右子树的所有值
举个栗子🌰:
6
/ \
3 8
/ \ / \
1 4 7 9
你看,3 的左边是 1(<3),右边是 4(>3);6 的左边全是小于6的(3,1,4),右边全是大于6的(8,7,9)——完美符合“左小右大”家规。
二、中序遍历:BST 的隐藏彩蛋 🎁
你知道吗?BST 最骚的操作不是插入删除,而是它的中序遍历结果天然有序!
什么叫中序遍历?顺序是:左 → 根 → 右
上面那棵树中序走一圈:1 → 3 → 4 → 6 → 7 → 8 → 9 —— 哇哦,升序排列!
👉 所以说:BST 是一棵会自动排序的树,简直是懒人福音。
三、三大基本操作:查、插、删,谁不会啊?
🔍 1. 查找:我要找值为 n 的节点
思路超简单,跟二分查找一样:
function search(root, n) {
if (!root) return null; // 没找到,凉了
if (root.val === n) return root; // 找到了,牛逼!
if (root.val > n) return search(root.left, n); // 往左找
else return search(root.right, n); // 往右找
}
每次比较都能砍掉一半子树,效率杠杠的!
🎯 类比:就像在字典里找“苹果”,你不会一页页翻,而是直接翻到 A 开头。
➕ 2. 插入:新来的,请坐叶子位!
要插入一个新值,就得找个“合适的位置”安家,还得不破坏“左小右大”的规矩。
function insertIntoBST(root, val) {
if (!root) return new TreeNode(val); // 空位?直接上岗!
if (root.val > val) {
root.left = insertIntoBST(root.left, val); // 小的放左边
} else {
root.right = insertIntoBST(root.right, val); // 大的放右边
}
return root;
}
📌 注意:新节点永远插在叶子节点位置,绝不搞“插队领导”的事情。
❌ 3. 删除:最难的来了!别慌,分三种情况搞定
删除为啥难?因为你不能删完让树“乱套”。我们按目标节点的孩子数量分类处理:
✅ 情况1:没孩子(叶子节点)——干净利落,直接删!
if (!root.left && !root.right) {
root = null;
}
就像公司裁掉实习生,毫无波澜。
✅ 情况2:只有一个孩子 —— 孩子顶上,继承家业!
比如只有左孩子:
if (root.left && !root.right) {
root = root.left; // 左孩子接班
}
// 同理,只有右孩子就让右孩子上位
家族企业传承,子承父业,稳得很。
✅ 情况3:两个孩子都有 —— 麻烦了,得“换人再删”
这时候不能直接删,否则左右两派要打架。怎么办?
👉 经典策略:拿“左子树的最大值”或“右子树的最小值”来顶替它!
通常我们选右子树的最小值(也就是最左边的那个):
function deleteNode(root, key) {
if (!root) return null;
if (key < root.val) {
root.left = deleteNode(root.left, key);
} else if (key > root.val) {
root.right = deleteNode(root.right, key);
} else {
// 找到目标节点,开始分类讨论
if (!root.left && !root.right) {
return null; // 叶子,直接消失
} else if (!root.left) {
return root.right; // 有右孩子,他上
} else if (!root.right) {
return root.left; // 有左孩子,他上
} else {
// 两个孩子都有!找右子树最小值来替换
const minNode = findMin(root.right);
root.val = minNode.val;
root.right = deleteNode(root.right, minNode.val); // 再删那个最小值
}
}
return root;
}
// 辅助函数:找最小值(一直往左)
function findMin(node) {
while (node.left) node = node.left;
return node;
}
🧠 思考一下:为什么用右子树的最小值?
因为它刚好比当前节点大一点点,又能比右子树其他人都小,完美过渡!
就像 CEO 退休,选了个能力最强、资历最顺的副总接班,团队稳定如初。
四、时间复杂度:平衡才是王道 ⚖️
BST 的性能完全取决于它的“身材”是否匀称:
| 情况 | 树的高度 | 时间复杂度 |
|---|---|---|
| 平衡树(理想状态) | O(log n) | 查/插/删都是 O(log n) |
| 不平衡(退化成链表) | O(n) | 啥都变成 O(n),慢得像爬 |
🌰 举个反面教材:
1
\
2
\
3
\
4
\
5
这不就是个链表嘛!查找要从头走到尾,O(n) 直接破防。
💡 所以后人发明了 AVL树、红黑树 这类自平衡 BST,每次插入删除自动“整容”,保证树始终矮胖结实。
就像健身教练天天盯着你:“兄弟,别驼背,挺直了!”
五、实际应用场景:不只是面试题!
你以为 BST 只是用来刷题的?Too young too simple!
🚀 1. 数据库索引(MySQL 的幕后英雄)
MySQL 的 InnoDB 引擎用的是 B+树,本质是多路版的搜索树,思想源头就是 BST!
没有它,你的
SELECT * FROM users WHERE id=10086得扫全表,老板看了都想开除你。
🧰 2. 有序映射容器(Java/C++ 必备)
- Java 的
TreeMap、TreeSet - C++ 的
std::map、std::set
底层都是基于红黑树实现的,支持按键排序、范围查询、快速增删改。
面试官最爱问:“HashMap 和 TreeMap 有啥区别?”——现在你可以笑着说:“一个无序一个有序,背后一个是哈希表,一个是BST派系。”
🔍 3. 范围查询:找出价格在 [100, 500] 的商品
利用中序遍历的有序性,BST 可以高效完成这类需求:
function rangeQuery(root, low, high, res = []) {
if (!root) return;
if (low < root.val) rangeQuery(root.left, low, high, res); // 先查左边可能符合条件的
if (root.val >= low && root.val <= high) {
res.push(root.val); // 符合范围,加入结果
}
if (high > root.val) rangeQuery(root.right, low, high, res); // 再查右边
return res;
}
电商系统、股票行情、用户积分排行……统统适用!
六、总结:BST 的哲学,不止于代码
| 特性 | 说明 |
|---|---|
| 🧠 核心思想 | 左小右大,递归统治一切 |
| ⏱️ 平均效率 | O(log n),接近二分查找 |
| 💣 致命弱点 | 容易失衡,需靠自平衡树补救 |
| 🛠️ 实际用途 | 索引、有序集合、范围查询 |
| 🧙♂️ 学习意义 | 是理解 AVL、红黑树、B树的起点 |
❤️ 写在最后
二叉搜索树,就像编程世界的“中庸之道”——
不大不小,不偏不倚,左有规矩,右有秩序。
虽然它自己不够稳重(容易不平衡),但它是无数高级数据结构的“亲爹”。
学好 BST,不只是为了过面试,更是为了看清整个“树家族”的权力游戏。
下次有人问你:“BST 和普通二叉树有啥区别?”
你可以微微一笑:“一个讲秩序,一个像疯子。”