🌲 二叉搜索树(BST):查找效率的平衡大师!

49 阅读10分钟

"左小右大,二分查找的树形版本!" 🎯


📖 一、什么是二叉搜索树?从家族族谱说起

1.1 生活中的场景

想象你在查族谱找人:

普通链表查找:

张三 → 李四 → 王五 → 赵六 → 钱七 → ...

找"王五":
1. 从头开始
2. 逐个比较
3. 第3个才找到
时间:O(n)  😰

二叉搜索树查找:

          李四(4)
         /      \
    张三(3)      王五(5)
                /      \
            赵六(6)   钱七(7)

找"王五":
1. 从根开始:李四(4),5 > 4,往右
2. 到达王五(5),找到!
时间:O(log n)  ⚡

1.2 专业定义

二叉搜索树(Binary Search Tree, BST) 是一种特殊的二叉树,满足以下性质:

核心性质:

  • 左子树的所有节点值 < 根节点值
  • 右子树的所有节点值 > 根节点值
  • ✅ 左右子树也都是二叉搜索树(递归定义)

时间复杂度:

  • 平均:O(log n) - 查找、插入、删除
  • 最坏:O(n) - 退化成链表时

🎨 二、二叉搜索树的结构

2.1 基本结构图解

         8          ← 根节点
        / \
       3   10       ← 第2层
      / \    \
     1   6    14    ← 第3层
        / \   /
       4   7 13     ← 第4层

性质验证:
- 左子树 {1,3,4,6,7} < 8
- 右子树 {10,13,14} > 8
- 递归:3的左子树{1} < 3,右子树{4,6,7} > 3

2.2 节点定义

class TreeNode {
    int val;           // 节点值
    TreeNode left;     // 左子节点
    TreeNode right;    // 右子节点
    
    public TreeNode(int val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

2.3 错误示例(不是BST)

    ❌ 错误示例1:
         8
        / \
       3   10
      / \    
     1   12  ← 错!12 > 8,不应在左子树
     
    ❌ 错误示例2:
         8
        / \
       3   10
          /    
         5     ← 错!5 < 8,不应在右子树

🔧 三、核心操作

3.1 查找操作(Search)

算法思路:

  1. 从根节点开始
  2. 如果目标值 = 当前节点值,找到!
  3. 如果目标值 < 当前节点值,往左子树找
  4. 如果目标值 > 当前节点值,往右子树找
  5. 重复2-4,直到找到或遇到null

递归实现:

public class BinarySearchTree {
    private TreeNode root;
    
    // 查找(递归)
    public TreeNode search(int key) {
        return searchRecursive(root, key);
    }
    
    private TreeNode searchRecursive(TreeNode node, int key) {
        // 基本情况:空节点或找到
        if (node == null || node.val == key) {
            return node;
        }
        
        // 递归查找
        if (key < node.val) {
            return searchRecursive(node.left, key);  // 左子树
        } else {
            return searchRecursive(node.right, key); // 右子树
        }
    }
    
    // 查找(迭代)
    public TreeNode searchIterative(int key) {
        TreeNode current = root;
        
        while (current != null) {
            if (key == current.val) {
                return current;
            } else if (key < current.val) {
                current = current.left;
            } else {
                current = current.right;
            }
        }
        
        return null;
    }
}

查找过程演示:

查找 6:
         8
        / \
       3   10
      / \    \
     1   6    14
        / \   /
       4   7 13

步骤:
1. 从8开始:6 < 8,往左
2. 到3:6 > 3,往右
3. 到6:找到!✅

查找次数:3次(树的高度)

3.2 插入操作(Insert)

算法思路:

  1. 如果树为空,新节点成为根节点
  2. 否则,从根节点开始比较:
    • 如果新值 < 当前值,往左走
    • 如果新值 > 当前值,往右走
  3. 找到空位置,插入新节点

代码实现:

// 插入(递归)
public void insert(int val) {
    root = insertRecursive(root, val);
}

private TreeNode insertRecursive(TreeNode node, int val) {
    // 找到空位置,创建新节点
    if (node == null) {
        return new TreeNode(val);
    }
    
    // 递归插入
    if (val < node.val) {
        node.left = insertRecursive(node.left, val);
    } else if (val > node.val) {
        node.right = insertRecursive(node.right, val);
    }
    // 如果val == node.val,不插入(避免重复)
    
    return node;
}

// 插入(迭代)
public void insertIterative(int val) {
    if (root == null) {
        root = new TreeNode(val);
        return;
    }
    
    TreeNode current = root;
    TreeNode parent = null;
    
    while (current != null) {
        parent = current;
        if (val < current.val) {
            current = current.left;
        } else if (val > current.val) {
            current = current.right;
        } else {
            return;  // 已存在,不插入
        }
    }
    
    // parent是待插入位置的父节点
    if (val < parent.val) {
        parent.left = new TreeNode(val);
    } else {
        parent.right = new TreeNode(val);
    }
}

插入过程演示:

原树:
         8
        / \
       3   10

插入 6:
步骤1:6 < 8,往左
步骤2:6 > 3,往右
步骤3:右边为空,插入

结果:
         8
        / \
       3   10
        \
         6  ← 新插入

3.3 删除操作(Delete)⭐ 最复杂

删除有3种情况:

情况1:删除叶子节点(无子节点)

最简单,直接删除

删除 1:
         8              8
        / \            / \
       3   10    →    3   10
      /                \
     1                  6

情况2:删除只有一个子节点的节点

用子节点替换被删除节点

删除 10:
         8              8
        / \            / \
       3   10    →    3   14
      / \    \       / \
     1   6    14    1   6

情况3:删除有两个子节点的节点 ⭐

找到右子树的最小值(或左子树的最大值)替换

删除 3:
步骤1:找右子树最小值(4)
步骤2:用4替换3
步骤3:删除原来的4

         8              8
        / \            / \
       3   10    →    4   10
      / \    \       / \    \
     1   6    14    1   6    14
        / \              \
       4   7              7

完整代码:

// 删除节点
public void delete(int val) {
    root = deleteRecursive(root, val);
}

private TreeNode deleteRecursive(TreeNode node, int val) {
    if (node == null) {
        return null;
    }
    
    // 查找要删除的节点
    if (val < node.val) {
        node.left = deleteRecursive(node.left, val);
    } else if (val > node.val) {
        node.right = deleteRecursive(node.right, val);
    } else {
        // 找到了要删除的节点
        
        // 情况1:叶子节点或只有一个子节点
        if (node.left == null) {
            return node.right;
        } else if (node.right == null) {
            return node.left;
        }
        
        // 情况2:有两个子节点
        // 找右子树的最小值
        TreeNode minNode = findMin(node.right);
        
        // 用最小值替换当前节点的值
        node.val = minNode.val;
        
        // 删除右子树中的最小值节点
        node.right = deleteRecursive(node.right, minNode.val);
    }
    
    return node;
}

// 找到子树的最小值节点
private TreeNode findMin(TreeNode node) {
    while (node.left != null) {
        node = node.left;
    }
    return node;
}

3.4 遍历操作

中序遍历(In-Order):左 → 根 → 右

特性:BST的中序遍历结果是有序的!

         8
        / \
       3   10
      / \    \
     1   6    14
        / \   /
       4   7 13

中序遍历:1, 3, 4, 6, 7, 8, 10, 13, 14  ← 有序!

代码:

// 中序遍历(递归)
public void inorderTraversal(TreeNode node) {
    if (node == null) return;
    
    inorderTraversal(node.left);   // 左
    System.out.print(node.val + " "); // 根
    inorderTraversal(node.right);  // 右
}

// 前序遍历:根 → 左 → 右
public void preorderTraversal(TreeNode node) {
    if (node == null) return;
    
    System.out.print(node.val + " "); // 根
    preorderTraversal(node.left);  // 左
    preorderTraversal(node.right); // 右
}

// 后序遍历:左 → 右 → 根
public void postorderTraversal(TreeNode node) {
    if (node == null) return;
    
    postorderTraversal(node.left);  // 左
    postorderTraversal(node.right); // 右
    System.out.print(node.val + " ");  // 根
}

💻 四、完整BST实现

public class BinarySearchTree {
    private TreeNode root;
    
    // 节点类
    static class TreeNode {
        int val;
        TreeNode left, right;
        
        TreeNode(int val) {
            this.val = val;
        }
    }
    
    // 插入
    public void insert(int val) {
        root = insertRecursive(root, val);
    }
    
    private TreeNode insertRecursive(TreeNode node, int val) {
        if (node == null) {
            return new TreeNode(val);
        }
        
        if (val < node.val) {
            node.left = insertRecursive(node.left, val);
        } else if (val > node.val) {
            node.right = insertRecursive(node.right, val);
        }
        
        return node;
    }
    
    // 查找
    public boolean search(int val) {
        return searchRecursive(root, val) != null;
    }
    
    private TreeNode searchRecursive(TreeNode node, int val) {
        if (node == null || node.val == val) {
            return node;
        }
        
        return val < node.val ? 
            searchRecursive(node.left, val) : 
            searchRecursive(node.right, val);
    }
    
    // 删除
    public void delete(int val) {
        root = deleteRecursive(root, val);
    }
    
    private TreeNode deleteRecursive(TreeNode node, int val) {
        if (node == null) return null;
        
        if (val < node.val) {
            node.left = deleteRecursive(node.left, val);
        } else if (val > node.val) {
            node.right = deleteRecursive(node.right, val);
        } else {
            if (node.left == null) return node.right;
            if (node.right == null) return node.left;
            
            TreeNode minNode = findMin(node.right);
            node.val = minNode.val;
            node.right = deleteRecursive(node.right, minNode.val);
        }
        
        return node;
    }
    
    private TreeNode findMin(TreeNode node) {
        while (node.left != null) {
            node = node.left;
        }
        return node;
    }
    
    // 中序遍历
    public void inorder() {
        inorderRecursive(root);
        System.out.println();
    }
    
    private void inorderRecursive(TreeNode node) {
        if (node != null) {
            inorderRecursive(node.left);
            System.out.print(node.val + " ");
            inorderRecursive(node.right);
        }
    }
    
    // 获取最小值
    public int findMinValue() {
        if (root == null) throw new IllegalStateException("Tree is empty");
        return findMin(root).val;
    }
    
    // 获取最大值
    public int findMaxValue() {
        if (root == null) throw new IllegalStateException("Tree is empty");
        TreeNode node = root;
        while (node.right != null) {
            node = node.right;
        }
        return node.val;
    }
    
    // 测试
    public static void main(String[] args) {
        BinarySearchTree bst = new BinarySearchTree();
        
        // 插入数据
        int[] values = {8, 3, 10, 1, 6, 14, 4, 7, 13};
        System.out.println("=== 插入数据 ===");
        for (int val : values) {
            bst.insert(val);
            System.out.print(val + " ");
        }
        System.out.println();
        
        // 中序遍历(有序)
        System.out.println("\n=== 中序遍历(有序)===");
        bst.inorder();
        
        // 查找
        System.out.println("\n=== 查找 ===");
        System.out.println("查找6: " + bst.search(6));
        System.out.println("查找15: " + bst.search(15));
        
        // 最值
        System.out.println("\n=== 最值 ===");
        System.out.println("最小值: " + bst.findMinValue());
        System.out.println("最大值: " + bst.findMaxValue());
        
        // 删除
        System.out.println("\n=== 删除节点3 ===");
        bst.delete(3);
        bst.inorder();
    }
}

输出:

=== 插入数据 ===
8 3 10 1 6 14 4 7 13 

=== 中序遍历(有序)===
1 3 4 6 7 8 10 13 14 

=== 查找 ===
查找6: true
查找15: false

=== 最值 ===
最小值: 1
最大值: 14

=== 删除节点3 ===
1 4 6 7 8 10 13 14 

🎯 五、应用场景

5.1 有序数据存储

// 学生成绩管理
class StudentGrade {
    int score;
    String name;
    
    StudentGrade(int score, String name) {
        this.score = score;
        this.name = name;
    }
}

// 按成绩排序的BST
BinarySearchTree scoreTree = new BinarySearchTree();
scoreTree.insert(85); // 张三
scoreTree.insert(92); // 李四
scoreTree.insert(78); // 王五

// 中序遍历得到有序成绩

5.2 范围查询

// 查找[low, high]范围内的所有值
public List<Integer> rangeScan(int low, int high) {
    List<Integer> result = new ArrayList<>();
    rangeScanHelper(root, low, high, result);
    return result;
}

private void rangeScanHelper(TreeNode node, int low, int high, List<Integer> result) {
    if (node == null) return;
    
    // 如果当前值 > low,左子树可能有符合条件的
    if (node.val > low) {
        rangeScanHelper(node.left, low, high, result);
    }
    
    // 如果当前值在范围内,加入结果
    if (node.val >= low && node.val <= high) {
        result.add(node.val);
    }
    
    // 如果当前值 < high,右子树可能有符合条件的
    if (node.val < high) {
        rangeScanHelper(node.right, low, high, result);
    }
}

5.3 第K小/大元素

// 找第K小的元素
public int kthSmallest(TreeNode root, int k) {
    List<Integer> list = new ArrayList<>();
    inorderToList(root, list);
    return list.get(k - 1);
}

private void inorderToList(TreeNode node, List<Integer> list) {
    if (node == null) return;
    inorderToList(node.left, list);
    list.add(node.val);
    inorderToList(node.right, list);
}

🎓 六、经典面试题

面试题1:验证BST

LeetCode 98

public boolean isValidBST(TreeNode root) {
    return isValidBSTHelper(root, null, null);
}

private boolean isValidBSTHelper(TreeNode node, Integer min, Integer max) {
    if (node == null) return true;
    
    // 检查当前节点是否在(min, max)范围内
    if ((min != null && node.val <= min) || 
        (max != null && node.val >= max)) {
        return false;
    }
    
    // 递归检查左右子树
    return isValidBSTHelper(node.left, min, node.val) &&
           isValidBSTHelper(node.right, node.val, max);
}

面试题2:BST转有序数组

public List<Integer> bstToArray(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    inorder(root, result);
    return result;
}

private void inorder(TreeNode node, List<Integer> result) {
    if (node == null) return;
    inorder(node.left, result);
    result.add(node.val);
    inorder(node.right, result);
}

面试题3:BST的最近公共祖先

LeetCode 235

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (p.val < root.val && q.val < root.val) {
        return lowestCommonAncestor(root.left, p, q);  // 都在左边
    } else if (p.val > root.val && q.val > root.val) {
        return lowestCommonAncestor(root.right, p, q); // 都在右边
    } else {
        return root;  // 分叉点就是LCA
    }
}

面试题4:BST的优缺点?

答案:

特性BST数组链表
查找O(log n)平均O(log n)二分O(n)
插入O(log n)平均O(n)移动O(1)头插
删除O(log n)平均O(n)移动O(n)查找
有序遍历O(n)O(n)O(n log n)排序
空间O(n)O(n)O(n)

优点:

  • ✅ 查找、插入、删除都是O(log n)
  • ✅ 中序遍历得到有序序列
  • ✅ 支持范围查询

缺点:

  • ❌ 最坏情况退化成链表O(n)
  • ❌ 不平衡时性能下降
  • ❌ 需要额外的指针空间

面试题5:如何避免BST退化?

答案: 使用平衡二叉搜索树

  • AVL树:严格平衡,|左高度 - 右高度| ≤ 1
  • 红黑树:近似平衡,保证O(log n)
  • B树/B+树:多路平衡,数据库索引

🎪 七、趣味小故事

故事:图书馆的分类法

从前有个图书馆,书籍都堆在一起。

没有BST的日子:

管理员老张每天被累惨:

  • 📚 找一本书要从头翻到尾
  • 新书来了不知道放哪
  • 查找时间:O(n) 😰

引入BST后:

老张设计了一套分类法:

         [5000号书]
        /          \
    [3000]        [7000]
    /    \        /    \
 [2000] [4000] [6000] [8000]

规则:
- 编号 < 当前,放左边
- 编号 > 当前,放右边

现在找书:

找6500号书:
1. 从5000开始:6500 > 5000,往右
2. 到7000:6500 < 7000,往左
3. 到6000:找到附近!

只需3步,而不是遍历所有书!⚡

新书入库:

新书6200:
1. 从5000开始:6200 > 5000,往右
2. 到7000:6200 < 7000,往左
3. 到6000:6200 > 6000,往右
4. 右边为空,放这里!

老张笑了:"BST真是神器!查找插入都快!"

这就是BST的魔力——O(log n)的效率!🎯


📚 八、知识点总结

核心要点 ✨

  1. 定义:左小右大的二叉树
  2. 性质:中序遍历得到有序序列
  3. 操作
    • 查找:O(log n) 平均
    • 插入:O(log n) 平均
    • 删除:O(log n) 平均,3种情况
  4. 优势:查找、插入、删除都快
  5. 劣势:可能退化成链表
  6. 改进:平衡树(AVL、红黑树)

记忆口诀 🎵

二叉搜索树真巧,
左小右大要记牢。
查找插入都二分,
时间复杂对数好。
删除情况分三种,
叶子单子双子考。
中序遍历很神奇,
有序序列自然到。
退化问题要警惕,
平衡树来解烦恼!

操作总结表 📊

操作平均时间最坏时间说明
查找O(log n)O(n)二分查找
插入O(log n)O(n)找到位置插入
删除O(log n)O(n)3种情况
最小值O(log n)O(n)最左节点
最大值O(log n)O(n)最右节点
遍历O(n)O(n)访问所有节点

🌟 九、总结彩蛋

恭喜你!🎉 你已经掌握了二叉搜索树这个重要的数据结构!

记住:

  • 🌲 左小右大是核心
  • ⚡ O(log n)是优势
  • 🔄 中序遍历得有序
  • ⚠️ 退化问题要注意

最后送你一张图

         根
        /  \
       小    大
      / \  / \
    更小 小大 更大
    
   左小右大永不忘!

下次见,继续学习平衡树! 💪😄


📖 参考资料

  1. 《算法导论》第12章 - 二叉搜索树
  2. 《数据结构与算法分析》- BST
  3. LeetCode BST专题
  4. GeeksforGeeks - Binary Search Tree

作者: AI算法导师
最后更新: 2025年11月
难度等级: ⭐⭐⭐ (中级)
预计学习时间: 3-4小时

💡 温馨提示:理解BST是学习平衡树(AVL、红黑树)的基础,一定要掌握好!