【C++】二叉搜索树(BST):从原理到实现

二叉搜索树(BST)以"左小右大"的有序性约束组织数据,使查找、插入、删除可在树高范围内完成。本文系统梳理 BST的定义与中序有序性、性能与树高的关系、插入/查找/删除的关键边界处理,并给出 key 与 key/value 两套 C++模板实现,实现把规则、指针操作与可运行代码建立明确对应。


一、概念

二叉搜索树也称 二叉排序树。它要么是空树,要么满足下面这套"大小规则"(这就是 BST 的灵魂):

  1. 若它的左子树不为空,则左子树上所有结点的值均 小于 它的根结点的值。
  2. 若它的右子树不为空,则右子树上所有结点的值均 大于 它的根结点的值。
  3. 它的左右子树也分别为二叉搜索树(也要继续满足上面 1、2)。

换句话说:对任意结点,都要满足 左边都比我小,右边都比我大,而且子树也要同样遵守。

二叉搜索树支持 中序遍历(左-根-右)。如果一棵二叉树是 BST,那么对它做中序遍历得到的结果一定是一个 有序序列。


二、二叉搜索树的性能分析

BST 的插入、查找、删除,真正花多少时间,主要取决于一个东西:树高(高度越小,走的路越短)。

  • 最好情况:BST 是一棵完全二叉树

    此时树高约为:log2(N)

    所以增删查改都大约是:O(log2 N)

  • 最坏情况:BST 退化成单支树(一边倒,像链表)

    此时树高约为:N

    所以增删查改会变成:O(N)

综合而言,二叉搜索树的增删查改时间复杂度为:O(N)(按最坏情况算)。

为什么后面还要学二叉平衡树?

因为有些场景不能接受"最坏 O(N)",就需要 AVL 树、红黑树这类 二叉平衡树 来把高度控制在 O(logN),从而稳定地快。

另外,二分查找也可以做到 O(log2 N),但它有两个问题:

  1. 必须支持随机访问(典型是数组)。
  2. 数据必须有序。
    而有序数组要插入/删除一个元素,通常需要大量移动元素,效率不高。

所以:想要"查找快",又想要"插入/删除也相对方便",平衡 BST 就很有价值。

情况 树形态 树高 查找/插入/删除
最好 完全二叉树 log2(N) O(log2 N)
最坏 单支树(退化) N O(N)

三、二叉搜索树的插入

BST 的插入,本质就是:按大小规则一路往下走,直到走到空位置,把新结点挂上去。

插入过程:

  1. 先看根结点是否为空。
    • 若为空,直接用待插入值构造根结点。
  2. 若不为空:
    • 把当前结点 cur 指向根,从根开始找插入位置;
    • 比较插入值和 cur 的值:
      • 小:往左子树走
      • 大:往右子树走
    • cur 走到空(nullptr)时,说明位置找到了:把新结点插入,并挂到它父结点的左/右上。
  3. 二叉搜索树 不允许冗余(相等)元素。
    当需要冗余元素时,这里用不着二叉搜索树,用后面的二叉平衡树。
    当然,如果需要实现插入相同值的元素,也是可以实现的,只需要提前规定:
    • 比如遇到相同值时都插入到右子树,或者都插入到左子树。
    • 归结为:遇到相同元素时,一定要优先去某一个方向。
步骤 当前状态 判断 下一步
1 cur 指向根 root 是否为空 为空:root=new;不空:进入循环
2 循环 key 与 cur->_key 比较 小:cur=cur->_left;大:cur=cur->_right
3 cur 为空 找到插入位置 parent 挂接新结点到 left/right

例子:给定数组,按顺序插入到 BST 中

cpp 复制代码
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};



四、二叉搜索树的查找

查找也是同一套走向规则:

  1. 从根开始,把 cur 指向根。

  2. 比较要查找的值 xcur

    • x > cur:往右子树走
    • x < cur:往左子树走
    • 相等:找到了
  3. 如果一路走到 cur == nullptr 还没找到,说明不存在。

因此:BST 的查找效率同样依赖 树高。

如果 BST 支持冗余数据:查找时必须返回 中序的第一个 x。

例如:查找 3,应该返回的 3 是结点 1 的右孩子。


五、二叉搜索树的删除

删除要比插入/查找更麻烦一点,因为要维护 BST 的大小规则。

删除思路先分两步:

  1. 先判断要删除的元素是否在 BST 中。

    • 不存在:删除失败,返回 false
  2. 存在:根据被删结点 N 的孩子情况分 4 类讨论

    1)N 的左右孩子都为空

    2)N 的左孩子为空,右孩子不为空

    3)N 的右孩子为空,左孩子不为空

    4)N 的左右孩子都不为空

对应以上四种情况的解决方案:

1)当 N 的左右孩子都为空:直接删除 N。(也可以按情况 2 或 3 处理,效果一样)

2)当 N 的左孩子为空,右孩子不为空:删除 N,用 N 的右孩子替代 N 的位置。

3)当 N 的右孩子为空,左孩子不为空:删除 N,用 N 的左孩子替代 N 的位置。

4)当 N 的左右孩子都不为空:不能直接删除 N

此时需要用 替换法删除:

  • 找到 N 左子树的最大结点(最右结点)或者 N 右子树的最小结点(最左结点),记为替代结点 R
  • R 替换 N(通常是把值交换/覆盖到 N 上);
  • 再删除 R。因为 R 一定满足情况 2 或 3,所以可以直接删除。



六、二叉搜索树代码实现

重点:

  • Insert/Find:比较后往左/右走
  • Erase:要处理"删根"和"删两孩子"的指针调整
cpp 复制代码
template<class K>
struct BSTNode
{
    BSTNode(const K& key)
        :_key(key)
        ,_left(nullptr)
        ,_right(nullptr)
    {}

    K _key;
    BSTNode<K>* _left;
    BSTNode<K>* _right;
};

//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
                {
                    //2个孩子的情况
                    //删除情况4,替换法删除
                    //假设这里我们取右子树的最小结点作为替代结点去删除
                    //这里尤其要注意右子树的根就是最小情况的情况的处理,对应课件图中删除8的情况
                    //一定要把cur给rightMinP,否则会报错。
                    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;
};

注意:

  • Insert 必须用 parent 记住"cur 的上一个结点",否则你走到空结点时就不知道该把新结点挂到谁下面。
  • Erase 要特别注意 删除根结点:代码用 parent == nullptr 来区分根。
  • Erase 的两孩子情况,用的是"右子树最小结点 rightMin"来替换,再把 rightMin 删除掉(rightMin 最终一定能落到 0/1 孩子的分支里处理)。

七、二叉搜索树key和key/value使用场景

7.1 key搜索场景:

这里只存 key,重点是:能不能快速判断"某个 key 是否存在"。

支持的操作:插入、删除、查找。

但通常不支持 修改:因为 BST 的结构就是靠 key 的大小关系组织的,你把 key 改了,整棵树的大小规则就可能被破坏。

场景:

1)停车场:每一辆车都具有一个唯一的车牌号,使用车牌号作为 key 值,建立 BST,车辆进场和出场分别对应插入和删除,查找即查找某辆车是否在场。

2)拼写检查:单词作为 key,建立 BST,查找时判断是否存在,若不存在则拼写错误。

7.2 key/value搜索场景:

这里每个 key 都有一个对应的 value。value 的类型可以很灵活(看业务需求)。

BST 的结点需要存 key 和 value。插入、删除、查找时,仍然按 BST 规则用 key 比较大小 来走左/右子树,因此可以快速找到 key 对应的 value。

key/value BST 支持 修改(通常指修改 value),但不支持修改 key:key 还是结构的"排序依据",改 key 会破坏整棵树。

场景:

1)字典:英文单词 key,对应翻译 value。

2)购物广场停车场:车牌 key,进场时间 value。

3)统计单词出现次数:单词 key,出现次数 value。

第一次出现:插入 <word, 1>

再次出现:找到结点,把次数 ++

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

下面是课件给出的 key/value BST 实现与两个例子(字典、词频统计)。注意:代码里"比较大小"仍然只比较 _key

cpp 复制代码
template<class K, class V>
struct BSTNode
{
    //pair<K, V> _kv;
    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;
    //BSTree<string, string> copy = 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、在,则查找到的结点中水果对应的次数++
        //BSTreeNode<string, int>* ret = countTree.Find(str);
        auto ret = countTree.Find(str);
        if (ret == NULL)
        {
            countTree.Insert(str, 1);
        }
        else
        {
            ret->_value++;
        }
    }

    countTree.InOrder();

    return 0;
}

重点:

  • Insert:比较 _key 决定走左/右;最终用 parent 挂接新结点。
  • Find:一路比较 _key,返回结点指针,这样外部就能直接读/改 _value
  • 词频统计:先 Find,找不到就 Insert(str, 1),找到就 ret->_value++

相关推荐
zylyehuo2 小时前
error: no matching function for call to ‘ros::NodeHandle::param(const char [11], std::string&, const char [34])’
c++·ros1
程序猿炎义2 小时前
【Easy-VectorDB】Faiss数据结构与索引类型
数据结构·算法·faiss
星火开发设计2 小时前
C++ 函数定义与调用:程序模块化的第一步
java·开发语言·c++·学习·函数·知识
天赐学c语言3 小时前
1.20 - x的平方根 && vector的扩容机制以及删除元素是否会释放内存
c++·算法·leecode
CC.GG3 小时前
【C++】用哈希表封装myunordered_map和 myunordered_set
java·c++·散列表
jiaguangqingpanda4 小时前
Day24-20260120
java·开发语言·数据结构
ValhallaCoder4 小时前
Day53-图论
数据结构·python·算法·图论
C雨后彩虹4 小时前
羊、狼、农夫过河
java·数据结构·算法·华为·面试
xiaoye-duck4 小时前
C++ string 类使用超全攻略(上):创建、遍历及容量操作深度解析
c++·stl