sgi-stl3.0 - 手搓二叉搜索树

文章目录

前言

一、什么是二叉搜索树

二、结构

三、实现代码

拷贝构造函数

赋值运算符重载

析构函数

插入

中序遍历

 查找

删除

总结


前言

路漫漫其求远兮,吾将上下而求索;


一、什么是二叉搜索树

二叉搜索树(Binary Search Tree,BST),又称二叉排序树,是一种特殊的二叉树结构,它满足以下性质之一:

  1. 是一棵空树;

  2. 或满足以下所有条件:

    • 若左子树非空,则左子树上所有结点的值均小于或等于根结点的值;

    • 若右子树非空,则右子树上所有结点的值均大于或等于根结点的值;

    • 左子树和右子树也分别是二叉搜索树。

需要注意的是,根据实现需求,二叉搜索树可分为 不允许重复值(无冗余) 和 允许重复值(有冗余) 两种设计;

这种结构支持高效的查找、插入和删除操作,平均时间复杂度为 O(log n),但在最坏情况下(如退化为链表)可能降至 O(n)。所以综合而言,二叉搜索树的查找、插入、删除的效率为O(N),如果想要维持其O(logN)的效率,需要对其平衡进行控制,也就是AVL树、红黑树

二、结构

定义搜索二叉树的节点结构:

    //定义节点
    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;
    public:
        BSTree():_root(nullptr){}

        //拷贝构造,需要遍历+构造节点
        BSTree(const Node& t){}

        //赋值运算符重载 --》 利用拷贝构造的现代写法
        Node& operator=(Node t){}

        //析构函数 --》后序遍历释放节点
        ~BSTree(){}

        //插入
        bool Insert(const K& key){}
        //中序遍历
        void Inorder(){}
        //查找
        bool Find(const K& key){}
        //删除
        bool Erase(const K& key){}
    private:
        Node* _root;
    };

三、实现代码

拷贝构造函数

需要递归地遍历该二叉搜索树当中的节点,创建节点并处理连接关系;

参考代码如下:

        //拷贝构造,需要遍历+构造节点
        BSTree(const Node& t)
        {
            _root = copy(t._root);
        }

        Node* copy(Node* root)
        {
            //前序遍历构造一棵树
            if(root==nullptr) return nullptr;
            Node* newRoot = new Node(root->_key);
            newRoot->left = copy(root->_left);
            newRoot->right = copy(root->_right);

            return newRoot;
        }

赋值运算符重载

此处实现巧妙利用拷贝构造函数(传值传参会进行拷贝,那么我们在赋值运算符重载中利用swap 交换this 与 传值传参的局部对象即可实现)

参考代码如下:

        //赋值运算符重载 --》 利用拷贝构造的现代写法
        Node& operator=(Node t)
        {
            std::swap(_root , t._root);
            return *this;
        }

析构函数

析构的实现需要后序遍历这颗二叉搜索树,释放节点

参考代码如下:

        //析构函数 --》后序遍历释放节点
        ~BSTree()
        {
            Destory(_root);
            _root = nullptr;
        }
        void Destory(Node* root)
        {
            if(root==nullptr) return;
            Destory(root->_left);
            Destory(root->_right);
            delete root;
        }

插入

过程:

  • 如果根节点为空,直接创建一个节点然后插入
  • 如果根节点不为空,需要找到插入值key 合适的位置进行插入:当key 小于当前节点cur中存储的值,那么cur = cur->_left , 如果key 大于当前节点cur 当中存储的值,那么cur = cur->_right;至于key等于当前节点cur 中存储值的情况,需要结合实际情况(是否支持冗余)来决定,此处实现为不冗余;

参考代码如下:

        //插入数据,不允许插入冗余的数据
        bool Insert(const K& key)
        {   
            //空树的话,直接创建节点
            if(_root==nullptr)
            {
                _root = new Node(key);
                return true;
            }
            //找到合适的位置,利用前后指针
            Node* cur = _root, *prev = nullptr;
            while(cur)
            {
                if(key < cur->_key)
                {
                    prev = cur;
                    cur = cur->_left;
                }
                else if(key > cur->_key)
                {
                    prev = cur;
                    cur = cur->_right;
                }
                else{
                    //数据已经存在
                    return false;
                }
            }

            //找到了合适的位置,需要创建节点并处理链接关系
            cur = new Node(key);
            //判断这个节点需要放在prev 的左边还是右边
            if(key < prev->_key)
            {
                prev->_left = cur;
            }
            else{
                prev->_right = cur;
            }
            return true;
        }

中序遍历

二叉搜索树中序遍历的结果是有序的,我们实现的Inorder 需要传参,而在外部由于访问限定符的限制不易于外部调用传参,所以我们可以考虑在内部封装一层,即: void _Inorder(Node* root)

参考代码如下:

        //中序遍历
        void Inorder()
        {
            _Inorder(_root);
            std::cout << std::endl;
        }
        void _Inorder(Node* root)
        {
            if(root == nullptr) return;
            _Inorder(root->_left);
            std::cout << root->_key << " ";
            _Inorder(root->_right);
        }

 查找

查找的逻辑与插入数据的逻辑较为相似,比对当前节点cur 当中存储的值与 key 的大小关系,然后决定当前节点 cur 应该往左还是往右走;

参考代码如下:

        //查找数据
        bool Find(const K& key)
        {
            Node* cur = _root;
            while(cur)
            {
                if(key < cur->_key)
                {
                    cur = cur->_left;
                }
                else if(key > cur->_key)
                {
                    cur = cur->_right;
                }
                else{
                    return true;
                }
            }
            return false;
        }

删除

删除=查找存储值为key 的节点+处理删除之后的链接关系

需要分情况讨论:

  • 所删除的节点没有孩子节点 --》直接删除、处理其父节点的指针指向
  • 所删除的节点只有左孩子,需要其父节点”接管“该所删除节点的左孩子;
  • 所删除的节点只有右孩子,需要其父节点”接管“所删除节点的右孩子;
  • 所删除的节点既有左孩子又有右孩子,此时需要使用交换删除法,即在其左子树中找到最大的节点(或者是右子树中最小的节点)与所删除的节点进行交换,然后再删除;

需要注意的是,在实现删除逻辑的时候,需要判断所删除节点是否为根节点(如果为根节点,需要处理成员变量 _root)

参考代码如下:

        //删除一个节点 --》 需要判断当前这个节点是否存在孩子节点,需要对孩子节点的个数情况进行判断
        bool Erase(const K& key)
        {
            //查找 + 判断子节点的情况
            Node* cur = _root, * prev = nullptr;
            while(cur)
            {
                if(key < cur->_key)
                {
                    prev = cur;
                    cur = cur->_left;
                }
                else if(key > cur->_key)
                {
                    prev = cur;
                    cur = cur->_right;
                }
                else{
                    //找到了节点,判断这个节点是否存在子节点;当然,找到的节点也有可能为根节点,那么prev 为 nullptr
                    if(cur->_left == nullptr)
                    {
                        if(prev == nullptr)
                        {
                            _root = cur->_right;
                        }
                        else
                        {
                            //不存在左孩子,可能存在右孩子,判断cur 当前在 prev 的左边还是右边,然后将cur 的右孩子给给prev
                            if(cur == prev->_left)
                            {
                                prev->_left = cur->_right;
                            }else{
                                prev->_right = cur->_right;
                            }
                        }
                        delete cur;
                    }
                    else if(cur->_right == nullptr)
                    {
                        if(prev==nullptr)
                        {
                            _root = cur->_left;
                        }
                        else
                        {
                            if(cur == prev->_left)
                            {
                                prev->_left = cur->_left;
                            }else{
                                prev->_right = cur->_left;
                            }
                        }
                        delete cur;
                    }
                    else
                    {
                        //存在两个孩子,交换+删除的思想,需要找到cur 左子树当中的最大节点或者右子树当中的最小节点
                        Node* replaceParent = cur, *replace = cur;
                        while(replace->_left)//此处实现为找到右子树当中的最小节点,即最左节点
                        {
                            replaceParent = replace;
                            replace = replace->_left;
                        }
                        std::swap(cur->_key , replace->_key);
                        //处理连接关系,当前的replace 可能存在右孩子
                        if(replace == replaceParent->_left)
                        {
                            replaceParent->_left = replace->_right;
                        }
                        else
                        {
                            replaceParent->_right = replace ->_left;
                        }
                        delete replace;
                    }
                    return true;
                }
            }
            return false;
        }

总结

动手写一写吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值