第15讲:二叉搜索树

目录

  • [1. 二叉搜索树的概念](#1. 二叉搜索树的概念)
  • [2. 二叉搜索树的性能分析](#2. 二叉搜索树的性能分析)
  • [3. 二叉搜索树的插入](#3. 二叉搜索树的插入)
  • [4. 二叉搜索树的查找](#4. 二叉搜索树的查找)
  • [5. 二叉搜索树的删除](#5. 二叉搜索树的删除)
  • [6. 二叉搜索树的实现代码](#6. 二叉搜索树的实现代码)
  • [7. 二叉搜索树 key 和 key/value 使用场景](#7. 二叉搜索树 key 和 key/value 使用场景)
    • [7.1 key 搜索场景](#7.1 key 搜索场景)
    • [7.2 key/value 搜索场景](#7.2 key/value 搜索场景)
    • [7.3 key/value 二叉搜索树代码实现](#7.3 key/value 二叉搜索树代码实现)

1. 二叉搜索树的概念

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

  • 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值。
  • 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值。
  • 它的左右子树也分别为二叉搜索树。
  • 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义。后续学习的 map、set、multimap、multiset 系列容器底层就是二叉搜索树,其中 map/set 不支持插入相等值,multimap/multiset 支持插入相等值。

2. 二叉搜索树的性能分析

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:log_2(N)。

最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为: N N N。

所以综合而言,二叉搜索树增删查改时间复杂度为: O ( N ) O(N) O(N)。

那么这样的效率显然是无法满足我们需求的,我们后续课程需要继续讲解二叉搜索树的变形,平衡二叉搜索树 AVL 树和红黑树,才能适用于我们在内存中存储和搜索数据。

另外需要说明的是,二分查找也可以实现 O ( log ⁡ 2 N ) O(\log_2 N) O(log2N) 级别的查找效率,但是二分查找有两大缺陷:

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

这里也就体现出了平衡二叉搜索树的价值。

3. 二叉搜索树的插入

插入的具体过程如下:

  1. 树为空,则直接新增结点,赋值给 root 指针。
  2. 树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
  3. 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走。)
cpp 复制代码
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

4. 二叉搜索树的查找

  1. 从根开始比较,查找 x。x 比根的值大则往右边走查找,x 比根值小则往左边走查找。
  2. 最多查找高度次,走到空还没找到,这个值不存在。
  3. 如果不支持插入相等的值,找到 x 即可返回。
  4. 如果支持插入相等的值,意味着有多个 x 存在,一般要求查找中序的第一个 x。如下图,查找 3,要找到 1 的右孩子的那个 3 返回。

5. 二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回 false。

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为 N)

  1. 要删除结点 N 左右孩子均为空。
  2. 要删除的结点 N 左孩子为空,右孩子结点不为空。
  3. 要删除的结点 N 右孩子为空,左孩子结点不为空。
  4. 要删除的结点 N 左右孩子结点均不为空。

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

  1. 把 N 结点的父亲对应孩子指针指向空,直接删除 N 结点(情况 1 可以当成情况 2 或者情况 3 处理,效果是一样的)。
  2. 把 N 结点的父亲对应孩子指针指向 N 的右孩子,直接删除 N 结点。
  3. 把 N 结点的父亲对应孩子指针指向 N 的左孩子,直接删除 N 结点。
  4. 无法直接删除 N 结点,因为 N 的两个孩子无处安放,只能用替换法删除。找 N 左子树的值最大结点 R(最右结点)或者 N 右子树的值最小结点 R(最左结点)替代 N,因为这两个结点中任意一个放到 N 的位置,都满足二叉搜索树的规则。替代 N 的意思就是 N 和 R 两个结点的值交换,转而变成删除 R 结点,R 结点符合情况 2 或情况 3,可以直接删除。

6. 二叉搜索树的实现代码

cpp 复制代码
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
                {
                    // 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;
};

7. 二叉搜索树 key 和 key/value 使用场景

7.1 key 搜索场景

只有 key 作为关键码,结构中只需要存储 key 即可,关键码即为需要搜索到的值,搜索场景只需要判断 key 在不在。key 的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改 key 破坏搜索树结构了。

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

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

7.2 key/value 搜索场景

每一个关键码 key,都有与之对应的值 value,value 可以任意类型对象。树的结构中(结点)除了需要存储 key 还要存储对应的 value,增/删/查还是以 key 为关键字走二叉搜索树的规则进行比较,可以快速查找到 key 对应的 value。key/value 的搜索场景实现的二叉树搜索树支持修改,但是不支持修改 key,修改 key 破坏搜索树性质了,可以修改 value。

场景 1:简单中英互译字典,树的结构中(结点)存储 key(英文)和 value(中文),搜索时输入英文,则同时查找到了英文对应的中文。

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

场景 3:统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,(单词,1),单词存在,则 ++ 单词对应的次数。

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

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

字典示例:

cpp 复制代码
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;
}

统计单词出现次数示例:

cpp 复制代码
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;
}
相关推荐
猫猫.1531 小时前
HashTable、HashMap、ConcurrentHashMap 核心区别总结
java·开发语言·java-ee
pipixia200332 小时前
C++继承面试四连问:内存叠加、私有继承、sizeof、权限重映射
c++·面试·嵌入式·继承·面向对象·对象模型
乌暮2 小时前
综合案例:宠物电商系统
java·开发语言·spring·mvc·restful·1024程序员节
大文说跨境2 小时前
亚马逊多店铺的图片素材怎么批量去重:EXIF 与哈希的处理流程
开发语言·php
大风起兮云飞扬丶4 小时前
Java——枚举
java·开发语言
小灰灰搞电子4 小时前
Rust+Slint 实现高级动态消息提示框源码分享
开发语言·rust
Dev _4 小时前
Python解释器安装在系统级目录导致命令行用户‌没有权限‌向该目录写入文件
开发语言·python
牵猫散步的鱼儿4 小时前
C++ 内存分区
c++
qq_406981034 小时前
大白话rust系列08 当变量遮蔽遇上引用,探究变量遮蔽的本质
开发语言·后端·rust