二叉搜索树、AVL树、红黑树

在这里插入图片描述
为者常成,行者常至

二叉搜索树

二叉搜索树就是在二叉树的基础上增加一些规则,使该结构具备某些规则方便查找,提升查找效率。

规则:

  1. 右子节点都比父节点大
  2. 左子节点都比父节点小

image-20240406163804365

节点

标准二叉树的节点定义,一个值,两个指针。

	template<class K>
	struct TreeNode
	{
		TreeNode(const K& key)
			:_key(key)
			,_left(nullptr)
			,_right(nullptr)
		{}

		K _key;
		TreeNode* _left;
		TreeNode* _right;
	};

查找

二叉搜索树从名字就可以看出搜索就是它的特色,如上图,例如要查找值为16的节点。

image-20240406184953235

首先在根节点进行比较,查找的值16小于根节点17,因此往根节点左边找,然后到值为12的节点,发现查找值16大于该节点值12,因此往右边找,到了节点15,又发现查找值比15大,再往右边走,到了值为16的节点,查找到了然后返回。

如果一直查找到空还没有匹配的值,那么就认为没有这个值,没有查找到。

// 根节点指针 _root 是指向根节点的指针 
bool Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_key > key) cur = cur->_left;
        else if (cur->_key < key) cur = cur->_right;
        else return true;
    }
    return false;
}

插入

插入操作和查找操作差不多,插入也需要找到一个合适的位置,然后链接即可。

image-20240406190124340

如上,插入一个值为16的节点,需要从根节点向下逐层比较,插入值小于节点值就往左走,反之往右走,直到合适位置。

合适位置的判断一般有两种方法:

  1. 用前后指针,cur和prev指针,当cur走到null的时候,prev就是链接节点,只需要比较插入值与prev值的大小来确定插入的位置。
  2. 边走边判断,如果当前位置的值比插入值小并且左边为空,那么左边就是插入位置,反之右边也是一样。
bool InSert(const K& key)
{
    Node* node = new Node(key); // new一个新增节点
    if (_root == nullptr)  // 链表为空,直接动_root
    {
        _root = node;
        return true;
    }
    // 不为空,查找合适插入位置
    //前后指针,大于往右,小于往左
    Node* prev = nullptr;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_key > key) 
        {
            prev = cur;
            cur = cur->_left;
        }
        else if (cur->_key < key)
        {
            prev = cur;
            cur = cur->_right;
        }
        else //如果找到值相等已经存在的节点,默认插入成功
        {
            delete node; //记住释放刚才new的节点,不然内存泄漏
            return true;
        }
    }
    //到这里就已经找到了合适位置,prev指向的节点就是链入节点
    if (prev->_key > key) prev->_left = node;
    else prev->_right = node;
    return true;
}

重头戏——删除

查找和插入都是开胃菜,删除才是主食,删除分为3种情况

  1. 删除节点为叶子结点,即左右孩子都为nullptr
  2. 删除节点有一个孩子
  3. 删除节点有两个孩子

叶子节点

image-20240406192457332

叶子结点的删除最为简单,无需考虑其他情况直接将它的父节点链接它的指针指向nullptr就可以了,然后delete该节点。

if (cur->_left == nullptr && cur->_right == nullptr) //叶子结点
{
    if (prev == nullptr) _root = nullptr; // 如果prev为空代表删除的节点为根节点
    else if (prev->_left == cur) prev->_left = nullptr;
    else prev->_right = nullptr;
    delete cur; 
    cur = nullptr;
}

只有一个子节点

在这里插入图片描述

当删除节点只有一个子节点,那么就只需要将删除节点的父节点指向它的指针移动到指向它的唯一孩子上即可(无论左右孩子)

if ((cur->_left == nullptr && cur->_right) || (cur->_left && cur->_right == nullptr)) // 单孩子节点
{
    if (prev == nullptr) //删除节点为根节点
    {
        if (cur->_left) _root = cur->_left;
        else _root = cur->_right;
    }
    else if (prev->_left == cur) //父节点的左边指向删除节点
    {
        if (cur->_left) prev->_left = cur->_left;
        else prev->_left = cur->_right;
    }
    else //父节点的右边指向删除节点
    {
        if (cur->_left) prev->_right = cur->_left;
        else prev->_right = cur->_right;
    }
    delete cur;
    cur = nullptr;
}

有两个子节点

image-20240406193526989

当删除有两个孩子的节点时,不能直接在该节点上动手脚,需要移花接木一下,有种方法进行移花接木

  1. 找左子树最大节点替换
  2. 找右子树最小节点替换

在找到合适的替死鬼之后,就可以直接删除替死鬼节点了,因为无论是左子树最大节点还是右子树最小节点再替换后都不会打乱二叉搜索树的特性,且这两个节点必定是叶子结点或者只有一个子节点,此时删除两个子节点的操作就转移为删除叶子结点或者单字节点。

//三指针,当cur为空时,sprev刚好指向替死鬼节点
// spprev指向sprev的父节点
Node* scur = cur->_right;  // 找右子树最小节点
Node* sprev = cur;
Node* spprev = prev;
while (scur)
{
    spprev = sprev;
    sprev = scur;
    scur = scur->_left;
}
cur->_key = sprev->_key; //替换
if (spprev->_left == sprev) spprev->_left = sprev->_right;
else spprev->_right = sprev->_right;
delete sprev;

分析

二叉搜索树的约束条件非常简单,因此查找的性能不稳定,当插入较为随机时查找的效率可以达到O(logn),但是当插入不够随机时,例如当插入序列为顺序或者逆序,就会变成歪脖子树,查找效率降低为O(n)

image-20240406195748555

平衡二叉搜索树

因为二叉搜索树可能会退化为歪脖子树,导致查找效率降低为O(n),因此平衡二叉搜索树就是来解决这个问题。平衡二叉搜索树简称AVL树,AVL树就是在二叉搜索树的基础上增加一个条件

任何节点的左右子树高度差值不能大于1

通过这个条件来保证“相对平衡”状态,一般用平衡因子来表示这个条件。

image-20240407123212453

如下图,开始树的每个节点左右子树高度相差都不大于1,且维持了二叉搜索树的特性,因此这个树是一个AVL树。

但是当在新增一个值为11的节点时,那么插入这个节点的祖先节点的树高都会受到影响,因此值为18和值为22的节点左右子树的高度差为2,打破AVL树的特性。

当插入节点时,树的AVL特性被打破就需要做一些处理,让这棵树又变为AVL树,这个处理方式分4种,单旋和双旋

  1. 左单旋
  2. 右单旋
  3. 左单旋 + 右单旋
  4. 右单旋 + 左单旋

右单旋

image-20240407130709296

如上,当插入值为11的节点时,AVL树的相对平衡被打破,执行右单旋,k2节点的左接收k1节点的右子节点,然后降低高度成为k1的右子节点。

左单旋情况类似

左右双旋

image-20240407131447483

当插入值为15时,打破AVL树相对平衡状态,执行左右双旋操作,先对k3节点的左子节点k1执行左单旋,然后再对k3执行右单旋,大功告成。

右左单旋情况类似

插入的四种情况

四种旋转对应着插入的4种情况(四种情况都是破坏了AVL树"相对平衡"的情况,没有破坏则不需要旋转)

  1. 插入左子树的左边——左左(右单旋)
  2. 插入左子树的右边——左右(左右双旋)
  3. 插入右子树的左边——右左(右左双旋)
  4. 插入右子树的右边——右右(左单旋)

左左

在这里插入图片描述

插入该节点的左子树的左边,简称左左,执行右单旋。最近受影响节点接收左子树的右子节点,并成为左子树的右子节点,降低高度。

最近受影响节点即为从插入节点开始,依次更新它的祖先节点,当第一个被打破“相对平衡”状态的祖先节点

void RotateR(Node* parent)
{
    Node* subL = parent->_left; 
    Node* subLR = subL->_right; 
    parent->_left = subLR;
    if (subLR) subLR->_parent = parent;
    
    Node* ppnode = parent->_parent;
    subL->_right = parent;
    parent->_parent = subL;
    if (parent == _root)
    {
        _root = subL;
        _root->_parent = nullptr;
    }
    else
    {
        if (ppnode->_left == parent) ppnode->_left = subL; 
        else ppnode->_right = subL;
        
        subL->_parent = ppnode;
    }
    subL->_bf = parent->_bf = 0;
}

右右

image-20240407133458938

插入该节点的右子树的右边,简称右右,执行左单旋。最近受影响节点接收它右子树的左子节点,并成为右子树的左子节点,降低高度。

void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    parent->_right = subRL;
    if (subRL) subRL->_parent = parent;
        
    Node* ppnode = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

    if (ppnode == nullptr)
    {
        _root = subR;
        _root->_parent = nullptr;
    }
    else
    {
        if (ppnode->_left == parent)
        {
            ppnode->_left = subR;
        }
        else
        {
            ppnode->_right = subR;
        }
        subR->_parent = ppnode;
    }

    parent->_bf = subR->_bf = 0;
}

左右

image-20240407134500420

插入左子树的右子节点里,这种情况单靠一种单旋是没办法解决问题的,因此需要借助两次单旋来处理。首先将最近受影响节点的左子节点进行左单旋,旋转之后的结果就和右单旋的插入情况一样,然后进行右单旋,降低高度。

void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;

    RotateL(parent->_left);
    RotateR(parent);

    if (bf == 1)
    {
        subL->_bf = -1;
    }
    else if (bf == -1)
    {
        parent->_bf = 1;
    }
}

右左

image-20240407140620012

插入右子树的左子节点里,这种情况单靠一种单旋也是没办法解决问题的,因此需要借助两次单旋来处理。首先将最近受影响节点的右子节点进行右单旋,旋转之后的结果就和左单旋的插入情况一样,然后进行左单旋,降低高度。

void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;

    RotateR(parent->_right);
    RotateL(parent);

    if (bf == 1)
    {
        parent->_bf = -1;
    }
    else if (bf == -1)
    {
        subR->_bf = 1;
    }
}

插入操作

综合四种旋转和四种插入情况,即可得插入操作

bool InSert(const K& key, const V& val)
{
    Node* node = new Node(key, val);
    if (_root == nullptr)
    {
        _root = node;
        return true;
    }
    Node* prev = nullptr;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_key > key)
        {
            prev = cur;
            cur = cur->_left;
        }
        else if (cur->_key < key)
        {
            prev = cur;
            cur = cur->_right;
        }
        else
        {
            delete node;
            return true;
        }
    }
    if (prev->_key > key)
    {
        prev->_left = node;
    }
    else
    {
        prev->_right = node;
    }
    node->_parent = prev;
    upDate_In(node, prev);
    return true;
}

void upDate_In(Node* cur, Node* parent)
{
    while (parent)
    {
        if (parent->_left == cur) parent->_bf--;
        else parent->_bf++;
        if (parent->_bf == 0) return;
        if (parent->_bf == -2 && cur->_bf == -1)
        {
            RotateR(parent);
            return;
        }
        else if (parent->_bf == -2 && cur->_bf == 1)
        {
            RotateLR(parent);
            return;
        }
        else if (parent->_bf == 2 && cur->_bf == 1)
        {
            RotateL(parent);
            return;
        }
        else if(parent->_bf == 2 && cur->_bf == -1)
        {
            RotateRL(parent);
            return;
        }
        else
        {
            cur = parent;
            parent = parent->_parent;
        }
    }
}

小结

image-20240407172740558

虽说平衡二叉搜索树的查找效率为O(logn),治好了二叉搜索树的歪脖子病,但是其过分严格的限制左右子树的高度差不能大于1,导致再插入和删除数据时会多次进行旋转,影响性能。

红黑树

红黑树相较于AVL树,它没有那么严格的平衡要求,因此不会进行大量频繁的旋转,而且相较于搜索二叉树,他又有自己的约束条件来维持相对平衡状态,因此红黑树通常是个不错的选择,map和set容器都是以红黑树作为底层容器。

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 不存在两个相邻的红色节点。
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

image-20240407212326038

更多红黑树介绍直接跳转http://t.csdnimg.cn/CUgRR,文章里有红黑树的插入操作的详解,这里直接给出插入代码

enum Color  //枚举颜色
{
    RED,
    BLACK
};

template <class K, class V>
struct RB_Node
{
    typedef std::pair<K, V> Pair;
    typedef RB_Node<K, V> Node;
    RB_Node(const Pair& node)
        :_val(node)
        ,_col(RED)
        ,_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
	{}

    Pair _val;
    Color _col;
    Node* _left;
    Node* _right;
    Node* _parent;
};

void InSert(const K& key, const V& val)
{
    if (_root == nullptr)
    {
        _root = new Node({ key, val });
        _root->_col = BLACK;
        return;
    }

    Node* cur = _root;
    Node* prev = nullptr;
    while (cur)
    {
        if (key > cur->_val.first)
        {
            prev = cur;
            cur = cur->_right;
        }
        else if (key < cur->_val.first)
        {
            prev = cur;
            cur = cur->_left;
        }
        else return;
    }

    Node* node = new Node({ key, val });
    if (prev->_val.first > key)
    {
        prev->_left = node;
    }
    else
    {
        prev->_right = node;
    }
    node->_parent = prev;
    if (prev->_col == RED)
    {
        UpDate(prev, node);
    }
}

void UpDate(Node* parent, Node* cur)
{
    while (parent && parent->_col == RED)
    {
        Node* grandparent = parent->_parent;
        Node* uncle = nullptr;
        if (grandparent->_left == parent) uncle = grandparent->_right;
        else uncle = grandparent->_left;

        if (!uncle || uncle->_col == BLACK)
        {
            if (grandparent->_left == parent && parent->_left == cur)
            {
                RevoleR(grandparent);
                parent->_col = BLACK;
                grandparent->_col = RED;
            }
            else if (grandparent->_right == parent && parent->_right == cur)
            {
                RevoleL(grandparent);
                parent->_col = BLACK;
                grandparent->_col = RED;
            }
            else if (grandparent->_left == parent && parent->_right == cur)
            {
                RevoleLR(grandparent);
                grandparent->_col = RED;
                cur->_col = BLACK;
            }
            else if (grandparent->_right == parent && parent->_left == cur)
            {
                RevoleRL(grandparent);
                grandparent->_col = RED;
                cur->_col = BLACK;
            }
            else assert(0);
            if (grandparent->_parent == nullptr) grandparent->_col = BLACK;
            return;
        }
        else if (uncle && uncle->_col == RED)
        {
            parent->_col = BLACK;
            uncle->_col = BLACK;
            grandparent->_col = RED;
            parent = grandparent->_parent;
            cur = grandparent;
        }
    }
    if (parent == nullptr)
    {
        cur->_col = BLACK;
    }
}

void RevoleLR(Node* grandparent)
{
    RevoleL(grandparent->_left);
    RevoleR(grandparent);
}
void RevoleRL(Node* grandparent)
{
    RevoleR(grandparent->_right);
    RevoleL(grandparent);
}
void RevoleL(Node* grandparent)
{
    Node* subR = grandparent->_right;
    Node* subRL = subR->_left;
    Node* ggrand = grandparent->_parent;

    grandparent->_right = subRL;
    if (subRL) subRL->_parent = grandparent;
    subR->_left = grandparent;
    grandparent->_parent = subR;

    if (ggrand)
    {
        if (ggrand->_left == grandparent)
            ggrand->_left = subR;
        else
            ggrand->_right = subR;
    }
    else
    {
        _root = subR;
    }
    subR->_parent = ggrand;
}
void RevoleR(Node* grandparent)
{
    Node* subL = grandparent->_left;
    Node* subLR = subL->_right;
    Node* ggrand = grandparent->_parent;

    grandparent->_left = subLR;
    if (subLR) subLR->_parent = grandparent;
    subL->_right = grandparent;
    grandparent->_parent = subL;

    if (ggrand)
    {
        if (ggrand->_left == grandparent)
            ggrand->_left = subL;
        else
            ggrand->_right = subL;
    }
    else
    {
        _root = subL;
    }
    subL->_parent = ggrand;
}
private:
Node* _root;
};
  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

拖拉机厂第一代码手

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

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

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

打赏作者

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

抵扣说明:

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

余额充值