C++ 二叉搜索树:从概念到实战实现,掌握高效数据检索

        二叉搜索树(Binary Search Tree,简称 BST)是一种兼具 “有序性” 和 “高效检索” 特性的二叉树结构,它通过特定的节点值分布规则,实现了插入、查找、删除操作的高效执行(最优时间复杂度 O (log₂N))。在 STL 中,map、set、multimap、multiset 等容器的底层均基于二叉搜索树的变形(平衡二叉树)实现。本文将从 “概念→性能→核心操作→实战实现” 的路径,系统讲解二叉搜索树的核心原理,帮你彻底掌握这一基础数据结构。

一、二叉搜索树的概念:有序性是核心

二叉搜索树(也称二叉排序树)是一种满足以下规则的二叉树:

  1. 若左子树非空,则左子树中所有节点的值 ≤ 根节点的值
  2. 若右子树非空,则右子树中所有节点的值 ≥ 根节点的值
  3. 左、右子树也分别是二叉搜索树(递归定义)。

关键说明:

        相等值的处理:根据场景需求,相等值可插入左子树或右子树(需保持逻辑一致)。例如 STL 的 set/map 不允许插入相等值,而 multiset/multimap 允许;

        中序遍历特性:二叉搜索树的中序遍历结果是严格递增的(若相等值插入右子树)或非递减的(若相等值插入左子树)—— 这是判断一棵二叉树是否为 BST 的重要依据。

示例(中序遍历验证):

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

中序遍历结果:1 3 4 6 7 8 10 13 14(严格递增),符合 BST 特性。

二、二叉搜索树的性能分析:最优与最差场景

二叉搜索树的性能取决于树的 “高度”—— 高度越小,操作效率越高。

2.1 最优情况:接近完全二叉树

当插入的节点值随机时,BST 会形成接近完全二叉树的结构,树的高度为 log₂N(N 为节点总数)。此时插入、查找、删除操作的时间复杂度均为 O(log₂N),与二分查找效率相当。

2.2 最差情况:退化为单支树

当插入的节点值有序(如递增或递减)时,BST 会退化为 “单支树”(类似链表),树的高度为 N。此时所有操作的时间复杂度退化为 O(N),效率极低。

示例(有序插入退化为单支树):插入序列 1,2,3,4,5,BST 退化为右单支树:

1
 \
  2
   \
    3
     \
      4
       \
        5

2.3 BST vs 二分查找:各有优劣

二分查找虽能实现 O (log₂N) 的查找效率,但存在明显缺陷:

  1. 依赖支持随机访问的结构(如数组),且数据必须有序;
  2. 插入 / 删除效率低(需挪动大量元素,时间复杂度 O (N))。

BST 的优势在于:

        无需依赖连续空间,插入 / 删除无需挪动元素;

        兼顾有序性与动态操作效率(最优场景下)。

注意:普通 BST 的最差性能无法满足工业需求,因此实际应用中会使用其变形 —— 平衡二叉树(如 AVL 树、红黑树),通过自平衡机制保证树的高度始终为 O (log₂N)。

三、二叉搜索树的核心操作:插入、查找、删除

3.1 插入操作:按规则找到空位

插入的核心是 “遵循 BST 规则,找到合适的空位插入新节点”,步骤如下:

  1. 若树为空,直接创建根节点;
  2. 若树非空,从根节点开始比较:
    • 插入值 > 当前节点值:向右子树移动;
    • 插入值 < 当前节点值:向左子树移动;
    • 插入值 == 当前节点值:根据需求决定是否插入(本文默认不允许重复,直接返回 false);
  3. 找到空位(当前节点为 nullptr),创建新节点,并让父节点的左 / 右指针指向新节点。
代码实现(插入):
template <class K>
struct BSTNode {
    K _key;
    BSTNode<K>* _left;
    BSTNode<K>* _right;

    BSTNode(const K& key) : _key(key), _left(nullptr), _right(nullptr) {}
};

template <class K>
class BSTree {
    typedef BSTNode<K> Node;
public:
    bool Insert(const K& key) {
        // 情况1:树为空,创建根节点
        if (_root == nullptr) {
            _root = new Node(key);
            return true;
        }

        // 情况2:树非空,找到插入位置
        Node* parent = nullptr;
        Node* cur = _root;
        while (cur) {
            if (cur->_key < key) {
                parent = cur;
                cur = cur->_right;
            } else if (cur->_key > key) {
                parent = cur;
                cur = cur->_left;
            } else {
                // 不允许重复插入,返回false
                return false;
            }
        }

        // 创建新节点,链接到父节点
        cur = new Node(key);
        if (parent->_key < key) {
            parent->_right = cur;
        } else {
            parent->_left = cur;
        }
        return true;
    }

private:
    Node* _root = nullptr;
};

3.2 查找操作:按规则遍历树

查找的核心是 “根据 BST 的有序性,从根节点开始定向查找”,步骤如下:

  1. 从根节点开始比较;
  2. 查找值 > 当前节点值:向右子树移动;
  3. 查找值 < 当前节点值:向左子树移动;
  4. 查找值 == 当前节点值:返回 true(找到);
  5. 遍历到 nullptr 仍未找到:返回 false。
代码实现(查找):
bool Find(const K& key) {
    Node* cur = _root;
    while (cur) {
        if (cur->_key < key) {
            cur = cur->_right;
        } else if (cur->_key > key) {
            cur = cur->_left;
        } else {
            // 找到目标值
            return true;
        }
    }
    // 遍历到空,未找到
    return false;
}
特殊场景:支持重复值的查找

若 BST 允许插入重复值(如 multiset),查找时需返回 “中序遍历的第一个目标值”(确保有序性)。例如查找值 3,需返回左子树中最靠右的 3:

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

查找 3 时,需返回 1 的右孩子(中序第一个 3)。

3.3 删除操作:分四种情况处理

删除是 BST 最复杂的操作,需保证删除节点后,树仍满足 BST 规则。根据删除节点(N)的子节点数量,分为四种情况:

情况 1:N 的左、右子树均为空(叶子节点)

        处理方式:直接删除 N,让 N 的父节点对应指针指向 nullptr。

        示例:删除节点 1(叶子节点),父节点 3 的左指针改为 nullptr。

情况 2:N 的左子树为空,右子树非空

        处理方式:让 N 的父节点对应指针指向 N 的右子树,直接删除 N。

        示例:删除节点 10(右子树为 14),父节点 8 的右指针指向 14,删除 10。

情况 3:N 的右子树为空,左子树非空

        处理方式:让 N 的父节点对应指针指向 N 的左子树,直接删除 N。

        示例:删除节点 14(左子树为 13),父节点 10 的右指针指向 13,删除 14。

情况 4:N 的左、右子树均非空(核心难点)

        问题:直接删除 N 会导致左、右子树无处安放,破坏 BST 结构;

        解决方案:替换法删除—— 找到 N 的 “替代节点”,用替代节点的值覆盖 N 的值,再删除替代节点(替代节点必为情况 2 或 3,可直接删除)。

        替代节点的选择(二选一):
                N 左子树的最大值节点:左子树中最靠右的节点(如删除 3,左子树最大值为 1 的右孩子 3);
                N 右子树的最小值节点:右子树中最靠左的节点(如删除 8,右子树最小值为 10 的左孩子 13)。

代码实现(删除):
bool Erase(const K& key) {
    Node* parent = nullptr;
    Node* cur = _root;

    // 步骤1:找到要删除的节点cur
    while (cur) {
        if (cur->_key < key) {
            parent = cur;
            cur = cur->_right;
        } else if (cur->_key > key) {
            parent = cur;
            cur = cur->_left;
        } else {
            // 步骤2:分情况删除cur
            // 情况2:左子树为空,右子树继位
            if (cur->_left == nullptr) {
                if (parent == nullptr) {
                    // 特殊情况:删除根节点且左子树为空
                    _root = cur->_right;
                } else {
                    if (parent->_left == cur) {
                        parent->_left = cur->_right;
                    } else {
                        parent->_right = cur->_right;
                    }
                }
                delete cur;
                return true;
            }
            // 情况3:右子树为空,左子树继位
            else if (cur->_right == nullptr) {
                if (parent == nullptr) {
                    // 特殊情况:删除根节点且右子树为空
                    _root = cur->_left;
                } else {
                    if (parent->_left == cur) {
                        parent->_left = cur->_left;
                    } else {
                        parent->_right = cur->_left;
                    }
                }
                delete cur;
                return true;
            }
            // 情况4:左右子树均非空,替换法删除(选右子树最小值)
            else {
                // 找到右子树的最小值节点(最靠左的节点)
                Node* rightMinP = cur; // 替代节点的父节点
                Node* rightMin = cur->_right;
                while (rightMin->_left) {
                    rightMinP = rightMin;
                    rightMin = rightMin->_left;
                }

                // 用替代节点的值覆盖cur的值
                cur->_key = rightMin->_key;

                // 删除替代节点(rightMin必为情况2或3)
                if (rightMinP->_left == rightMin) {
                    rightMinP->_left = rightMin->_right;
                } else {
                    rightMinP->_right = rightMin->_right;
                }
                delete rightMin;
                return true;
            }
        }
    }

    // 未找到要删除的节点
    return false;
}

四、二叉搜索树的两种应用场景:Key 与 Key-Value

BST 根据存储数据的类型,分为 “仅存储 Key” 和 “存储 Key-Value 键值对” 两种场景,分别对应不同的业务需求。

4.1 Key 场景:仅需判断 “存在性”

核心需求:仅需存储关键码(Key),判断某个 Key 是否存在(无关联值)。特点:不支持修改 Key(修改会破坏 BST 结构),仅支持增、删、查。

典型场景:
  1. 小区车库车牌验证:将业主车牌作为 Key 存入 BST,车辆进入时扫描车牌,查找是否存在(存在则抬杆);
  2. 单词拼写检查:将词库单词作为 Key 存入 BST,读取文章单词时查找是否存在(不存在则标红)。

4.2 Key-Value 场景:需关联 “数据与值”

核心需求:存储 Key 与关联值 Value(如英文单词 - 中文释义、车牌 - 入场时间),通过 Key 快速查找 Value。特点:不支持修改 Key(破坏结构),但支持修改 Value;增、删、查均以 Key 为依据。

典型场景:
  1. 中英字典:Key 为英文单词,Value 为中文释义,输入英文可快速查找中文;
  2. 停车费计算:Key 为车牌,Value 为入场时间,出场时查找 Value,计算停车时长与费用;
  3. 单词频次统计:Key 为单词,Value 为出现次数,读取单词时若存在则 Value++,否则插入 <单词,1>。
代码实现(Key-Value 版 BST):
template <class K, class V>
struct BSTNode {
    K _key;
    V _value;
    BSTNode<K, V>* _left;
    BSTNode<K, V>* _right;

    BSTNode(const K& key, const V& value) 
        : _key(key), _value(value), _left(nullptr), _right(nullptr) {}
};

template <class K, class V>
class BSTree {
    typedef BSTNode<K, V> Node;
public:
    // 插入Key-Value
    bool Insert(const K& key, const V& value) {
        if (_root == nullptr) {
            _root = new Node(key, value);
            return true;
        }

        Node* parent = nullptr;
        Node* cur = _root;
        while (cur) {
            if (cur->_key < key) {
                parent = cur;
                cur = cur->_right;
            } else if (cur->_key > key) {
                parent = cur;
                cur = cur->_left;
            } else {
                // 不允许重复Key
                return false;
            }
        }

        cur = new Node(key, value);
        if (parent->_key < key) {
            parent->_right = cur;
        } else {
            parent->_left = cur;
        }
        return true;
    }

    // 查找Key对应的节点(返回节点指针,可修改Value)
    Node* Find(const K& key) {
        Node* cur = _root;
        while (cur) {
            if (cur->_key < key) {
                cur = cur->_right;
            } else if (cur->_key > key) {
                cur = cur->_left;
            } else {
                return cur;
            }
        }
        return nullptr;
    }

    // 中序遍历(输出Key-Value,验证有序性)
    void InOrder() {
        _InOrder(_root);
        cout << endl;
    }

private:
    void _InOrder(Node* root) {
        if (root == nullptr) return;
        _InOrder(root->_left);
        cout << root->_key << ":" << root->_value << " ";
        _InOrder(root->_right);
    }

private:
    Node* _root = nullptr;
};
实战示例:单词频次统计
int main() {
    string arr[] = {"苹果", "西瓜", "苹果", "西瓜", "苹果", "香蕉"};
    BSTree<string, int> countTree;

    for (const auto& str : arr) {
        Node* ret = countTree.Find(str);
        if (ret == nullptr) {
            // 第一次出现,插入<单词, 1>
            countTree.Insert(str, 1);
        } else {
            // 已出现,频次+1
            ret->_value++;
        }
    }

    // 中序遍历输出(有序且带频次)
    countTree.InOrder(); // 输出:苹果:3 西瓜:2 香蕉:1
    return 0;
}

五、总结

二叉搜索树是一种基于 “有序性” 设计的基础数据结构,其核心要点如下:

  1. 概念:左子树值≤根≤右子树,中序遍历有序;
  2. 性能:最优 O (log₂N)(接近完全二叉树),最差 O (N)(单支树);
  3. 操作
    • 插入:按规则找空位,链接新节点;
    • 查找:按规则定向遍历,判断存在性;
    • 删除:分四种情况,左右子树均非空时用替代法;
  4. 场景:Key 场景(判断存在)、Key-Value 场景(关联数据)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值