【C++从入门到踹门】第十四篇:二叉搜索树


在这里插入图片描述

二叉搜索树的概念

以递归的方式来定义二叉搜索树:

  • 若它的左子树不为空,那么它左子树上所有结点的值都小于根节点的值。
  • 若它的右子树不为空,那么它右子树上所有结点的值都大于根结点的值。
  • 它的左右子树也为二叉搜索树。

在这里插入图片描述

操作与实现

二叉树结点的结构体:

template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)//结点的构造函数
	{
		_key = key;
		_left = nullptr;
		_right = nullptr;
	}
};

二叉搜索树类:

template<class K>
class BSTree
{
public:
    typedef BSTreeNode<K> node;

    //构造
    BSTree():_root(nullptr){}

private:
    node* _root;//根结点
}

二叉搜索树

二叉搜索树的拷贝构造,赋值重载,析构函数

整棵树的拷贝是深拷贝

写一个_Copy函数,使用前序递归遍历树的同时执行深拷贝:

node* _Copy(node* root)
{
    if(root==nullptr)
    {
        return nullptr;
    }
    node* copynode=new node(root->_key);
    copynode->left=_Copy(root->left);
    copynode->right=_Copy(root->right);
    return copyNode;
}

// 拷贝构造
BSTree(const BSTree<K>& t)
{
    _root=_Copy(t._root);
}

void Destroy(node* root)
{
    if(root==nullptr)
    {
        return ;
    }
    Destroy(root->left);
    Destroy(root->right);
    delete root;
}

//赋值重载
BSTree<K>& operator=(BSTree<K> T)
{
    Destroy(_root);
    _root=nullptr;
    swap(_root,t._root);
    return *this;
}

//析构
~BSTree()
{
    Destroy(_root);
    _root=nullptr;
}

二叉搜索树的查找

如果根结点值 ==查找值,返回true;
如果根结点值 > 查找值,在其左子树查找;
如果根结点值 < 查找值,在其右子树查找;
否则,返回false。

在这里插入图片描述

分别使用递归和迭代的方法进行查找

  • 使用递归进行查找
//_root是private权限,而递归查找则需要传入_root作为参数
//所以这里需要套个娃,public的查找函数嵌套另一个可调用_root的查找函数
private:
node* _FindR(const K& key,node* root)
{
    if(root==nullptr)
    {
        return nullptr;
    }
    if(root->_key>key)
    {
        return _FindR(key,root->_left);
    }
    else if(root->_key<key)
    {
        return _FindR(key,root->_right);
    }
    else
    {
        return root;
    }
}
public:
node* FindR(const K& key)
{
    _FindR(key,_root);
}
  • 使用迭代查找
public:
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;
}

二叉搜索树的插入

插入的过程如下:

  1. 树为空,则直接插入,然后返回true。
  2. 树不空,按二叉搜索树性质查找插入为止,插入新结点。

在这里插入图片描述

分别用迭代和递归的方法来实现插入结点:

  • 迭代
private:
bool _insertR(const K& key,node* root)
{
    if(root==nullptr)
    {
        root=new node(key);
        return true;
    }
    if(root->_key>key)
    {
        return _insertR(key,root->left);
    }
    else if(root->_key<key)
    {
        return _insertR(key,root->right);
    }
    else
    {
        return false;
    }
}
public:
bool insertR(const K& key)
{
    return _insertR(key,_root);
}
  • 迭代
public:
bool insert(const K& key)
{
    if(_root==nullptr)
    {
        _root=new node(key);
        return true;
    }

    node* cur =_root;
    node* parent=_root;//记录父结点,方便插入结点后进行连接
    while(cur)
    {
        if(root->_key>key)
        {
            parent=cur;
            cur=cur->_left;
        }
        else if(root->_key<key)
        {
            parent=cur;
            cur=cur->_right;
        }
        else
        {
            return false;
        }
    }
    cur=new node(key);
    if(parent->_key>key)
    {
        parent->_left=cur;
    }
    else 
    {
        parent->_right=cur;
    }
    return true;
}

二叉搜索树的删除

二叉搜索树的难点便是在于删除操作,首先依旧是查找元素是否在二叉搜索树中,如果不存在则返回,否则要删除的点可能分为如下三个情况:

  1. 要删除的结点A没有孩子结点,直接删除即可;
  2. 要删除的结点A仅有左孩子或者右孩子,直接将A的子节点连至A的父结点,并将A删除;

在这里插入图片描述

  1. A有两个子节点,用左子树的最大结点B(或者右子树的最小结点)取代A,随后删除B即可,删除B一定可以满足第一或者第二种情况。左子树的最大节点是极易获得的:从左子结点开始一直向右走至底即是。

在这里插入图片描述

我们亦可以使用迭代和递归来删除结点:

  • 递归
private:
bool _EraseR(const K& key,node* root)
{
    if(root==nullptr)
    {
        return false;
    }
    if(root->_key>key)
    {
        return _EraseR(key,root->_left);
    }
    else if(root->_key<key)
    {
        return _EraseR(key,root->_right);
    }
    else//找到删除的元素
    {
        if(root->_right==nullptr)
        {
            node* del=root;
            root=root->_left;
            delete del; 
        }
        else if(root->_left==nullptr)
        {
            node* del=root;
            root=root->_right;
            delete del;
        }
        else//拥有左右子树
        {
            node* maxleft=root->left;
            while(maxleft->_right)
            {
                maxleft=maxleft->_right;
            }
            K tmp=maxleft->_key;
            _Erase(tmp,root->_left);
            root->_key=tmp;
        }
        return true;
    }
}
public:
bool EraseR(const K& key)
{
    return _EraseR(key,_root);
}
  • 迭代
public:
bool Erase(const K& key)
{
    node* cur=_root;
    node* parent=nullptr;
    while(cur)
    {
        if (cur->_key > key)
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (cur->_key < key)
        {
            parent = cur;
            cur = cur->_right;
        }
        else//找到删除点位cur和其父结点parent
        {
            //删除的三种情况
            //有一边为空,那么就用另一边 的孩子直接连上去(parent不为nullptr)
            if(cur->left==nullptr)//左为空
            {
                if(cur==root)
                {
                    _root=cur->_right;
                }
                else
                {
                    if(parent->_left==cur)
                    {
                        parent->_left=cur->_right;
                    }
                    else
                    {
                        parent->_right=cur->_right;
                    }
                }
                delete cur;
                cur=nullptr;
            }
            else if (cur->_right == nullptr)//右为空
            {
                if (cur == _root)
                {
                    _root = cur->_left;
                }
                else
                {
                    if (parent->_left == cur)
                    {
                        parent->_left = cur->_left;
                    }
                    else
                    {
                        parent->_right = cur->_left;
                    }
                }
                delete cur;
                cur = nullptr;
            }
            else//两边都不为空,需要使用替换删除
            {
                //找左子树中的最大结点
                node* Maxleft = cur->_left;
                node* MaxParent = cur;
                while (Maxleft->_right)
                {
                    MaxParent = Maxleft;
                    Maxleft = Maxleft->_right;
                }//循环结束 Maxleft 找到了左子树的最大点
                cur->_key = Maxleft->_key;
                //删除掉Maxleft这个位置的结点
                if (MaxParent->_right == Maxleft)//左子树最大结点在右子树的最右处
                {
                    MaxParent->_right = Maxleft->_left;
                }
                else//左子树本身没有右子树,所以最大结点为左子树的根
                {
                    MaxParent->_left = Maxleft->_left;
                }
                delete Maxleft;
                Maxleft = nullptr;
            }
            return true;
        }
    }
    return false;
}

应用

  1. K模型

    K模型只有一个key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值。

    例如,给一个单词word,判断该单词是否拼写正确:

  • 以单词集合的每个单词作为key,构建二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
  1. KV模型——键值对

    每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。

    该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;

    再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

    比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:

  • <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树键值对比较时只比较Key。
  • 查询英文单词时,只需给出英文单词,就可快速找到与其对应的key。

关于键值对模型的二叉搜索树的代码实现我会置于文末。

性能分析

插入和删除操作之前都必须要先查找,查找效率代表了二叉搜索树中各个操作的性能

有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度与结点在二叉搜索树的深度相关,即结点越深,则比较次数越多。

对于同一个关键码集合,如果各关键码插入次序的不同,可能得到不同结构的二叉搜索树。

在这里插入图片描述

最优情况下,二叉树为完全二叉树,其平均比较次数为:log2N

最差情况下,二叉搜索树为单支树,其平均比较次数为:N/2

所以在每次插入结点时,我们都可以对二叉搜索树的树形进行调整,使其成为一棵平衡的二叉树,这将在后续博客中讲述。


二叉搜索树代码实现(包含K、KV模型)

-end-
青山不改 绿水长流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值