Appearance
20. 会找路的树(二叉搜索树)
知识点:二叉搜索树(BST)—— 左小右大,查找像二分(平均 O(log n))
项目:成绩管理器(查询 / 插入 / 按顺序打印)
故事开场:三样都想要的老师
期中考试出成绩了。老师要一个"成绩管理器",三个要求:
- 查询:报一个分数,马上知道"有没有这个分"
- 插入:新成绩随时加进来
- 按顺序打印:随时能把所有成绩从低到高列出来
你翻了翻学过的武器库:
- 数组 + 二分查找(第 11 章):查询 O(log n)!打印也快(本来就排着)。 可插入要挪位——O(n),新成绩多了就慢了
- Map(第 8 章):插入 O(1)!可按顺序打印?Map 不按大小排
- 链表(第 9 章):插入 O(1)!可查询只能从头走——O(n)
"数组查得快、链表插得快、Map 都不慢……"你叹气,"就没有一个 三个愿望都满足的?"
笨办法先行:把"二分查找"种成一棵树
老师看你发愁,提示了一句:"想想第 19 章练习里那个巧合—— 中序遍历二叉树,输出刚好是从小到大!"
你回去翻作业:那棵成绩树是 80 在根,60 在左,90 在右—— 左边的比根小,右边的比根大!所以中序(左→自己→右)就是排序!
"如果我种一棵有纪律的树:左孩子永远比自己小,右孩子永远比自己大……"
- 查询:从根出发,目标比根小就走左边,比根大就走右边—— 和二分查找一模一样,每次排除半棵树!O(log n)!
- 插入:也走这条路,走到空位就"挂"上去——像链表一样改一个钩子!
- 打印:中序遍历,自动从小到大!
你一拍大腿:"这棵树自己会找路!"
引出知识点:二叉搜索树——左小右大的树
**二叉搜索树(Binary Search Tree,简称 BST)**的规矩只有一条:
左子树的所有节点 < 根节点 < 右子树的所有节点 (而且左子树、右子树自己也要守这个规矩——递归!)
有了这条规矩,整棵树就是"立起来的二分查找":
50
↙ ↘
30 70
↙ ↘ ↙ ↘
20 40 60 80找 60:50 比 60 小 → 走右 → 70 比 60 大 → 走左 → 找到了! 三步,排除了 5 个数。树越"平衡",每次排除的越多,平均 O(log n)。
动手实现:成绩管理器
先造"种树"和"找路"两个函数:
ts
type BSTNode = {
value: number
left: BSTNode | null
right: BSTNode | null
}
function makeNode(value: number): BSTNode {
return { value: value, left: null, right: null }
}
// 插入:新成绩"走一路、找个空位挂上"
function insert(root: BSTNode | null, value: number): BSTNode {
if (root === null) {
return makeNode(value) // 找到空位!种一个新节点
}
if (value < root.value) {
root.left = insert(root.left, value) // 比根小 → 去左子树找空位
} else if (value > root.value) {
root.right = insert(root.right, value) // 比根大 → 去右子树找空位
}
return root // 相等:重复成绩,不种了
}
// 查询:从根出发,一路"问路"
function search(root: BSTNode | null, target: number): boolean {
if (root === null) {
return false // 问路问到死胡同:没有
}
if (root.value === target) {
return true // 找到了!
}
if (target < root.value) {
return search(root.left, target) // 比根小,去左边问
}
return search(root.right, target) // 比根大,去右边问
}
// 按顺序打印:中序遍历(左 → 自己 → 右)
function inorder(root: BSTNode | null): void {
if (root === null) { return }
inorder(root.left)
console.log(root.value)
inorder(root.right)
}组装成成绩管理器:
ts
let root: BSTNode | null = null
root = insert(root, 50)
root = insert(root, 30)
root = insert(root, 70)
root = insert(root, 20)
root = insert(root, 40)
root = insert(root, 60)
root = insert(root, 80)
console.log("有 60 分吗?" + search(root, 60)) // true
console.log("有 65 分吗?" + search(root, 65)) // false
// 新成绩插入
root = insert(root, 65)
console.log("插入 65 后有 65 分吗?" + search(root, 65)) // true
console.log("按顺序打印所有成绩:")
inorder(root)输出:
有 60 分吗?true
有 65 分吗?false
插入 65 后有 65 分吗?true
按顺序打印所有成绩:
20
30
40
50
60
65
70
80查询、插入、按顺序打印——三个愿望,一棵树全满足!
跑起来:BST 的"路"有多短?
和数组版"插入 + 查找"比比看:造 2 万个成绩,交替插入、查询:
ts
// 造 2 万个随机成绩,插进 BST
let tree: BSTNode | null = null
for (let i = 0; i < 20000; i++) {
tree = insert(tree, Math.floor(Math.random() * 100000))
}
// 查询 1000 次
console.time("BST 查询 1000 次")
let hit = 0
for (let i = 0; i < 1000; i++) {
if (search(tree, Math.floor(Math.random() * 100000))) {
hit++
}
}
console.timeEnd("BST 查询 1000 次")
console.log("查到了 " + hit + " 次")在我这台电脑上,输出(数字可能不一样):
BST 查询 1000 次: 0.5ms1000 次查询,半毫秒。每次查询只走大约 15 层(log₂ 20000 ≈ 15) ——2 万个数,每层排除一半,15 步就能到任何地方。 这比数组的二分查找(不用挪位)还方便:插入和查询一样快。
危险:树长歪了怎么办?
老师突然说:"把成绩按从小到大的顺序插进去试试!"
ts
let crooked: BSTNode | null = null
for (let i = 1; i <= 10; i++) {
crooked = insert(crooked, i) // 1, 2, 3, 4, ..., 10
}
inorder(crooked)树变成了什么样?画一画:
1
\
2
\
3
\
4
\
...长成一条链了! 每个新节点都比根大,永远往右挂—— "树"退化成了"链表"!查询"10"要走 10 步(O(n)), 查询"1 万"要走 1 万步——BST 的 O(log n) 没了!
所以真正的二叉搜索树都有"防歪"机制:插完自动旋转,让树保持 两边差不多高(这叫平衡)。你以后会学到 AVL 树、红黑树—— 它们就是"不会长歪的 BST"。今天先记住: BST 平均 O(log n),但前提是树不能歪。
小挑战
- 找最小 / 找最大:BST 里最小的数在哪里?最大的呢? 写
findMin(root)和findMax(root)(提示:一路往左走 / 一路往右走,走到走不动为止——不用递归也行!)。 - 数节点:写
countNodes(root)数出一共有多少个节点 (提示:1 + 左子树 + 右子树,递归)。 - 成绩范围:找出所有"60 到 80 之间"的成绩(提示:中序遍历时, 只打印范围内的——或者更聪明:如果根小于 60,左边整棵都不用看!)。
课后练习
第 1 题(动手题):种一棵自己的 BST
依次插入 [5, 3, 8, 1, 4, 7, 9],画出这棵树(在纸上), 然后回答:树高几层?中序遍历输出什么?用程序验证。
参考答案(点开查看)
5
↙ ↘
3 8
↙ ↘ ↙ ↘
1 4 7 9- 树高 3 层(5 → 3 → 1 是 3 步)
- 中序遍历:1 3 4 5 7 8 9(从小到大!)
程序验证:insert 依次种入,inorder 输出应该完全一致。
第 2 题(思考题):为什么中序遍历是排序?
BST 的规矩是"左 < 根 < 右",中序遍历是"左 → 自己 → 右"。 把这两个结合起来,解释一下为什么中序遍历的输出一定是从小到大。
参考答案(点开查看)
中序遍历每个节点时,都先走完它的左子树(全比它小), 然后打印它自己,再走右子树(全比它大)。
所以打印的顺序是:小 → 中 → 大,一层层递归下去, 整棵树打印出来就是从小到大。
一句话:树的规矩(左小右大) + 遍历的顺序(左中右) = 排序。 这就是 BST 的"隐藏技能"——它天然自带排序输出。
第 3 题(思考题):BST vs Map
BST 能"按顺序打印",Map 不能。那 BST 能完全替代 Map 吗? (提示:想想 Map 的 O(1) 是从哪来的,BST 是多少。)
参考答案(点开查看)
不能完全替代。
- Map 查找是 O(1)(哈希函数一步算出位置),BST 是 O(log n)(要走几层)
- 所以"只要查得快"的场景,Map 更优
但 Map 有两个短板:① 不能按大小顺序遍历 ② 不能找"比 X 小的有多少个" 这类范围问题。BST(和它的平衡版本)擅长这些。
选择口诀:只要"按键查找"→ Map;要"有序 + 范围查询"→ BST。 两种结构都常用,谁也替代不了谁。
下一课预告:游戏排行榜!班主任要知道"全班最高分",而且 新成绩随时进来、旧成绩随时出去。数组?每次找最大要遍历 一遍 O(n)。BST?找最大要一路往右走。有没有一个结构, 最顶上永远放着最大的数,取它只要一步?