二叉搜索树(BST)

77 阅读7分钟

二叉搜索树(Binary Search Tree,简称 BST),也叫二叉排序树,是二叉树中最常用的类型之一,核心特点是「有序性」—— 基于节点值的大小关系组织树结构,让查找、插入、删除操作能高效执行。

一、核心定义

二叉搜索树是满足以下递归规则的二叉树:

  1. 任意节点的左子树中,所有节点的键值(数值)都 严格小于 该节点的键值;
  2. 任意节点的右子树中,所有节点的键值都 严格大于 该节点的键值;
  3. 左、右子树本身也必须是二叉搜索树;
  4. (可选)树中不允许重复键值(若需支持重复,可约定「左子树≤当前节点」或「右子树≥当前节点」,需提前明确规则)。

5 (根节点) / \ 3 7 (3<5,7>5) / \ / \ 2 4 6 8 (2<3、4>3;6<7、8>7)

二、核心特性(高频考点)

  1. 中序遍历升序:对 BST 做「左→根→右」的中序遍历,结果是严格升序的序列(无重复时)。示例中序遍历结果:2 → 3 → 4 → 5 → 6 → 7 → 8(这是 BST 最核心的特性,面试必考)。

  2. 极值位置固定

    • 整棵树的最小值:最左侧的叶子节点(示例中是 2);
    • 整棵树的最大值:最右侧的叶子节点(示例中是 8)。
  3. 无冗余节点:相同值的节点不会出现(默认规则),避免查找 / 插入歧义。

三、节点结构定义(C++ 示例)

先定义 BST 的节点结构,后续操作均基于此:

#include <iostream>
#include <vector> 
using namespace std; // 二叉搜索树节点结构 
struct TreeNode {
int val; // 节点值(键值) 
TreeNode* left; // 左子节点指针 
TreeNode* right; // 右子节点指针 
// 构造函数 
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} 
};

四、BST 核心操作(插入 / 查找 / 删除)

BST 的操作均基于「值的比较」递归 / 迭代实现,其中删除操作是难点,需分情况处理。

1. 查找操作(Search)

思路

从根节点开始,按「值的大小」逐层缩小查找范围:

  • 若目标值 = 当前节点值:找到,返回该节点;
  • 若目标值 < 当前节点值:递归 / 迭代查找左子树;
  • 若目标值 > 当前节点值:递归 / 迭代查找右子树;
  • 若子树为空:未找到,返回 nullptr
// 查找值为target的节点,返回节点指针(未找到返回nullptr) 
TreeNode* searchBST(TreeNode* root, int target) { 
TreeNode* cur = root; 
while (cur != nullptr) { 
    if (cur->val == target) {
        return cur; // 找到目标节点 } 
    else if (target < cur->val) {
     cur = cur->left; // 往左找 }
    else { 
        cur = cur->right; // 往右找
    } 
} 
return nullptr; // 未找到 
}

复杂度

  • 时间:O (h)(h 为树的高度);
  • 空间:O (1)(迭代版)/ O (h)(递归版,栈空间)。

2. 插入操作(Insert)

思路

插入的核心是「找到空的插入位置」,且不破坏 BST 规则:

  1. 若树为空:直接将新节点作为根节点;
  2. 若树非空:从根节点开始比较,小则往左、大则往右,直到找到「空的左 / 右子节点位置」,插入新节点。
// 插入值为val的节点,返回插入后的树根(处理空树情况)
TreeNode* insertIntoBST(TreeNode* root, int val) {
    // 空树:直接新建根节点
    if (root == nullptr) {
        return new TreeNode(val);
    }
    TreeNode* cur = root;
    TreeNode* parent = nullptr; // 记录当前节点的父节点
    // 找到插入位置的父节点
    while (cur != nullptr) {
        parent = cur;
        if (val < cur->val) {
            cur = cur->left;
        } else if (val > cur->val) {
            cur = cur->right;
        } else {
            // 已有相同值,按规则不插入(直接返回原树)
            return root;
        }
    }
    // 插入新节点(父节点的左/右)
    if (val < parent->val) {
        parent->left = new TreeNode(val);
    } else {
        parent->right = new TreeNode(val);
    }
    return root;
}

关键注意

插入操作不会修改已有节点的位置,仅在叶子节点的空位置添加新节点,因此不会破坏原有 BST 结构。

3. 删除操作(Delete)

删除是 BST 最复杂的操作,需根据「待删除节点的子节点数量」分 3 种情况处理:

情况处理方式
情况 1:叶子节点(无左右子节点)直接删除,将父节点指向该节点的指针置空
情况 2:单孩子节点(仅左 / 右子节点)将父节点的指针指向该节点的子节点,再删除原节点
情况 3:双孩子节点(左右子节点都有)找「中序后继」(右子树的最小节点)或「中序前驱」(左子树的最大节点),替换待删除节点的值,再删除后继 / 前驱节点(后继 / 前驱必是情况 1 或 2)
// 删除值为key的节点,返回删除后的树根
TreeNode* deleteNode(TreeNode* root, int key) {
    // 步骤1:找到待删除节点(cur)及其父节点(parent)
    TreeNode* cur = root;
    TreeNode* parent = nullptr;
    while (cur != nullptr && cur->val != key) {
        parent = cur;
        if (key < cur->val) {
            cur = cur->left;
        } else {
            cur = cur->right;
        }
    }
    if (cur == nullptr) {
        return root; // 未找到待删除节点,直接返回
    }

    // 步骤2:分情况删除节点
    // 情况1+2:无孩子 或 只有一个孩子
    if (cur->left == nullptr || cur->right == nullptr) {
        TreeNode* child = (cur->left != nullptr) ? cur->left : cur->right;
        // 若待删除节点是根节点(无父节点)
        if (parent == nullptr) {
            root = child;
        } else if (parent->left == cur) {
            parent->left = child;
        } else {
            parent->right = child;
        }
        delete cur; // 释放内存
    } 
    // 情况3:有两个孩子(找中序后继替换)
    else {
        // 找右子树的最小节点(中序后继)
        TreeNode* successor = cur->right;
        TreeNode* sucParent = cur; // 后继节点的父节点
        while (successor->left != nullptr) {
            sucParent = successor;
            successor = successor->left;
        }
        // 替换待删除节点的值为后继节点的值
        cur->val = successor->val;
        // 删除后继节点(后继必是叶子/单孩子)
        if (sucParent->left == successor) {
            sucParent->left = successor->right;
        } else {
            sucParent->right = successor->right;
        }
        delete successor;
    }
    return root;
}

五、BST 的遍历(重点:中序遍历)

遍历是验证 BST 合法性、提取有序数据的核心手段,其中中序遍历是 BST 的「专属遍历方式」。

1. 中序遍历(左→根→右)

// 中序遍历BST,结果存入res(升序)
void inorderTraversal(TreeNode* root, vector<int>& res) {
    if (root == nullptr) return;
    inorderTraversal(root->left, res);  // 左
    res.push_back(root->val);           // 根
    inorderTraversal(root->right, res); // 右
}

// 调用示例
vector<int> res;
inorderTraversal(root, res);
// res 为升序数组:[2,3,4,5,6,7,8]

2. 验证 BST 合法性

利用「中序遍历升序」的特性,验证一棵二叉树是否为 BST:


bool isValidBST(TreeNode* root) {
    vector<int> res;
    inorderTraversal(root, res);
    // 检查是否严格升序
    for (int i = 1; i < res.size(); ++i) {
        if (res[i] <= res[i-1]) {
            return false;
        }
    }
    return true;
}

六、性能分析

BST 操作的时间复杂度完全取决于树的高度 h

树的形态树高 h操作时间复杂度示例场景
平衡 BSTlog₂nO(logn)随机插入节点
退化 BST(链表)nO(n)依次插入 1→2→3→4→5(右斜链)

优化方向:平衡二叉树

为解决 BST 退化问题,衍生出「平衡二叉树」,强制树的高度保持 O (logn):

  • AVL 树:严格平衡,任意节点的左右子树高度差≤1,插入 / 删除需旋转调整;
  • 红黑树:近似平衡,通过颜色规则限制高度,插入 / 删除旋转次数更少(C++ set/map、Java TreeMap 底层实现)。

七、应用场景

  1. 动态有序集合:高效支持插入、删除、查找(替代数组,数组插入 / 删除需移动元素,时间 O (n));
  2. 范围查询:比如「找所有大于 3 且小于 7 的节点」,中序遍历到 3 开始收集,到 7 停止;
  3. 排序:中序遍历直接得到升序序列(时间 O (n),但空间复杂度高,不如快排 / 归并);
  4. 数据库索引:MySQL 索引的底层是 B+ 树(BST 的变种,更适合磁盘存储)。

八、核心总结

  1. BST 核心规则:左小、右大、子树也是 BST;
  2. 中序遍历升序是 BST 的「身份标识」,验证 / 解题必用;
  3. 插入 / 查找简单,删除需分 3 种情况(双孩子节点找后继 / 前驱是关键);
  4. 性能瓶颈在树高,平衡 BST(红黑树 / AVL)是工业级解决方案。