二叉搜索树(BST)完全入门指南:从原理到代码,手把手带你玩转这棵“会思考”的树!

122 阅读9分钟

引言

你是否曾好奇:计算机是如何在成千上万的数据中快速找到某个数字的?
答案之一,就是——二叉搜索树(Binary Search Tree, BST)!
它结构优雅、逻辑清晰,是理解更高级数据结构(如红黑树、B树)的基石。
今天,我们将结合 readme.md 的理论说明和 1.js2.js3.js 的三段核心代码,逐行注释 + 深入讲解,带你从零构建对 BST 的完整认知。
即使你是编程新手,也能轻松看懂!


第一部分:什么是二叉搜索树?

二叉搜索树(排序二叉树)满足:左子树 < 根节点 < 右子树。
二叉搜索树支持高效查找。平均时间复杂度为 O(log n) ,最坏时间复杂度为 O(n)
二叉搜索树的定义是一棵:

  • 空树;

  • 或由根节点、左子树、右子树组成的树,同时:

    • 左子树和右子树都是二叉搜索树;
    • 左子树的所有节点的值均小于根节点的值;
    • 右子树的所有节点的值均大于根节点的值。
  1. 递归定义:一棵 BST 要么是空的,要么它的左右子树也必须是 BST。
  2. 有序性:这是 BST 的灵魂!左边小,右边大,这个规则贯穿始终。
  3. 高效性:因为有序,我们每次比较都能“砍掉一半”搜索空间,所以平均速度很快(像二分查找)。
  4. 最坏情况:如果插入顺序是单调递增或递减(比如 1→2→3→4),树会退化成一条链,此时性能退化为 O(n),和数组遍历一样慢。

类比理解:想象你在玩“猜数字”游戏(1~100)。每次你猜一个数,对方告诉你“大了”或“小了”。BST 就像这个游戏的决策树——每一步都帮你缩小范围!


第二部分:准备工具箱——定义树节点(来自 1.js

在操作 BST 之前,我们需要先定义“节点”这个基本单元。来看 1.js 开头的部分:

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

逐行解析:

  • class TreeNode:定义一个名为 TreeNode 的类,代表树中的一个节点。
  • constructor(val):构造函数,接收一个值 val
  • this.val = val;:将传入的值保存为当前节点的值。
  • this.left = null;:初始化左孩子为 null(表示暂时没有左子节点)。
  • this.right = null;:初始化右孩子为 null(表示暂时没有右子节点)。

这就是 BST 的“原子” !所有复杂的树,都是由这样的小节点通过 leftright 指针连接而成。

接着,1.js 手动构建了一棵测试用的小树:

const root = new TreeNode(6);
root.left = new TreeNode(3);
root.right = new TreeNode(8);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(4);
root.right.left = new TreeNode(7);
root.right.right = new TreeNode(9);

这棵树的结构如下:

        6
       / \
      3   8
     / \ / \
    1  4 7 9
  • 根是 6;
  • 左子树:3(<6),其下有 1(<3)和 4(>3);
  • 右子树:8(>6),其下有 7(<8)和 9(>8)。

完全符合 BST 规则!


第三部分:操作一 —— 查找(Search)【来自 1.js

原始代码

// 查找
function search(root, n) {
  if (!root) {                // 如果当前节点为空(已走到叶子的下一层),说明没找到
    return;
  }
  if (root.val === n) {       // 如果当前节点的值等于目标值,找到了!
    console.log('目标节点', root)
  } else if (root.val > n) {  // 如果当前节点的值大于目标值 → 目标一定在左子树
    search(root.left, n);     // 递归地在左子树中查找
  } else {                    // 否则(当前节点值 < 目标值)→ 目标一定在右子树
    search(root.right, n);    // 递归地在右子树中查找
  }
}

逻辑详解:

这是一个典型的递归查找过程,思路完全遵循 BST 的有序性:

  1. 边界条件:如果 rootnull,说明已经走到了树的“尽头”,但还没找到,直接返回(结束递归)。
  2. 命中:如果当前节点的值等于 n,打印出来,任务完成!
  3. 向左走:如果当前值太大(root.val > n),根据 BST 规则,目标只能在左子树
  4. 向右走:如果当前值太小(root.val < n),目标只能在右子树

举个例子:search(root, 7)

  • 当前节点:6 → 6 < 7 → 去右子树(8)
  • 当前节点:8 → 8 > 7 → 去左子树(7)
  • 当前节点:7 → 7 === 7 → 找到!输出 目标节点 TreeNode{val:7, ...}

时间复杂度:每次递归都进入下一层,最多走树的高度。

  • 平衡时:高度 ≈ log₂n → O(log n)
  • 退化成链:高度 = n → O(n)

第四部分:操作二 —— 插入(Insert)【来自 2.js

原始代码

// 插入
function insertIntoBst(root, n) {
  if (!root) {                     // 如果当前位置为空,说明找到了插入位置
    root = new TreeNode(n);        // 创建新节点
    return root;                   // 返回这个新节点,让父节点的 left/right 指向它
  }
  if (root.val > n) {              // 如果新值小于当前节点 → 应插入左子树
    root.left = insertIntoBst(root.left, n);  // 递归插入左子树,并更新左指针
  } else {                         // 否则(新值 >= 当前节点)→ 插入右子树
    root.right = insertIntoBst(root.right, n); // 递归插入右子树,并更新右指针
  }
  return root;                     // 返回当前子树的根(用于向上连接)
}

逻辑详解:

插入的本质是先查找插入位置,再创建节点

  1. 找到空位:当 root === null 时,说明我们走到了一个“可以安家”的地方,于是新建节点并返回。

  2. 决定方向

    • 如果 n < root.val → 往左走;
    • 否则(包括相等)→ 往右走(注意:这里允许重复值插入到右子树,实际应用中可根据需求修改)。
  3. 递归连接:函数返回的是“更新后的子树根”,所以要用 root.left = ...root.right = ... 把新节点“挂”回去。

举个例子:插入 5

  • 从根 6 开始:5 < 6 → 进入左子树(3)
  • 在 3:5 > 3 → 进入右子树(4)
  • 在 4:5 > 4 → 进入右子树(null
  • 遇到 null → 创建 new TreeNode(5),返回
  • 回溯:4 的 right 指向 5;3 的 right 仍是 4;6 的 left 仍是 3

最终树变成:

        6
       / \
      3   8
     / \ / \
    1  4 7 9
        \
         5   ← 新增!

✅ 依然满足 BST 规则!

💡 注意:这个插入函数不会改变已有节点的结构,只是在叶子处添加新节点,非常安全。


第五部分:操作三 —— 删除(Delete)【来自 3.js

删除是 BST 中最难的操作,因为要在删除后依然保持 BST 的有序性。我们来看看 3.js 的实现。

原始代码

// 删除
function deleteNode(root, n) {
  if (!root) {                    // 空树,直接返回(没找到要删的节点)
    return root;
  }
  if (root.val === n) {           // 找到了要删除的节点!
    if (!root.left && !root.right) {  // 情况1:叶子节点(无左右孩子)
      root = null;                // 直接删除,返回 null
    } else if (root.left) {       // 情况2:有左子树(不管有没有右子树)
      // 左子树最大的结点找出来,代替这个结点
      const maxLeft = findMax(root.left);
      root.val = maxLeft.val;     // 用左子树的最大值覆盖当前节点的值
      // 删除左子树最大的结点(它一定没有右孩子,可能有左孩子)
      root.left = deleteNode(root.left, maxLeft.val);
    } else {                      // 情况3:只有右子树(没有左子树)
      // 右子树最小的结点找出来,代替这个结点
      const minRight = findMin(root.right);
      root.val = minRight.val;    // 用右子树的最小值覆盖当前节点的值
      // 删除右子树最小的结点(它一定没有左孩子,可能有右孩子)
      root.right = deleteNode(root.right, minRight.val);
    }
  } else if (root.val > n) {      // 目标值更小 → 去左子树删
    root.left = deleteNode(root.left, n);
  } else {                        // 目标值更大 → 去右子树删
    root.right = deleteNode(root.right, n);
  }
  return root;                    // 返回更新后的子树根
}

// 寻找左子树最大值(一直往右走到底)
function findMax(root) {
  while (root.right) {            // 只要有右孩子,就继续往右
    root = root.right;
  }
  return root;                    // 最右边的节点就是最大值
}

// 寻找右子树的最小值(一直往左走到底)
function findMin(root) {
  while (root.left) {             // 只要有左孩子,就继续往左
    root = root.left;
  }
  return root;                    // 最左边的节点就是最小值
}

删除的三种情况详解

✅ 情况1:删除叶子节点(如 1、4、7、9)

  • 直接设为 null,父节点的指针自然断开。
  • 例如删 1root.left.left = null

✅ 情况2:删除有左子树的节点(如 3、6)

  • 策略:用左子树中的最大值来替代当前节点。

  • 为什么?

    • 左子树最大值 < 原根(因为是左子树);
    • 且它 > 左子树所有其他节点;
    • 替换后,整棵树依然满足 BST 规则!
  • 如何找左子树最大值?findMax(root.left):从左子树根开始,一直往右走到底。

  • 替换后怎么办? → 那个最大值节点现在“身兼两职”,需要从原位置删除。但它一定没有右孩子(否则还能往右走),所以删除它最多只涉及一个子树,递归调用 deleteNode 即可。

例子:删除 3

  • 左子树是 [1, 4],最大值是 4
  • 3 改成 4
  • 然后去左子树删掉原来的 4(它是叶子,直接删);
  • 结果:3 的位置变成 4,1 成为它的左孩子。

✅ 情况3:删除只有右子树的节点(如 8)

  • 策略:用右子树中的最小值来替代。

  • 为什么?

    • 右子树最小值 > 原根;
    • < 右子树所有其他节点;
    • 替换后依然合法!
  • 如何找?findMin(root.right):一直往左走到底。

例子:删除 8

  • 右子树是 [7,9],最小值是 7
  • 8 改成 7
  • 删掉原来的 7(叶子);
  • 结果:8 的位置变成 7,9 成为它的右孩子。

第六部分:总结与思考

📊 BST 核心操作对比

操作思路时间复杂度(平均)时间复杂度(最坏)
查找二分式递归搜索O(log n)O(n)
插入先找位置,再挂新节点O(log n)O(n)
删除分三种情况,用前驱/后继替代O(log n)O(n)

✅ BST 的优点

  • 实现简单,逻辑直观;
  • 动态支持插入/删除;
  • 中序遍历可得到有序序列(试试对示例树中序遍历:1,3,4,5,6,7,8,9)。

❌ BST 的缺点

  • 不平衡问题:最坏情况下退化成链表,性能骤降。
  • 不支持重复值的良好处理(需额外设计)。

🌟 进阶方向

为了解决不平衡问题,计算机科学家发明了自平衡二叉搜索树

  • AVL 树:严格平衡,旋转操作保证高度差 ≤1;
  • 红黑树:近似平衡,广泛用于 C++ STL(如 map)、Java TreeMap
  • Treap、Splay Tree 等。

学会 BST,你就拿到了通往这些高级结构的“门票”!


结语:动手是最好的学习!

现在,你已经掌握了 BST 的全部核心知识。不妨:

  1. 复制 1.js2.js3.js 到浏览器控制台;
  2. 构建那棵示例树;
  3. 尝试 search(root, 5)(找不到)、insertIntoBst(root, 5)、再 search(root, 5)(找到!);
  4. 尝试 deleteNode(root, 3),然后中序遍历看看结果。

编程不是看会的,是写会的!
愿你在数据结构的世界里,越走越远,越玩越嗨!🌳✨


附:完整代码链接lesson_zp/algorithm/bst: AI + 全栈学习仓库

运行它,亲眼见证 BST 的魔力吧!