二叉搜索树与平衡树

本节目标

BST 的性质:左子树所有值 < 根 < 右子树所有值。据此查找/插入只需沿一条路径走,平均 O(log n)。

// 运行环境:Node.js 14+
// 保存为 dsa-l12.js,执行:node dsa-l12.js

class TreeNode {
  constructor(val) { this.val = val; this.left = null; this.right = null }
}

function insert(root, val) {
  if (!root) 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)
  return root
}

function search(root, val) {
  while (root) {
    if (val === root.val) return root
    root = val < root.val ? root.left : root.right
  }
  return null
}

// 删除:①叶子直接删 ②单子节点接上 ③双子节点→用"后继"(右子树最小)替身
function remove(root, val) {
  if (!root) return null
  if (val < root.val) root.left = remove(root.left, val)
  else if (val > root.val) root.right = remove(root.right, val)
  else {
    if (!root.left) return root.right
    if (!root.right) return root.left
    let succ = root.right
    while (succ.left) succ = succ.left
    root.val = succ.val
    root.right = remove(root.right, succ.val)
  }
  return root
}

// 中序验证 BST(升序即通过)
function isValidBST(root, prev = { v: -Infinity }) {
  if (!root) return true
  if (!isValidBST(root.left, prev)) return false
  if (root.val <= prev.v) return false
  prev.v = root.val
  return isValidBST(root.right, prev)
}

let r = null
;[5, 3, 7, 2, 4].forEach(v => r = insert(r, v))
console.log(search(r, 4)?.val, isValidBST(r)) // 4 true

平衡树的意义:若插入有序数据,BST 会退化成链表(O(n))。AVL / 红黑树通过旋转保持左右子树高度差有界,维持 O(log n)。JS 的 Map 在部分引擎底层即红黑树。

名词解释

课后练习

  1. BST 删除"双子节点"为什么用右子树最小值替身?
    • 答案:右子树最小值(最左节点)比左子树都大、比右子树其余都小,恰好满足 BST 的"左<根<右",替身后整棵树性质不变。
  2. 为什么有序插入会让 BST 退化成链表?
    • 答案:每次新值都比当前最大值大,永远只往右走,树变成一条向右的链,查找退化 O(n)。需要平衡树来救。

总结

BST 是把"二分查找"从数组搬到了树上,让"动态插入/删除"也能保持 O(log n) 的查找效率——这是它比静态排序数组强的地方。但 BST 有个致命弱点:数据有序时它会歪成链表,性能雪崩。这一课真正重要的不是 insert/remove 那几行代码,而是引出"平衡"这个贯穿数据结构的核心命题:任何依赖"树高"的复杂度,都必须防止树被拉偏。AVL、红黑树正是为解决这个问题而生,它们用旋转把树高锁在 O(log n)。理解到这一层,你再看 Map、数据库索引(B+ 树)、甚至搜索引擎的倒排结构,都会会心一笑:原来都是"在动态与有序之间找平衡"。