二叉搜索树详解:概念、增删查与 key/value 应用

二叉搜索树详解:概念、增删查与 key/value 应用(C++ 实现)

1. 引言:为什么需要二叉搜索树

先看一组朴素的数据结构对比:

  • 有序数组:二分查找很快,O(log₂N)。但插入、删除需要搬移大量数据,效率极低。
  • 链表:插入、删除很快,但查找要 O(N),无法使用"二分"思想。

有没有一种结构,既能高效查找,又能高效插入删除

二叉搜索树(Binary Search Tree,BST)应运而生。它把"二分思想"迁移到链式结构上,是后续平衡树(AVL、红黑树)以及 STL 中 map / set / multimap / multiset 的底层基础。

2. 二叉搜索树的概念与性质

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有结点的值都 ≤ 根结点的值
  • 若它的右子树不为空,则右子树上所有结点的值都 ≥ 根结点的值
  • 它的左右子树也分别为二叉搜索树

由性质可知:对二叉搜索树进行中序遍历,得到的一定是有序序列,所以它也常被叫做"二叉排序树"。

关于"相等值":二叉搜索树中可以支持插入相等的值,也可以不支持,具体看使用场景定义。这正是 STL 四兄弟的差异所在:

容器是否允许重复 key
map / set不允许,去重
multimap / multiset允许,不去重
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

按这个序列构建出的二叉搜索树如下(中序遍历结果为 1 3 4 6 7 8 10 13 14,恰好有序):

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

3. 性能分析

查找、插入、删除的效率都取决于树的高度

  • 最优情况:树为完全二叉树(或接近完全二叉树),高度为 log₂N,此时增删查效率为 O(log₂N)
  • 最差情况:树退化成单支树(或类似单支),高度为 N,此时增删查退化为 O(N)
最优:完全二叉树                最差:单支树
       8                          8
      / \                          \
     3   10                        10
    / \    \                         \
   1   6    14                       14
  ...

因此二叉搜索树增删查改的综合时间复杂度为 O(N)。这样的效率显然无法满足海量数据存储搜索的需求,所以后续课程还会继续讲解二叉搜索树的变形 —— 平衡二叉搜索树(AVL 树)和红黑树,才能可靠地用于在内存中存储和搜索数据。

顺便聊聊二分查找的缺陷:二分查找虽然也能达到 O(log₂N) 的查找效率,但它有两个致命缺陷:

  1. 需要存储在支持下标随机访问的结构中,并且数据有序;
  2. 插入和删除效率很低,因为在这种结构中增删一般需要挪动数据。

平衡二叉搜索树既保留了二分查找的效率,又解决了插入删除低效的问题,这正是它的价值所在。

4. 查找

查找过程非常直观:

  1. 从根结点开始比较,查找 x:
    • x 比根的值大,则往右边走;
    • x 比根的值小,则往左边走。
  2. 最多查找高度次;走到空还没找到,说明该值不存在。
  3. 若不支持插入相等值,找到 x 即可返回。
  4. 若支持插入相等值,意味着可能有多个 x,一般要求查找中序遍历的第一个 x。例如下图查找 3,要返回左子树中那一个 3(而非更早到达的 3)。

5. 插入

插入的具体过程:

  1. 树为空:直接新增结点,赋值给 root 指针。
  2. 树不为空:按二叉搜索树性质走 —— 插入值比当前结点大往右走,小往左走,找到空位置插入新结点。
  3. 支持相等值的情况:插入值跟当前结点相等时,可以往右走也可以往左走(要保持逻辑一致,不要一会往右一会往左)。

{8, 3, 1, 10, 6, 4, 7, 14, 13} 为例,一步步构建(最终形态见第 2 节):
8 → 3(左) → 1(3左) → 10(8右) → 6(3右) → 4(6左) → 7(6右) → 14(10右) → 13(14左)

6. 删除(难点)

删除是二叉搜索树最复杂的操作。首先查找元素是否在树中,不存在则返回 false;若存在,假设要删除的结点为 N,分四种情况处理:

情况描述
1N 的左右孩子均为空
2N 的左孩子为空,右孩子不为空
3N 的右孩子为空,左孩子不为空
4N 的左右孩子均不为空

对应解决方案:

  1. 情况 1:把 N 的父亲对应孩子指针置空,直接删除 N(实际可当作情况 2 或 3 处理,效果一样)。
  2. 情况 2:把 N 的父亲对应孩子指针指向 N 的右孩子,直接删除 N。
  3. 情况 3:把 N 的父亲对应孩子指针指向 N 的左孩子,直接删除 N。
  4. 情况 4:无法直接删除 N,因为它的两个儿子无处安放,只能使用替换法
    • N 左子树的最大结点(最右结点) N 右子树的最小结点(最左结点)R 替代 N —— 这两个结点中的任意一个放到 N 的位置,都仍满足二叉搜索树规则;
    • "替代"即交换 N 与 R 的值,转而删除 R 结点;
    • 由于 R 必然符合情况 2 或情况 3,可以"直接删除"。

易错点提示:当右子树的根结点本身就是右子树最小结点时(例如删除根结点 8,其右子树最小结点就是 10,此时 rightMinP 必须初始化为 cur),处理时要格外小心,否则会出现指针悬空报错。代码注释里也专门标注了这一点。

7. 完整代码实现(key 版)

template<class K>
struct BSTNode
{
    K _key;
    BSTNode<K>* _left;
    BSTNode<K>* _right;
    BSTNode(const K& key)
        : _key(key)
        , _left(nullptr)
        , _right(nullptr)
    {}
};

// Binary Search Tree
template<class K>
class BSTree
{
    typedef BSTNode<K> Node;
public:
    bool Insert(const K& key)
    {
        if (_root == nullptr)
        {
            _root = new Node(key);
            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
            {
                return false;   // 默认不支持插入相等值
            }
        }

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

    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;
    }

    bool Erase(const K& key)
    {
        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
            {
                // 0~1 个孩子的情况:情况 1/2/3 均可直接删除,改变父亲对应孩子指针指向即可
                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;
                }
                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;
                }
                else
                {
                    // 两个孩子的情况:替换法删除
                    // 这里取右子树的最小结点作为替代结点
                    // 注意:右子树根就是最小结点的情况,rightMinP 一定要初始化为 cur
                    Node* rightMinP = cur;
                    Node* rightMin = cur->_right;
                    while (rightMin->_left)
                    {
                        rightMinP = rightMin;
                        rightMin = rightMin->_left;
                    }

                    cur->_key = rightMin->_key;
                    if (rightMinP->_left == rightMin)
                        rightMinP->_left = rightMin->_right;
                    else
                        rightMinP->_right = rightMin->_right;

                    delete rightMin;
                    return true;
                }
            }
        }
        return false;
    }

    void InOrder()
    {
        _InOrder(_root);
        cout << endl;
    }

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

private:
    Node* _root = nullptr;
};

提示:实际工程中还应补全拷贝构造、赋值运算符重载和析构函数(后文 key/value 版给出了完整写法,可参考)。

8. key 与 key/value 使用场景

8.1 key 搜索场景

只有 key 作为关键码,结构中只存储 key,搜索场景只需判断 key 在不在。key 搜索树支持增删查,不支持修改(修改 key 会破坏搜索树结构)。

场景 1:小区无人值守车库。物业把购买车位的业主车牌号录入后台系统,车辆进入时扫描车牌,在系统中则抬杆放行,不在则提示非本小区车辆。

场景 2:英文文章拼写检查。将词库中所有单词放入二叉搜索树,读取文章中的单词逐一查找,不在树中的单词就用波浪线标红提示。

8.2 key/value 搜索场景

每个关键码 key 都有对应的 value,value 可以是任意类型对象。结点中除了 key 还要存 value,增删查仍以 key 为关键字走二叉搜索树规则。key/value 树支持修改,但只允许修改 value,不能修改 key

场景 1:简单中英互译字典。结点存 key(英文)和 value(中文),输入英文即可查到对应中文。

场景 2:商场无人值守车库。入场扫描车牌,记录车牌和入场时间;出场扫描车牌,查找入场时间,用当前时间减入场时间算出停车时长与费用,缴费后抬杆。

场景 3:统计一篇文章中单词出现次数。读到一个单词先查找:不存在说明首次出现,插入 (单词, 1);已存在则对应次数 ++。

8.3 key/value 二叉搜索树代码实现

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:
    BSTree() = default;

    BSTree(const BSTree<K, V>& t)
    {
        _root = Copy(t._root);
    }

    BSTree<K, V>& operator=(BSTree<K, V> t)
    {
        swap(_root, t._root);
        return *this;
    }

    ~BSTree()
    {
        Destroy(_root);
        _root = nullptr;
    }

    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
            {
                return false;
            }
        }

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

    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;
    }

    bool Erase(const K& key)
    {
        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
            {
                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;
                }
                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;
                }
                else
                {
                    Node* rightMinP = cur;
                    Node* rightMin = cur->_right;
                    while (rightMin->_left)
                    {
                        rightMinP = rightMin;
                        rightMin = rightMin->_left;
                    }
                    cur->_key = rightMin->_key;
                    if (rightMinP->_left == rightMin)
                        rightMinP->_left = rightMin->_right;
                    else
                        rightMinP->_right = rightMin->_right;
                    delete rightMin;
                    return true;
                }
            }
        }
        return false;
    }

    void InOrder()
    {
        _InOrder(_root);
        cout << endl;
    }

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

    void Destroy(Node* root)
    {
        if (root == nullptr)
            return;
        Destroy(root->_left);
        Destroy(root->_right);
        delete root;
    }

    Node* Copy(Node* root)
    {
        if (root == nullptr)
            return nullptr;
        Node* newRoot = new Node(root->_key, root->_value);
        newRoot->_left = Copy(root->_left);
        newRoot->_right = Copy(root->_right);
        return newRoot;
    }

private:
    Node* _root = nullptr;
};

测试一:中英互译字典

int main()
{
    BSTree<string, string> dict;
    dict.Insert("left", "左边");
    dict.Insert("right", "右边");
    dict.Insert("insert", "插入");
    dict.Insert("string", "字符串");

    string str;
    while (cin >> str)
    {
        auto ret = dict.Find(str);
        if (ret)
            cout << "->" << ret->_value << endl;
        else
            cout << "无此单词,请重新输入" << endl;
    }
    return 0;
}

测试二:统计水果出现次数

int main()
{
    string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果",
                     "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

    BSTree<string, int> countTree;
    for (const auto& str : arr)
    {
        // 1. 不在树中:说明第一次出现,插入 <水果, 1>
        // 2. 在树中:查找到的结点次数++
        auto ret = countTree.Find(str);
        if (ret == nullptr)
            countTree.Insert(str, 1);
        else
            ret->_value++;
    }
    countTree.InOrder();   // 按键有序输出,且带出现次数
    return 0;
}

9. 总结

  • 二叉搜索树把二分思想迁移到链式结构,查找、插入、删除平均 O(log₂N),但最坏会退化为 O(N) 的单支树。
  • 中序遍历即得有序序列,这是它叫"排序树"的由来。
  • 不支持相等值 → 对应 map/set;支持相等值 → 对应 multimap/multiset
  • key 搜索解决"在不在";key/value 搜索解决"key 对应的 value 是什么",两者都不允许修改 key
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值