【数据结构】二叉搜索树的实现

一、二叉搜索树的相关概念

1.二叉搜索树的定义

(1)二叉搜索树首先是一颗二叉树。 

(2)树本身和子树都满足左子树的节点比根小,右子树节点比根大。

(3)二叉搜索树节点的值不允许重复。

2.二叉搜索树的使用价值

(1)搜索:最多查找高度次。

(2)排序:中序遍历是有序的,所以搜索二叉树也叫做排序二叉树。

二、二叉搜索树的操作

1.二叉搜索树的查找

//查找
Node* Find(const K& key)
{
    Node* cur = _root;
    while(cur)
    {
        //找小的,去左边找
        if(key < cur->_key)
            cur = cur->_left;
        //找大的,去右边找
        else if(key > cur->_key)
            cur = cur->_right;
        //找到相等的了,返回true
        else
            return cur;
    }
    //走到空都没有,就证明没有
    return nullptr;
}

2.二叉搜索树的插入

(1)树为空:直接插入。

(2)树不为空:按照二叉搜索树的性质找到要插入的位置,然后插入。

//插入
bool Insert(const K& key)
{
    //树为空,直接插入
    if(_root == nullptr)
    {
        _root = new Node(key);
        return true;
    }
    //树不为空
    Node* cur = _root;
    Node* parent = nullptr;
    //找到要插入的位置
    while(cur)
    {
        //插小的
        if(key < cur->_key)
        {
            parent = cur;
            cur = cur->_left;
        }
        //插大的
        else if(key > cur->_key)
        {
            parent = cur;
            cur = cur->_right;
        }
        //插一样的(不允许有一样的)
        else
        {
            return false;
        }
    }
    //插入
    cur = new Node(key);
    //连接
    if(key < parent->_key)
    {
        parent->_left = cur;
    }
    else
    {
        parent->_right = cur;
    }
    return true;
}

3.二叉搜索树的删除

可以分为以下四种情况

(1)删除叶子节点:直接删除

(2)要删除的节点 左子树为空:父亲指向我的右

(3)要删除的节点 右子树为空:父亲指向我的左

(4)要删除的节点 左右子树都不为空:不能直接删除,使用替换法删除

找左子树最大节点(左子树的最右节点)或右子树的最小节点(左子树的最左节点)去替代他

但是删除叶子节点可以归类到上面提到的(2)(3)类中,所以删除只分为以下三种情况

(1)要删除的节点 左子树为空:父亲指向我的右

(2)要删除的节点 右子树为空:父亲指向我的左

(3)要删除的节点 左右子树都不为空:不能直接删除,使用替换法删除

展示删除的完整代码

//删除(分成三种情况,详细见博客)
bool Erase(const K& key)
{
    Node* parent = nullptr;
    Node* cur = _root;
    //找到要删除的位置
    while(cur)
    {
        //删除小的,去左边删
        if(key < cur->_key)
        {
            parent = cur;
            cur = cur->_left;
        }
        //删除大的,去右边删
        else if(key > cur->_key)
        {
            parent = cur;
            cur = cur->_right;
        }
        //找到要删的了,开始删除(cur指向要被删除的节点)
        else
        {
            //1.左为空
            if(cur->_left == nullptr)
            {
                //删除根节点(因为删除根节点,_root会发生改变)
                if(cur == _root)
                {
                    _root = cur->_right;
                }
                //删除不是根的节点
                else
                {
                    //连接新节点
                    if(cur == parent->_left)
                        parent->_left = cur->_right;
                    else
                        parent->_right = cur->_right;
                }
                //删除
                delete cur;
            }
            //2.右为空
            else if(cur->_right == nullptr)
            {
                //删除根节点(因为删除根节点,_root会发生改变)
                if(cur == _root)
                {
                    _root = cur->_left;
                }
                //删除不是根的节点
                else
                {
                    //连接新节点
                    if(cur == parent->_left)
                        parent->_left = cur->_left;
                    else
                        parent->_right = cur->_left;
                }
                //删除
                delete cur;
            }
            //3.左右都不为空
            else
            {
                Node* rightMin = cur->_right;//最终要指向右子树的最小的节点
                Node* rightMinParent = cur;
                //找到右子树最小的节点
                while(rightMin->_left)
                {
                    rightMinParent = rightMin;
                    rightMin = rightMin->_left;
                }
                //节点替换:将找到的右子树最小的节点 和 要删除的节点交换
                //std::swap(cur->_key,rightMin->_key);
                cur->_key = rightMin->_key;//把cur改了就行,反正rightMin也要被删掉了
                //转换成删除rightMin:只会出现情况一和情况二
                //rightMin是左孩子
                if(rightMin == rightMinParent->_left)
                    rightMinParent->_left = rightMin->_right;
                //rightMin是右孩子
                else
                    rightMinParent->_right = rightMin->_right;
                delete rightMin;
            }
            //到这成功删除,返回true
            return true;
        }
    }
    //走到这没找到要删除的节点,返回false
    return false;
}

删除的大体框架:1.先找到要删除的节点  2.删除节点

先找到要删除的节点(cur会指向要删除的节点)

//删除的大体框架
bool Erase(const K& key)
{
    Node* parent = nullptr;
    Node* cur = _root;
    //找到要删除的位置
    while(cur)
    {
        //删除小的,去左边删
        if(key < cur->_key)
        {
            parent = cur;
            cur = cur->_left;
        }
        //删除大的,去右边删
        else if(key > cur->_key)
        {
            parent = cur;
            cur = cur->_right;
        }
        //找到要删的了,开始删除(cur指向要被删除的节点)
        else
        {
            //开始删除
        }
    }
    //走到这没找到要删除的节点,返回false
    return false;
}

删除cur所指向的节点

1.要删除的节点 左子树为空:父亲指向我的右

下列两个图对应解释了出现的两种情况

(1)如果要删除的节点是根节点

(2)要删除的节点有可能是左孩子也可能是右孩子,这就导致重新连接时会有不同

//1.左为空
if(cur->_left == nullptr)
{
    //删除根节点(因为删除根节点,_root会发生改变)
    if(cur == _root)
    {
        _root = cur->_right;
    }
    //删除不是根的节点
    else
    {
        //连接新节点
        if(cur == parent->_left)
            parent->_left = cur->_right;
        else
            parent->_right = cur->_right;
    }
    //删除
    delete cur;
}

2.要删除的节点 右子树为空:父亲指向我的左

和第一种情况差不多,所以就不在这里配图了,整体的逻辑都是一样的。

//2.右为空
else if(cur->_right == nullptr)
{
    //删除根节点(因为删除根节点,_root会发生改变)
    if(cur == _root)
    {
        _root = cur->_left;
    }
    //删除不是根的节点
    else
    {
        //连接新节点
        if(cur == parent->_left)
            parent->_left = cur->_left;
        else
            parent->_right = cur->_left;
    }
    //删除
    delete cur;
}

3.要删除的节点 左右子树都不为空:不能直接删除,使用替换法删除

(1)首先找到用谁替换要被删除的节点:左子树的最大节点/右子树的最小节点

左子树的最大节点:左子树最右边的节点    右子树的最小节点:右子树最左边的节点

(2)节点替换:将找到的左子树的最大节点/右子树的最小节点 和 要删除的节点交换

(3)转换成删除 左子树的最大节点/右子树的最小节点 这时的删除一定满足情况1或情况2

下面用图来解释:我们选择用 右子树的最小节点 替换被删节点

同样,这里也要区分被删除的节点是左孩子还是右孩子

//3.左右都不为空
else
{
    Node* rightMin = cur->_right;//最终要指向右子树的最小的节点
    Node* rightMinParent = cur;
    //找到右子树最小的节点
    while(rightMin->_left)
    {
        rightMinParent = rightMin;
        rightMin = rightMin->_left;
    }
    //节点替换:将找到的右子树最小的节点 和 要删除的节点交换
    //把cur改了就行,反正rightMin也要被删掉了
    cur->_key = rightMin->_key;
    //转换成删除rightMin:只会出现情况一和情况二
    //rightMin是左孩子
    if(rightMin == rightMinParent->_left)
        rightMinParent->_left = rightMin->_right;
    //rightMin是右孩子
    else
        rightMinParent->_right = rightMin->_right;
    delete rightMin;
}

三、二叉搜索树的应用

1.K模型

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

key模型 -> 判断key在不在(这个比较简单,不在举例说明)

//Binary Search Tree Node
template<class K>
struct BSTreeNode{
public:
    BSTreeNode(const K& key = K())
        :_left(nullptr)
        ,_right(nullptr)
        ,_key(key)
    {}
public:
    BSTreeNode<K>* _left;
    BSTreeNode<K>* _right;
    K _key;
};

//Binary Search Tree
template<class K>
class BSTree{
public:
    typedef BSTreeNode<K> Node;
public:
    //构造函数
    BSTree();
    //析构函数
    ~BSTree();
    //查找
    bool Find(const K& key);
    //插入
    bool Insert(const K& key);
    //删除
    bool Erase(const K& key);

private:
    Node* _root;
};

2.KV模型

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

key/value模型 -> 通过key找对应的value -> 中英词典互译/统计次数等等

下面的代码只展示KV模型搜索树的大体框架,具体实现和K模型搜索树基本一致

//Binary Search Tree Node KV
template<class K, class V>
struct BSTreeNodeKV{
public:
    BSTreeNodeKV(const K& key = K(), const V& value = V())
        :_left(nullptr)
        ,_right(nullptr)
        ,_key(key)
        ,_value(value)
    {}
public:
    BSTreeNodeKV<K, V>* _left;
    BSTreeNodeKV<K, V>* _right;
    K _key;
    V _value;
};

//Binary Search Tree
template<class K, class V>
class BSTreeKV{
public:
    typedef BSTreeNodeKV<K, V> Node;
public:
    //构造函数
    BSTreeKV();
    //析构函数
    ~BSTreeKV();
    //查找(是查找key)
    Node* Find(const K& key);
    //插入(把<key,value>插入进去)
    bool Insert(const K& key, const V& value);
    //删除(删除key对应的节点)
    bool Erase(const K& key);

private:
    Node* _root;
};

(1)中英词典互译:英文单词与其对应的中文<word, chinese>就构成一种键值对

void TestBSTreeKVdict()
{
    //字典
    BSTreeKV<string, string> dict;
    dict.Insert("sort", "排序");
    dict.Insert("string", "字符串");
    dict.Insert("tree", "树");
    dict.Insert("insert", "插入");
    
    string str;
    while (cin >> str) {
        BSTreeNodeKV<string, string>* ret = dict.Find(str);
        if(ret)
            cout << ret->_value << endl;
        else
            cout << "没有这个单词" << endl;
    }
}

(2)统计次数:水果种类和次数<fruit, count>就构成一种键值对

void TestBSTreeKVcount()
{
    string strArr[] = {"西瓜", "苹果", "香蕉","西瓜", "苹果", 
            "香蕉","西瓜", "苹果", "香蕉", "西瓜","西瓜","西瓜"};
    BSTreeKV<string, int> countTree;
    for(auto str:strArr)
    {
        BSTreeNodeKV<string, int>* ret = countTree.Find(str);
        //如果计数的树中还没有,将新水果插入
        if(ret == nullptr)
            countTree.Insert(str, 1);
        //计数的树中已经有了这种水果,value++
        else
            ret->_value++;
    }
    countTree.InOrder();
}

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

插入的数据是有序或者接近有序的时候,搜索树的效率就完全没办法保证了(因为出现右单支)

在这种最坏的情况下,搜索树的效率是o(N) -> 如何解决?平衡树:AVLTree/红黑树。

当然如果是完全二叉树这种情况,搜索树的效率是o(logN)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值