C++起始之路——二叉搜索树

💁‍♂️个人主页:进击的荆棘

👇作者其它专栏:

《数据结构与算法》《算法》《C++起始之路》


目录

1.二叉搜索树的概念

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

3.二叉搜索树的插入

4.二叉搜索树的查找

5.二叉搜索树的删除

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

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


1.二叉搜索树的概念

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

●若它的左子树不为空,则左子树上所有节点的值都小于等于根节点的值

●若它的右子树不为空,则右子树上所有节点的值都大于等于根节点的值

●它的左右子树也分别为二叉搜索树

●二叉搜索树可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,当我们使用map/set/multimap/multiset系列容器时,它们的底层就是二叉搜索树,其中map/set不支持插入相等的值,multimap/multiset支持插入相等值

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

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

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

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

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

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

1.需要存储在支持下标随机访问的结构中,并且有序

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

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

3.二叉搜索树的插入

插入的具体过程如下:

1.树为空,则直接新增节点,赋值给root指针

2.树不空,按二叉搜索树性质,插入值比当前节点大往右走,插入值比当前节点小往左走,找到空位置,插入新节点

3.若支持插入相等的值,插入值跟当前节点相等的值可以往右走,也可以往左走,找到空位置,插入新节点(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走)

int a[]={8,3,1,10,6,4,7,14,13};

4.二叉搜索树的查找

1.从跟开始比较,查找x,x比根的值大就往右边查找,比它小就往左边查找。

2.最多查找高度次,走到空,若还没找到,则这个值不存在。

3.若不支持插入相等的值,找到x即可返回。

4.若支持插入相等的值,意味着有多个x存在,一般要求查找中序的第一个x。如下图,查找3,要找到1的右孩子的那个3返回。

5.二叉搜索树的删除

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

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

1.要删除节点N左右孩子均为空

2.要删除的节点N左孩子为空,右孩子节点不为空

3.要删除的节点N右孩子为空,左孩子节点不为空

4.要删除的节点左右孩子节点均不为空

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

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

2.把N节点的父亲对应孩子指针指向N的右孩子,直接删除N节点

3.把N节点的父亲对应孩子指针指向N的左孩子,直接删除N节点

4.无法直接删除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)
    {}
};
template<class K>
class BSTree{
    //typedef BSTNode<K> Node;
    //C++11新用法,意同上面
    using Node=BSTNode<K>;
public:
    bool Insert(const K& key){
        if(!_root) {
            _root=new Node(key);
            return true;
        }
        Node* cur=_root;
        Node* parent=nullptr;
        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){
        if(!_root) return false;
        Node* cur=_root;
        while(cur){
            if(cur->_key<key){
                cur=cur->_right;
            }
            else if(cur->_key>key){
                cur=cur->_left;
            }
            else break;
        }
        return true;
    }
    bool Erase(const K& key){
        Node* cur=_root;
        Node* parent=nullptr;
        while(cur){
            if(cur->_key<key){
                parent=cur;
                cur=cur->_right;
            }
            else if(cur->_key>key){
                parent=cur;
                cur=cur->_left;
            }
            //找到该值,有四种情况:1.无左右孩子 2.有左无右 3.有右无左 4.有两孩子
            //总结为两种情况,1.无左将右赋给对应的父节点的左右节点,无右将左赋给对应的父结点的左右节点(无孩子不影响,因为会赋空)
            //2.无法直接将节点删去,因为它的两孩子无处安放,应使用替代法,让它左子树的最大节点或右子树的最小节点与它替换,然后删去
            else{
                //左为空时
                if(!cur->_left){
                    if(cur==_root) _root=cur->_right;
                    else{
                        if(parent->_left==cur) parent->_left=cur->_right;
                        else parent->_right=cur->_right;
                    }
                    delete cur;
                }
                //右为空时
                else if(!cur->_right){
                    if(cur==_root) _root=cur->_left;
                    else{
                        if(parent->_left==cur) parent->_left=cur->_left;
                        else parent->_right=cur->_left;
                    }
                    delete cur;
                }
                //左右都不为空,右子树最左节点
                else{
                    Node* replaceParent=cur;
                    Node* replace=cur->_right;
                    while(replace->_left){
                        replaceParent=replace;
                        replace=replace->_left;
                    }
                    cur->_key=replace->_key;
                    if(replaceParent->_left==replace) replaceParent->_left=replace->_right;
                    else replaceParent->_right=replace->_right;
                    delete replace;
                }
                return true;
            }
        }
        return false;
    }
    void InOrder(){
        _InOrder(_root);
        cout<<endl;
    }
private:
    void _InOrder(const Node* root){
        if(!root) return ;
        _InOrder(root->_left);
        cout<<root->_key<<' ';
        _InOrder(root->_right);
    }
private:
    Node* _root=nullptr;
};

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

7.1key搜索场景:

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

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

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

7.2key/value搜索场景:

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

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

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

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

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

cpp 复制代码
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> Node;
    //C++11新用法,意同上面
    using Node=BSTNode<K,V>;
    //C++11,强制生成构造
    BSTree()=default;
    BSTree(const BSTree& t){
        _root=Copy(t._root);
    }
    BSTree& operator=(BSTree tmp){
        swap(_root,tmp._root);
        return *this;
    }
    ~BSTree(){
        Destroy(_root);
        _root=nullptr;
    }
public:
    bool Insert(const K& key,const V& value){
        if(!_root) {
            _root=new Node(key,value);
            return true;
        }
        Node* cur=_root;
        Node* parent=nullptr;
        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;
    }
    bool Find(const K& key){
        if(!_root) return false;
        Node* cur=_root;
        while(cur){
            if(cur->_key<key){
                cur=cur->_right;
            }
            else if(cur->_key>key){
                cur=cur->_left;
            }
            else break;
        }
        return true;
    }
    bool Erase(const K& key){
        Node* cur=_root;
        Node* parent=nullptr;
        while(cur){
            if(cur->_key<key){
                parent=cur;
                cur=cur->_right;
            }
            else if(cur->_key>key){
                parent=cur;
                cur=cur->_left;
            }
            //找到该值,有四种情况:1.无左右孩子 2.有左无右 3.有右无左 4.有两孩子
            //总结为两种情况,1.无左将右赋给对应的父节点的左右节点,无右将左赋给对应的父结点的左右节点(无孩子不影响,因为会赋空)
            //2.无法直接将节点删去,因为它的两孩子无处安放,应使用替代法,让它左子树的最大节点或右子树的最小节点与它替换,然后删去
            else{
                //左为空时
                if(!cur->_left){
                    if(cur==_root) _root=cur->_right;
                    else{
                        if(parent->_left==cur) parent->_left=cur->_right;
                        else parent->_right=cur->_right;
                    }
                    delete cur;
                }
                //右为空时
                else if(!cur->_right){
                    if(cur==_root) _root=cur->_left;
                    else{
                        if(parent->_left==cur) parent->_left=cur->_left;
                        else parent->_right=cur->_left;
                    }
                    delete cur;
                }
                //左右都不为空,右子树最左节点
                else{
                    Node* replaceParent=cur;
                    Node* replace=cur->_right;
                    while(replace->_left){
                        replaceParent=replace;
                        replace=replace->_left;
                    }
                    cur->_key=replace->_key;
                    if(replaceParent->_left==replace) replaceParent->_left=replace->_right;
                    else replaceParent->_right=replace->_right;
                    delete replace;
                }
                return true;
            }
        }
        return false;
    }
    void InOrder(){
        _InOrder(_root);
        cout<<endl;
    }
private:
    void _InOrder(const Node* root){
        if(!root) return ;
        _InOrder(root->_left);
        cout<<root->_key<<' ';
        _InOrder(root->_right);
    }
    void Destroy(Node* root){
        if(!root) return ;
        Destroy(root->_left);
        Destory(root->right);
        delete root;
    }
    Node* Copy(Node* root){
        if(!root){
            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;
}
相关推荐
少司府2 小时前
C++基础入门:类和对象(上)
c语言·开发语言·c++·类和对象·访问限定符
REDcker2 小时前
C++ new、堆分配与 brk / mmap
linux·c++·操作系统·c·内存
阿阿阿阿里郎2 小时前
C++面向对象--类、模板
c++
William_wL_2 小时前
【C++】list的使用
c++
Elnaij2 小时前
从C++开始的编程生活(25)——C++11标准Ⅱ
开发语言·c++
cjforever142 小时前
各STL容器的模拟实现
开发语言·数据结构·c++
带鱼吃猫2 小时前
C++11 核心特性解析(二):包装器、参数包与 emplace 进阶技术体系详解
开发语言·c++
重庆小透明2 小时前
Redis 九大数据结构:从原理到实战场景
数据结构·数据库·redis
Boop_wu2 小时前
[Java 算法] 快速排序和快速选择排序(※)
数据结构·算法·排序算法