C++【二叉搜索树的总结与模拟实现】

前言介绍

        二叉搜索树(Binary Search Tree)顾名思义,是一个为搜索效率而生的数据结构,那接下来我们就先了解二叉搜索树的概念及如何实现。

一、概念

二叉搜索树别名:二叉排序树 / 二叉查找树,它可以是一棵空树,或者是具备以下性质的树:

【非空二叉搜索树必须具备以下性质】

        1. 左子树不为空,左子树的所有节点值 < 根结点值

        2. 右子树不为空,右子树的所有节点值 > 根结点值

图形实例:(必须严格满足性质)

二、模拟实现

2.1 创造树的节点及树的结构

//思考:创建一个二叉搜索树,首先要构造树的节点,其次再实现树的结构
//创建树的节点时,必须思考节点内部应该包含哪些变量?
// 1. 节点代表的值,2. 节点指向左右子树的指针,3. 指向父亲节点的指针
//开始构建树的节点,我们希望节点内部的成员可以被外界使用,所以使用struct类
template <class V>
struct BSTreeNode
{
    BSTreeNode(const V& val)
        : _val(val)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
    {}
    V _val;
    BSTreeNode<V>* _left;
    BSTreeNode<V>* _right;
    BSTreeNode<V>* _parent;
};
//构造完节点,下一步去设计树的结构,我们希望二叉搜索树只对外界展现成员函数,所以使用class类
template <class V>
class BSTree
{
public:
    typedef BSTreeNode<V> Node;

    //插入
    bool insert(const V& val);
    //删除
    bool erase();
    //查找
    Node* find(const V& val);
    //中序遍历 -> 有序
    void inorder();
    
private:
    Node* _root = nullptr; //这里使用指针的原因是动态的增删查改
};

2.2 编写成员函数

(1)插入insert

        按照二叉搜索树的性质来插入即可,需要注意的是下面代码不允许插入相同值

 bool insert(const V& val)
    {
        //空树
        if(_root == nullptr)
        {
            _root = new Node(val);
            return true;
        }
        //非空树
        else
        {
            Node* cur = _root;
            Node* parent = nullptr;
            while(cur)
            {
                if(cur->_val > val)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if(cur->_val < val)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else
                {
                    cout << "不允许插入已有的数据" << endl;
                    return false;
                }
            }

            //已经找到插入节点的父亲节点,开始判断插入左边还是右边
            Node* newnode = new Node(val);

            // 比根小,去左边插入
            if(parent->_val > val)
            {
                parent->_left = newnode;
            }
            // 比根大,去右边插入
            else
            {
                parent->_right = newnode;
            }
            newnode->_parent = parent;
            return true;
        }
    }

(2)查找find

        按照二叉搜索树的规则来查找,会发现最坏情况下只需要查找高度次,后面有测效率的代码

Node* find(const V& val)
{
    Node* cur = _root;
    while(cur)
    {
        if(cur->_val > val)
        {
            cur = cur->_left;
        }
        else if(cur->_val < val)
        {
            cur = cur->_right;
        }
        else
        {
            return cur;
        }
    }
    return nullptr;
}

(3)中序遍历 inorder

        只有中序遍历二叉搜索树才是有序,所以这里只使用中序遍历

void inorder()
{
    _inorder(_root);
    cout << endl;
}

void _inorder(Node* root) //像这种被封装一层的函数,我们一般不会对外开放,所以要放到private里
{
    if(root == nullptr)
        return;
    _inorder(root->_left);
    cout << root->_val << " ";
    _inorder(root->_right);
}

(4)删除erase【重点】

        在删除节点这里,是会影响二叉搜索树的性质的,所以我们要分下面4种情况来讨论:

情况1:被删除的节点不存在,直接返回false
情况2:被删除的节点存在,但为叶节点,则直接删除,不会影响二叉搜索树

情况3:被删除的节点的只有左孩子

情况4:被删除的节点的只有右孩子

情况2、3、4可以合并解决:

        因为我们会发现,情况3、4只是将节点删除,再连接父节点和孩子节点即可,基本对树的结构也无影响,所以情况2、3、4可以合并解决

情况5:被删除的节点有左右孩子

当被删除节点有左右孩子节点的时候,如下图:

        在删除17后,我们如何连接父亲节点和孩子节点?所以直接删除17,一定会影响二叉搜索树,那我们就要去寻找删除之后不影响整棵树,且要保证是二叉搜索树的节点。

如何寻找?

        先思考:删除哪个节点不影响二叉搜索树?一定是叶子节点,删除叶子节点一定不会影响这棵树,且还可以保证删除后仍为二叉搜索树。

删除哪个叶子节点?

        首先,当前节点是大于左子树的所有节点,小于右子树的所有节点,那我们就要找一个满足这两个要求的叶子节点。

通过观察符合要求的叶子节点为:

        1. 左子树的最右节点

        2. 右子树的最左节点

所以我们无论将哪一个节点的值与del_node交换,再删除该节点,都符合我们的要求

方法1:与左子树的最右节点交换

方法2:与右子树的最左节点交换

代码实现:

注意:这里别忘了考虑删除节点为根结点的情况哦~

    //删除
    bool erase(const V& val)
    {
        Node* del_node = find(val);

        //1. 被删除节点不存在
        if(del_node == nullptr)
            return false;

        //2. 被删除节点只有左孩子/只有右孩子/左右孩子都没有
        else if(del_node->_left == nullptr || del_node->_right == nullptr)
        {
            //删除节点为根结点
            if(del_node == _root)
            {
                _root = del_node->_left == nullptr ? 
                del_node->_right : del_node->_left;
                delete del_node;
                return true;
            }

            //不为根结点
            Node* parent = del_node->_parent;
            //只有左孩子
            if(del_node->_left != nullptr)
            {
                if(parent->_left == del_node)
                {
                    parent->_left = del_node->_left;
                }
                else
                {
                    parent->_right = del_node->_left;
                }
                del_node->_left->_parent = parent;
            }
            //只有右孩子
            else if(del_node->_right != nullptr)
            {
                if(parent->_left == del_node)
                {
                    parent->_left = del_node->_right;
                }
                else
                {
                    parent->_right = del_node->_right;
                }
                del_node->_right->_parent = parent;
            }
            //叶节点,因为都要删除,所以就放在一起删除了
            delete del_node;
            del_node = nullptr;
        }
        //3. 被删除节点有左右孩子
        else
        {
            //这里只实现找左子树的最右节点,另外的方法大家自行实现
            Node* find_LT_right = del_node->_left;
            while(find_LT_right->_right)
            {
                find_LT_right = find_LT_right->_right;
            }
            //交换值
            swap(find_LT_right->_val, del_node->_val);
            //删除叶节点

            //删除之前,要让其父节点的被删除孩子指向空
            //这里不是父节点的右孩子原因:左子树只有一个节点
            find_LT_right == find_LT_right->_parent->_left ?
            find_LT_right->_parent->_left = nullptr
            : find_LT_right->_parent->_right = nullptr;

            delete find_LT_right;
            find_LT_right = nullptr;
        }
        return true;
    }

三、代码汇总

#include <iostream>
#include <cassert>
using namespace std;
//思考:创建一个二叉搜索树,首先要构造树的节点,其次再实现树的结构
//创建树的节点时,必须思考节点内部应该包含哪些变量?
// 1. 节点代表的值,2. 节点指向左右子树的指针,3. 指向父亲节点的指针
//开始构建树的节点,我们希望节点内部的成员可以被外界使用,所以使用struct类
template <class V>
struct BSTreeNode
{
    BSTreeNode(const V& val)
        : _val(val)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
    {}
    V _val;
    BSTreeNode<V>* _left;
    BSTreeNode<V>* _right;
    BSTreeNode<V>* _parent;
};
//构造完节点,下一步去设计树的结构,我们希望二叉搜索树只对外界展现成员函数,所以使用class类
template <class V>
class BSTree
{
public:
    typedef BSTreeNode<V> Node;

    //插入
    bool insert(const V& val)
    {
        //空树
        if(_root == nullptr)
        {
            _root = new Node(val);
            return true;
        }
        //非空树
        else
        {
            Node* cur = _root;
            Node* parent = nullptr;
            while(cur)
            {
                if(cur->_val > val)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if(cur->_val < val)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else
                {
                    cout << "不允许插入已有的数据" << endl;
                    return false;
                }
            }

            //已经找到插入节点的父亲节点,开始判断插入左边还是右边
            Node* newnode = new Node(val);

            // 比根小,去左边插入
            if(parent->_val > val)
            {
                parent->_left = newnode;
            }
            // 比根大,去右边插入
            else
            {
                parent->_right = newnode;
            }
            newnode->_parent = parent;
            return true;
        }
    }
    //删除
    bool erase(const V& val)
    {
        //没有节点了
        assert(size() > 0);

        Node* del_node = find(val);

        //1. 被删除节点不存在
        if(del_node == nullptr)
            return false;

        //2. 被删除节点只有左孩子/只有右孩子/左右孩子都没有
        else if(del_node->_left == nullptr || del_node->_right == nullptr)
        {
            //删除节点为根结点
            if(del_node == _root)
            {
                _root = del_node->_left == nullptr ? del_node->_right : del_node->_left;
                delete del_node;
                return true;
            }

            //不为根结点
            Node* parent = del_node->_parent;
            //只有左孩子
            if(del_node->_left != nullptr)
            {
                if(parent->_left == del_node)
                {
                    parent->_left = del_node->_left;
                }
                else
                {
                    parent->_right = del_node->_left;
                }
                del_node->_left->_parent = parent;
            }
            //只有右孩子
            else if(del_node->_right != nullptr)
            {
                if(parent->_left == del_node)
                {
                    parent->_left = del_node->_right;
                }
                else
                {
                    parent->_right = del_node->_right;
                }
                del_node->_right->_parent = parent;
            }
            //为叶节点,要处理叶节点的父亲节点被删除的孩子置为空
            else
            {
                parent->_left == del_node ?
                parent->_left = nullptr:
                parent->_right = nullptr;
            }
            delete del_node;
            del_node = nullptr;
        }
        //3. 被删除节点有左右孩子
        else
        {
            //这里只实现找左子树的最右节点,另外的方法大家自行实现
            Node* find_LT_right = del_node->_left;
            while(find_LT_right->_right)
            {
                find_LT_right = find_LT_right->_right;
            }
            //交换值
            swap(find_LT_right->_val, del_node->_val);
            //删除叶节点

            //删除之前,要让其父节点的被删除孩子指向空
            //这里不是父节点的右孩子原因:左子树只有一个节点
            find_LT_right == find_LT_right->_parent->_left ?
            find_LT_right->_parent->_left = nullptr
            : find_LT_right->_parent->_right = nullptr;

            delete find_LT_right;
            find_LT_right = nullptr;
        }
        return true;
    }
    //查找
    Node* find(const V& val)
    {
        Node* cur = _root;
        while(cur)
        {
            if(cur->_val > val)
            {
                cur = cur->_left;
            }
            else if(cur->_val < val)
            {
                cur = cur->_right;
            }
            else
            {
                return cur;
            }
        }
        return nullptr;
    }
    //中序遍历 -》 有序
    void inorder()
    {
        _inorder(_root);
        cout << endl;
    }
    //节点个数
    size_t size()
    {
        return _size(_root);
    }
    //求高度
    size_t height()
    {
        return _height(_root);
    }
private:
    Node* _root = nullptr; //这里使用指针的原因是动态的增删查改
    void _inorder(Node* root)
    {
        if(root == nullptr)
            return;
        _inorder(root->_left);
        cout << root->_val << " ";
        _inorder(root->_right);
    }
    size_t _size(Node* root)
    {
        if(root == nullptr)
            return 0;

        return _size(root->_left) + _size(root->_right) + 1;
    }
    size_t _height(Node* root)
    {
        if(root == nullptr)
            return 0;

        //求左子树的高度
        size_t left = _height(root->_left);
        //求右子树的高度
        size_t right = _height(root->_right);

        //返回左右子树最大的高度,作为树的高度
        return left > right ? left + 1 : right + 1;
    }
};

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

         插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。

4.1 不足

        1. 这里我们只能传整型、字符型、浮点型的数据,无法传入字符串、pair或其他自定义类型的数据

        2. 当插入的数据是递增或者递减时,会退化成单支树的情况,使得查找的效率退化为O(n)

4.2 优化

优化1:

        使用仿函数,并作为模版参数,来使string可以作为节点的值;

        但在pair这里,则在模拟实现map、set时介绍给大家,就是多个封装。

改动后代码:

//使用仿函数解决问题
template <class V>
class Comp
{
public:
    bool operator()(const V& v1, const V& v2)
    {
        return v1 < v2;
    }
};
template<>
class Comp<string>
{
public:
    bool operator()(const string& s1, const string& s2)
    {
        return s1 > s2;
    }
};

//构造完节点,下一步去设计树的结构,我们希望二叉搜索树只对外界展现成员函数,所以使用class类
template <class V, class Comp = Comp<V>> ///******/
class BSTree
{
public:
    typedef BSTreeNode<V> Node;

    //插入
    bool insert(const V& val)
    {
        //空树
        if(_root == nullptr)
        {
            _root = new Node(val);
            return true;
        }
        //非空树
        else
        {
            Comp comp;   ///*****
            Node* cur = _root;
            Node* parent = nullptr;
            while(cur)
            {
                if(comp(val, cur->_val))
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if(comp(cur->_val, val))
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else
                {
                    cout << "不允许插入已有的数据" << endl;
                    return false;
                }
            }

            //已经找到插入节点的父亲节点,开始判断插入左边还是右边
            Node* newnode = new Node(val);

            // 比根小,去左边插入
            if(comp(val, parent->_val))
            {
                parent->_left = newnode;
            }
            // 比根大,去右边插入
            else
            {
                parent->_right = newnode;
            }
            newnode->_parent = parent;
            return true;
        }
    }
    //查找
    Node* find(const V& val)
    {
        Comp comp;
        Node* cur = _root;
        while(cur)
        {
            if(comp(val, cur->_val))
            {
                cur = cur->_left;
            }
            else if(comp(cur->_val, val))
            {
                cur = cur->_right;
            }
            else
            {
                return cur;
            }
        }
        return nullptr;
    }

private:
    Node* _root = nullptr; //这里使用指针的原因是动态的增删查改
};

优化2:

        使用AVL树或者红黑树来优化查找效率,解决单支树的情况:

C++版【AVL树的模拟实现】-CSDN博客

五、优化后代码汇总

#include <iostream>
#include <cassert>
using namespace std;
//思考:创建一个二叉搜索树,首先要构造树的节点,其次再实现树的结构
//创建树的节点时,必须思考节点内部应该包含哪些变量?
// 1. 节点代表的值,2. 节点指向左右子树的指针,3. 指向父亲节点的指针
//开始构建树的节点,我们希望节点内部的成员可以被外界使用,所以使用struct类
template <class V>
struct BSTreeNode
{
    BSTreeNode(const V& val)
        : _val(val)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
    {}
    V _val;
    BSTreeNode<V>* _left;
    BSTreeNode<V>* _right;
    BSTreeNode<V>* _parent;
};
template <class V>
class Comp
{
public:
    bool operator()(const V& v1, const V& v2)
    {
        return v1 < v2;
    }
};
template<>
class Comp<string>
{
public:
    bool operator()(const string& s1, const string& s2)
    {
        return s1 > s2;
    }
};

//构造完节点,下一步去设计树的结构,我们希望二叉搜索树只对外界展现成员函数,所以使用class类
template <class V, class Comp = Comp<V>>
class BSTree
{
public:
    typedef BSTreeNode<V> Node;

    //插入
    bool insert(const V& val)
    {
        //空树
        if(_root == nullptr)
        {
            _root = new Node(val);
            return true;
        }
        //非空树
        else
        {
            Comp comp;
            Node* cur = _root;
            Node* parent = nullptr;
            while(cur)
            {
                if(comp(val, cur->_val))
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if(comp(cur->_val, val))
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else
                {
                    cout << "不允许插入已有的数据" << endl;
                    return false;
                }
            }

            //已经找到插入节点的父亲节点,开始判断插入左边还是右边
            Node* newnode = new Node(val);

            // 比根小,去左边插入
            if(comp(val, parent->_val))
            {
                parent->_left = newnode;
            }
            // 比根大,去右边插入
            else
            {
                parent->_right = newnode;
            }
            newnode->_parent = parent;
            return true;
        }
    }
    //删除
    bool erase(const V& val)
    {
        //没有节点了
        assert(size() > 0);

        Node* del_node = find(val);

        //1. 被删除节点不存在
        if(del_node == nullptr)
            return false;

        //2. 被删除节点只有左孩子/只有右孩子/左右孩子都没有
        else if(del_node->_left == nullptr || del_node->_right == nullptr)
        {
            //删除节点为根结点
            if(del_node == _root)
            {
                _root = del_node->_left == nullptr ? del_node->_right : del_node->_left;
                delete del_node;
                return true;
            }

            //不为根结点
            Node* parent = del_node->_parent;
            //只有左孩子
            if(del_node->_left != nullptr)
            {
                if(parent->_left == del_node)
                {
                    parent->_left = del_node->_left;
                }
                else
                {
                    parent->_right = del_node->_left;
                }
                del_node->_left->_parent = parent;
            }
            //只有右孩子
            else if(del_node->_right != nullptr)
            {
                if(parent->_left == del_node)
                {
                    parent->_left = del_node->_right;
                }
                else
                {
                    parent->_right = del_node->_right;
                }
                del_node->_right->_parent = parent;
            }
            //为叶节点,要处理叶节点的父亲节点被删除的孩子置为空
            else
            {
                parent->_left == del_node ?
                parent->_left = nullptr:
                parent->_right = nullptr;
            }
            delete del_node;
            del_node = nullptr;
        }
        //3. 被删除节点有左右孩子
        else
        {
            //这里只实现找左子树的最右节点,另外的方法大家自行实现
            Node* find_LT_right = del_node->_left;
            while(find_LT_right->_right)
            {
                find_LT_right = find_LT_right->_right;
            }
            //交换值
            swap(find_LT_right->_val, del_node->_val);
            //删除叶节点

            //删除之前,要让其父节点的被删除孩子指向空
            //这里不是父节点的右孩子原因:左子树只有一个节点
            find_LT_right == find_LT_right->_parent->_left ?
            find_LT_right->_parent->_left = nullptr
            : find_LT_right->_parent->_right = nullptr;

            delete find_LT_right;
            find_LT_right = nullptr;
        }
        return true;
    }
    //查找
    Node* find(const V& val)
    {
        Comp comp;
        Node* cur = _root;
        while(cur)
        {
            if(comp(val, cur->_val))
            {
                cur = cur->_left;
            }
            else if(comp(cur->_val, val))
            {
                cur = cur->_right;
            }
            else
            {
                return cur;
            }
        }
        return nullptr;
    }
    //中序遍历 -》 有序
    void inorder()
    {
        _inorder(_root);
        cout << endl;
    }
    //节点个数
    size_t size()
    {
        return _size(_root);
    }
    //求高度
    size_t height()
    {
        return _height(_root);
    }
private:
    Node* _root = nullptr; //这里使用指针的原因是动态的增删查改
    void _inorder(Node* root)
    {
        if(root == nullptr)
            return;
        _inorder(root->_left);
        cout << root->_val << " ";
        _inorder(root->_right);
    }
    size_t _size(Node* root)
    {
        if(root == nullptr)
            return 0;

        return _size(root->_left) + _size(root->_right) + 1;
    }
    size_t _height(Node* root)
    {
        if(root == nullptr)
            return 0;

        //求左子树的高度
        size_t left = _height(root->_left);
        //求右子树的高度
        size_t right = _height(root->_right);

        //返回左右子树最大的高度,作为树的高度
        return left > right ? left + 1 : right + 1;
    }
};

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

终将向阳而生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值