二叉搜索树(BST):会 “自动排序” 的二叉树,查询快如闪电(2)

如果你已经懂了二叉树的基础遍历,可能会问:“二叉树除了遍历,还能解决什么实际问题?”—— 答案是 “高效的排序和查询”。比如要从 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” 为例,步骤如下:

  1. 从根 5 开始:6>5→只看右子树(排除左子树 2、3、4,一半数据没了);

  2. 到右子树的根 7:6<7→只看左子树(排除右子树 8,又排除一半);

  3. 到 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” 为例)

  1. 从根 5 开始:9>5→查右子树;

  2. 到 7:9>7→查右子树;

  3. 到 8:9>8→查右子树(8 的右子树为空);

  4. 把 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”(叶子节点):

  1. 找到 2(3 的左孩子);

  2. 把 3 的 left 设为 null,删除完成。

情况 2:删除的是 “只有一个孩子的节点”

逻辑:用孩子节点替代被删除节点的位置(“子承父业”)。

示例:删除 BST 中的 “3”(只有右孩子 4):

  1. 找到 3(5 的左孩子,右孩子是 4);

  2. 把 5 的 left 设为 4(用 4 替代 3 的位置),删除完成。

情况 3:删除的是 “有两个孩子的节点”(最复杂)

逻辑:用 “右子树的最小节点”(或左子树的最大节点)替代被删除节点,再删除那个最小节点。

原因:右子树的最小节点是 “比被删除节点大的最小数”,替代后能维持左小右大规则。

示例:删除 BST 中的 “5”(有左孩子 3、右孩子 7):

  1. 找到 5 的右子树(根 7),找右子树的最小节点(6,7 的左孩子);

  2. 用 6 的 val 替代 5 的 val(把 5 变成 6);

  3. 删除右子树中的 6(6 是叶子节点,按情况 1 处理);

  4. 删除后 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:根是 1;

  2. 插入 2:2>1→作为 1 的右孩子;

  3. 插入 3:3>2→作为 2 的右孩子;

  4. 插入 4:4>3→作为 3 的右孩子;

  5. 插入 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 的核心总结

  1. BST 的核心是 “左小右大”:这条规则让它天生会查询(O (logn))、会排序(中序遍历升序);

  2. 操作围绕 “维持规则”:插入找空位置,删除分 3 种情况,核心是不破坏左小右大;

  3. 致命缺陷是 “失衡退化”:有序插入会变成链表,效率暴跌 —— 这就是下一篇要讲的 “红黑树” 需要解决的问题。

下一篇咱们会学习 “红黑树”:它是 BST 的 “平衡版”,能自动修复失衡,让查询、插入、删除效率始终稳定在 O (logn),是工业级项目的首选。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容