数据结构 -- 二叉搜索树

二叉搜索树(Binary Search Tree,简称 BST)是计算机科学中最基础也最重要的数据结构之一。它结合了数组的快速查找和链表的高效插入删除特性,是红黑树、AVL 树、B + 树等几乎所有高级树结构的理论基础。无论是语言标准库的容器实现,还是数据库索引的设计,都能看到二叉搜索树的核心思想。对于准备面试的开发者来说,二叉搜索树及其变种更是必考的核心知识点。

一、什么是二叉搜索树?

1.1 定义与核心性质

二叉搜索树是一种特殊的二叉树,它满足以下三个核心性质:

  1. 对于任意节点,其左子树中所有节点的值都小于该节点的值
  2. 对于任意节点,其右子树中所有节点的值都大于该节点的值
  3. 左子树和右子树本身也都是二叉搜索树

这三条性质是二叉搜索树的灵魂,所有操作和特性都建立在它们之上。

1.2 最核心的特性:中序遍历有序

二叉搜索树最独特也最有用的特性是:对二叉搜索树进行中序遍历(左 - 根 - 右),得到的结果是一个严格递增的有序序列

例如,对于下面这棵二叉搜索树:

bash 复制代码
        5
       / \
      3   7
     / \   \
    2   4   8

其中序遍历结果为:2, 3, 4, 5, 7, 8,完美有序。这个特性让二叉搜索树天然适合处理有序数据的各种操作。

1.3 节点结构定义(C++)

二叉搜索树的每个节点包含一个值和两个指向子节点的指针:

cpp 复制代码
struct TreeNode {
    int val;         // 节点存储的值
    TreeNode* left;  // 左子节点指针
    TreeNode* right; // 右子节点指针
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

二、二叉搜索树的基本操作

二叉搜索树的所有操作都遵循其核心性质,通过比较节点值来决定向左还是向右子树移动。

2.1 查找操作

查找是二叉搜索树最基本的操作,平均时间复杂度为 O (log n)。

算法思路

  1. 从根节点开始,将目标值与当前节点值比较
  2. 如果相等,找到目标节点,返回
  3. 如果目标值小于当前节点值,递归 / 迭代查找左子树
  4. 如果目标值大于当前节点值,递归 / 迭代查找右子树
  5. 如果遍历到空节点仍未找到,返回 nullptr

迭代实现(推荐,无递归栈溢出风险)

cpp 复制代码
TreeNode* searchBST(TreeNode* root, int val) {
    while (root != nullptr && root->val != val) {
        if (val < root->val) {
            root = root->left;  // 目标值更小,去左子树找
        } else {
            root = root->right; // 目标值更大,去右子树找
        }
    }
    return root;
}

扩展操作:查找最小 / 最大节点

  • 最小节点:一直向左走,直到左子节点为空
  • 最大节点:一直向右走,直到右子节点为空

2.2 插入操作

插入操作的核心是找到合适的叶子节点位置,保证插入后仍然满足二叉搜索树的性质。

算法思路

  1. 如果树为空,直接创建新节点作为根节点
  2. 从根节点开始,将待插入值与当前节点值比较
  3. 如果待插入值小于当前节点值,且左子节点为空,则插入到左子节点
  4. 如果待插入值大于当前节点值,且右子节点为空,则插入到右子节点
  5. 否则,递归 / 迭代继续向下查找合适的位置

迭代实现

cpp 复制代码
TreeNode* insertIntoBST(TreeNode* root, int val) {
    if (root == nullptr) {
        return new TreeNode(val);
    }
    
    TreeNode* curr = root;
    while (true) {
        if (val < curr->val) {
            if (curr->left == nullptr) {
                curr->left = new TreeNode(val);
                break;
            }
            curr = curr->left;
        } else {
            if (curr->right == nullptr) {
                curr->right = new TreeNode(val);
                break;
            }
            curr = curr->right;
        }
    }
    return root;
}

2.3 删除操作(重点难点)

删除是二叉搜索树最复杂的操作,需要根据被删除节点的子节点数量分三种情况处理:

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

最简单的情况,直接将父节点指向该节点的指针置为 nullptr 即可。

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

将父节点指向该节点的指针,直接指向它的唯一子节点,相当于用子节点替换自己。

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

这是最复杂的情况,不能直接删除,否则会导致两棵子树失去父节点。标准解法是后继替换法

  1. 找到被删除节点的后继节点(右子树中的最小节点,即右子树中最左边的节点)
  2. 将后继节点的值复制到被删除节点
  3. 删除后继节点(后继节点最多只有一个右子节点,属于情况 1 或情况 2)

也可以使用前驱替换法(左子树中的最大节点),效果完全相同。

完整删除实现

cpp 复制代码
TreeNode* deleteNode(TreeNode* root, int key) {
    if (root == nullptr) return nullptr;
    
    if (key < root->val) {
        root->left = deleteNode(root->left, key);
    } else if (key > root->val) {
        root->right = deleteNode(root->right, key);
    } else {
        // 找到要删除的节点
        if (root->left == nullptr) {
            // 情况1和情况2:无左子节点或只有右子节点
            TreeNode* temp = root->right;
            delete root;
            return temp;
        } else if (root->right == nullptr) {
            // 情况2:只有左子节点
            TreeNode* temp = root->left;
            delete root;
            return temp;
        } else {
            // 情况3:有两个子节点,找后继节点
            TreeNode* successor = root->right;
            while (successor->left != nullptr) {
                successor = successor->left;
            }
            // 复制后继节点的值
            root->val = successor->val;
            // 删除后继节点
            root->right = deleteNode(root->right, successor->val);
        }
    }
    return root;
}

三、二叉搜索树的遍历方式

二叉搜索树的遍历分为深度优先遍历(DFS)和广度优先遍历(BFS)两大类,其中深度优先遍历又分为前序、中序和后序三种。

3.1 深度优先遍历

表格

遍历方式 顺序 典型应用
前序遍历 根 - 左 - 右 复制树、前缀表达式
中序遍历 左 - 根 - 右 二叉搜索树的有序输出(核心)
后序遍历 左 - 右 - 根 删除树、后缀表达式

中序遍历迭代实现(面试高频)

cpp 复制代码
vector<int> inorderTraversal(TreeNode* root) {
    vector<int> res;
    stack<TreeNode*> st;
    TreeNode* curr = root;
    
    while (curr != nullptr || !st.empty()) {
        // 一直向左走,把所有左节点入栈
        while (curr != nullptr) {
            st.push(curr);
            curr = curr->left;
        }
        // 弹出栈顶节点(当前最左节点)
        curr = st.top();
        st.pop();
        res.push_back(curr->val);
        // 处理右子树
        curr = curr->right;
    }
    return res;
}

3.2 广度优先遍历(层序遍历)

按层次从上到下、从左到右遍历节点,使用队列实现:

cpp 复制代码
vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> res;
    if (root == nullptr) return res;
    
    queue<TreeNode*> q;
    q.push(root);
    
    while (!q.empty()) {
        int size = q.size();
        vector<int> level;
        for (int i = 0; i < size; i++) {
            TreeNode* curr = q.front();
            q.pop();
            level.push_back(curr->val);
            if (curr->left != nullptr) q.push(curr->left);
            if (curr->right != nullptr) q.push(curr->right);
        }
        res.push_back(level);
    }
    return res;
}

四、时间复杂度与空间复杂度分析

二叉搜索树的性能高度依赖于树的平衡性,即左右子树的高度差。

4.1 时间复杂度

表格

操作 最好情况(完全平衡) 平均情况 最坏情况(退化成链表)
查找 O(log n) O(log n) O(n)
插入 O(log n) O(log n) O(n)
删除 O(log n) O(log n) O(n)
  • 最好情况:树是完全平衡的,高度为 log₂n,所有操作都只需要遍历树的高度
  • 最坏情况:当按升序或降序插入元素时,二叉搜索树会退化成单链表,此时所有操作都需要遍历整个链表
  • 平均情况:随机插入数据时,树的高度约为 1.39log₂n,性能接近最好情况

4.2 空间复杂度

  • 递归实现:O (h),h 为树的高度,主要是递归调用栈的开销
  • 迭代实现:O (h),主要是栈或队列的开销
  • 最坏情况(退化成链表):O (n)
  • 最好情况(完全平衡):O (log n)

五、二叉搜索树的核心缺陷与改进方向

5.1 致命缺陷:退化问题

二叉搜索树最大的问题是无法保证自平衡。当插入的数据已经有序或接近有序时,树会退化成单链表,性能急剧下降到 O (n),完全失去了树结构的优势。

例如,按顺序插入 1,2,3,4,5,得到的树是:

bash 复制代码
1
 \
  2
   \
    3
     \
      4
       \
        5

这本质上就是一个链表,查找 5 需要遍历所有 5 个节点。

5.2 改进方向:自平衡二叉搜索树

为了解决退化问题,计算机科学家发明了各种自平衡二叉搜索树,它们在插入和删除操作时会自动调整树的结构,保证树的高度始终保持在 O (log n) 级别。

常见的自平衡二叉搜索树:

  1. AVL 树:最早的自平衡二叉树,要求左右子树的高度差不超过 1,严格平衡。插入删除时通过旋转操作维持平衡。
  2. 红黑树:目前应用最广泛的自平衡二叉树,通过颜色标记和旋转操作维持近似平衡。虽然平衡度不如 AVL 树,但插入删除的平均性能更好,是 C++ std::set/std::map、Java TreeMap 的底层实现。
  3. B/B + 树:多叉平衡树,每个节点可以有多个子节点,大大降低了树的高度,特别适合磁盘存储,是数据库和文件系统索引的标准实现。

六、实际应用场景

  1. 语言标准库容器

    • C++ STL 中的std::set(有序集合)和std::map(有序键值对)底层都是红黑树
    • Java 中的TreeMapTreeSet也是基于红黑树实现
    • 这些容器提供了 O (log n) 时间复杂度的插入、删除和查找操作
  2. 数据库索引

    • 几乎所有关系型数据库的索引都使用 B + 树实现
    • B + 树是二叉搜索树的多叉扩展,具有更低的高度和更好的磁盘 IO 性能
    • 利用二叉搜索树的有序性,B + 树可以高效支持范围查询和排序操作
  3. 有序数据处理

    • 快速查找最大值、最小值
    • 范围查询(如查找 100 到 200 之间的所有元素)
    • 查找前驱节点和后继节点
    • 实现排序算法(中序遍历即可得到有序序列)
  4. 其他应用

    • 表达式树:用于解析和计算算术表达式
    • 哈夫曼树:用于数据压缩
    • 搜索引擎中的倒排索引:部分实现基于二叉搜索树的变种

七、高频面试考点与易错点

  1. 如何正确判断一棵二叉树是二叉搜索树?

    • ❌ 错误做法:只比较当前节点和左右子节点的值
    • ✅ 正确做法:递归时传递上下界,保证左子树所有节点都小于当前节点,右子树所有节点都大于当前节点
    • ✅ 另一种方法:中序遍历,检查结果是否严格递增
  2. 二叉搜索树的第 k 大 / 小元素

    • 核心思路:利用中序遍历的有序性,第 k 小元素就是中序遍历的第 k 个节点
    • 优化:不需要遍历整个树,找到第 k 个节点后立即返回
  3. 二叉搜索树与双向链表的转换

    • 核心思路:中序遍历,将左指针指向前驱,右指针指向后继
  4. 二叉搜索树的最近公共祖先

    • 利用二叉搜索树的性质:如果两个节点的值都小于当前节点,公共祖先在左子树;如果都大于当前节点,公共祖先在右子树;否则当前节点就是公共祖先
  5. 为什么删除有两个子节点的节点要用后继替换?

    • 因为后继节点是右子树中最小的节点,它的值大于左子树所有节点,小于右子树其他节点,替换后仍然满足二叉搜索树的性质
    • 而且后继节点最多只有一个右子节点,删除起来非常简单

八、总结

二叉搜索树是数据结构与算法学习中的第一个里程碑。它的核心思想 ------通过比较值来划分搜索空间,不仅适用于树结构,更是二分查找、分治算法等众多高级算法的基础。

虽然原始的二叉搜索树存在退化的缺陷,但它的变种 ------ 红黑树、B + 树等,已经成为现代计算机系统中不可或缺的核心组件。深入理解二叉搜索树的原理和操作,不仅能帮助你轻松应对面试中的算法题,更能让你在面对复杂系统设计时,拥有更扎实的底层基础。

相关推荐
玖釉-3 小时前
下一个排列:从字典序到原地算法的完整推导
数据结构·c++·windows·算法
枕星而眠3 小时前
数据结构八大排序详解(一):四大简单排序
c语言·数据结构·c++·后端
过期动态4 小时前
【LeetCode 热题 100】移动零
java·数据结构·算法·leetcode·职场和发展·rabbitmq
努力努力再努力wz4 小时前
【Qt入门系列】:按钮组件全解析:从 QAbstractButton 到快捷键事件、单选与复选机制
c语言·开发语言·数据结构·c++·git·qt·github
Dlrb12114 小时前
数据结构-栈
数据结构··内核栈·满栈空栈·增栈减栈
菜菜的顾清寒6 小时前
力扣HOT100(32)二叉树的中序遍历
数据结构·算法·leetcode
Shan12057 小时前
线段树入门:更新数组后处理求和查询
数据结构·算法
澈2078 小时前
图论天花板:Dijkstra最短路径算法详解
数据结构·算法·图论
AllData公司负责人9 小时前
亲测丝滑,体验跃迁|AllData通过集成开源项目DataVines,一站式解决数据质量难题
java·大数据·数据结构·数据库·人工智能·算法·云原生
CQU_JIAKE9 小时前
5.25【A】
java·数据结构·算法