30天速通C++(十三):二叉搜索树

目录

前言

一.二叉搜索树的概念

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

三.二叉搜索树的实现

3.1二叉树的中序遍历

3.2二叉搜索树的插入

3.3二叉搜索树的查找

3.4二叉搜索树的删除

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

4.1key搜索场景

4.2key/value搜索场景

4.3key_value结构的实现

4.4二叉搜索树的拷贝构造和析构


前言


哈喽,各位小伙伴大家好!上次我们讲了C++的多态。今天我们来聊一聊二叉搜索树。


一.二叉搜索树的概念


二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
• 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
• 若它的右子树不为空,则右子树上所有结点的值都⼤于等于根结点的值
• 它的左右子树也分别为二叉搜索树
• ⼆叉搜索树中可以支持插入相等的值,也可以不支持插人相等的值,具体看使用场景定义,后续我们学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不之持插入相等值,multimap/multiset支持插如相等值

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


二叉搜索树最多查找高度次。因此二叉搜索树的性能取决的树的高度。

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


最坏情况
最差情况下,⼆叉搜索树退化为单⽀树(或者类似单⽀),其⾼度为: N


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

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

二分查找
另外需要说明的是,二分查找也可以实现 O(log2 N) 级别的查找效率,但是而分查找有两大缺陷:
①:需要存储在支持下标随机访问的结构中,并且有序。
②:插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据⼀般需要挪动数据。

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

三.二叉搜索树的实现


3.1二叉树的中序遍历

因为_root是私有成员,所以我们可以这样多套一层调用。

void Inorder()
{
    _Inorder(_root);
    cout << endl;
}
void _Inorder(const node* root)
{
    if (root == nullptr)
    {
        return;
    }
    _Inorder(root->left);
    cout << root->_key << " ";
    _Inorder(root->right);
}


3.2二叉搜索树的插入


插入的具体过程如下:

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


bool Insert(const k& x)
{
    if (_root == nullptr)//插入根节点
    {
        _root = new node(x);
        return true;
    }
    node* cur = _root;
    node* parent = nullptr;//记录父亲节点
    while (cur)
    {
        //介质不冗余
        /*if (x < cur->_key)
        {
            parent = cur;
            cur = cur->left;
        }
        else if (x > cur->_key)
        {
            parent = cur;
            cur = cur->right;
        }
        else
        {
            return false;
        }*/
        //介质冗余
        if (x <= cur->_key)//相等插入左子树
        {
            parent = cur;
            cur = cur->left;
        }
        else if (x > cur->_key)
        {
            parent = cur;
            cur = cur->right;
        }
    }
    if (x > parent->_key)
    {
        parent->right = new node(x);
    }
    else//相等插入左子树
    {
        parent->left = new node(x);
    }
    return true;
}


介质冗余:

介质不冗余:


3.3二叉搜索树的查找

bool Find(const k& x)
{
    node* cur = _root;
    while (cur)
    {
        if (x < cur->_key)//节点在左子树
        {
            cur = cur->left;
        }
        else if (x > cur->_key)//节点在右子树
        {
            cur = cur->right;
        }
        else
        {
            return true;//找到target
        }
    }
    return false;
}


3.4二叉搜索树的删除


⾸先查找元素是否在二叉搜索树中,如果不存在,则返回false。
如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)

要删除结点N左右孩子均为空
要删除的结点N左孩子位空,右孩子结点不为空
要删除的结点N右孩子位空,左孩子结点不为空
要删除的结点N左右孩子结点均不为空

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

左右为空
把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样的)


左空右不空
把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点

右空左不空
把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点


如果删除根节点直接修改_root。再删除节点即可。


左右均不空
无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的位置,都满足⼆叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除。

bool Erase(const k& x)
{
    node* cur = _root;
    node* parent = nullptr;
    while (cur)
    {
        if (x < cur->_key)
        {
            parent = cur;
            cur = cur->left;
        }
        else if (x > cur->_key)
        {
            parent = cur;
            cur = cur->right;
        }
        else//找到删除节点
        {
            if (cur->left == nullptr)//左为空
            {
                if (cur == _root)//防止删除根节点
                {
                    _root = cur->right;
                }
                else//判断连接父亲的左子树还是右子树
                {
                    if (cur == parent->left)
                    {
                        parent->left = cur->right;
                    }
                    else
                    {
                        parent->right = cur->right;
                    }
                }
                delete cur;
                return true;
            }
            else if (cur->right == nullptr)
            {
                if (cur == _root)//防止删除根节点
                {
                    _root = cur->left;
                }
                else//判断连接父亲的左子树还是右子树
                {
                    if (cur == parent->left)
                    {
                        parent->left = cur->left;
                    }
                    else
                    {
                        parent->right = cur->left;
                    }
                }
                delete cur;
                return true;
            }
            else
            {
             //替代节点的父亲初始化为删除节点防止不进去循环
                node* replaceparent = cur;//替代节点的父亲
                node* replace = cur->right;
                while (replace->left)//寻找右子树的最小节点
                {
                    replaceparent = replace;
                    replace = replace->left;
                }
                cur->_key = replace->_key;//替换根节点
                //判断连接父亲的左子树还是右子树
                if (replace == replaceparent->left)
                {
                    replaceparent->left = replace->right;
                }
                else
                {
                    replaceparent->right = replace->right;
                }
                delete replace;
                return true;
            }
        }
    }
    return false;
}



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


4.1key搜索场景


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

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

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


4.2key/value搜索场景


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

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

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

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

4.3key_value结构的实现


结构的定义
把节点封装,key_value多了一个V的类型。
同时这里using可以当成typedef使用。

//key结构
template<class k>
struct BSTNode
{
    using node = BSTNode<k>;
    k _key;
    node* left;
    node* right;
    BSTNode(const k& key)
        :_key(key)
        , left(nullptr)
        , right(nullptr)
    {}
};
template<class k>
class BSTree
{
    using node = BSTNode<k>;
public:
private:
    node* _root = nullptr;
};
//key_value结构
template<class k, class v>
struct BSTNode
{
    using node = BSTNode<k, v>;
    k _key;
    v _value;
    node* left;
    node* right;
    BSTNode(const k& key, const v& value)
        :_key(key)
        , _value(value)
        , left(nullptr)
        , right(nullptr)
    {}
};
template<class k, class v>
class BSTree
{
    using node = BSTNode<k, v>;
public:
private:
    node* _root = nullptr;
};



查找和删除
查找和删除的逻辑不需要改变。但是如果想知道查找的节点的value可以返回节点指针。同时如果允许介质冗余的话,那就需要查找中序的第一个,所以我们相同值时,先保留,继续去左子树查找即可。
 

node* Find(const k& x)
{
    node* ret = nullptr;
    node* cur = _root;
    while (cur)
    {
        if (x < cur->_key)
        {
            cur = cur->left;
        }
        else if (x > cur->_key)
        {
            cur = cur->right;
        }
        else
        {
            ret = cur;//保留当前节点
            cur = cur->left;//继续向左子树查找中序的第一个
        }
    }
    return ret;
}


插入
插入只需要多插入一个value值即可,逻辑不变。

bool Insert(const k& x, const v& v)
{
    if (_root == nullptr)//插入根节点
    {
        _root = new node(x, v);
        return true;
    }
    node* cur = _root;
    node* parent = nullptr;//保留父亲节点
    while (cur)
    {
        /*介质不冗余*/
        if (x < cur->_key)
        {
            parent = cur;
            cur = cur->left;
        }
        else if (x > cur->_key)
        {
            parent = cur;
            cur = cur->right;
        }
        else
        {
            return false;
        }
        //介质冗余
        //if (x <= cur->_key)//相等插入到左子树
        //{
        //    parent = cur;
        //    cur = cur->left;
        //}
        //else if (x > cur->_key)
        //{
        //    parent = cur;
        //    cur = cur->right;
        //}
    }
    if (x > parent->_key)
    {
        parent->right = new node(x, v);
    }
    else//相等插入左子树
    {
        parent->left = new node(x, v);
    }
    return true;
}


4.4二叉搜索树的拷贝构造和析构


拷贝构造
直接new构造根节点,再让根节点连接递归构造的左右子树即可。注意如果按照插入的思路构造,那就必须是层序遍历节点插入,否则拷贝出来的子树就不一样。

BSTree(const BSTree<k,v>& x)
{
    _root = Copy(x._root);
}
node* Copy( node* x)
{
    if (x == nullptr)
    {
        return x;
    }
    node* root = new node(x->_key, x->_value);
    root->left =Copy(x->left);
    root->right = Copy(x->right);
    return root;
}



析构
这里先析构左右子树在析构根节点即可。

    void Destory(const node* root)
    {
        if (root == nullptr)
        {
            return ;
        }
        Destory(root->left);
        Destory(root->right);
        delete root;
    }
    ~BSTree()
    {
        Destory(_root);
        _root = nullptr;
    }



赋值
这里先拷贝tmp,然后交换_root指针,函数结束后析构tmp空间。

BSTree<k, v>& operator = (BSTree<k, v> tmp)
{
    swap(_root,tmp._root);
    return *this;
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值