【手撕红黑树】

简介: 【手撕红黑树】

前言

相信很多人初学者听到了红黑树后心中不免有些心慌,那你看到了这篇文章后相信会有所收获,我其实刚开始也是对红黑树抱着一种害怕甚至是恐惧,但是在老师的帮助下也终于慢慢的不在恐惧了,你想知道为什么的话就继续往下看吧。(注意本篇博客只讲解了红黑树的插入,没有讲解红黑树的删除,删除比插入还要难一些,为了更好的阅读体验,就不再讲解了)

1 红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

如果说AVL树是大佬设计的话,那么红黑树就是大佬中的大佬设计出来的,为什么这么说呢,我们接下来慢慢看。


2 红黑树的性质

每个结点不是红色就是黑色

根节点是黑色的

如果一个节点是红色的,则它的两个孩子结点是黑色的

对于每个结点,从该结点到其所有后代叶结点的简单路径上均包含相同数目的黑色结点

每个叶子结点都是黑色的 (此处的叶子结点指的是空结点)

我们先不要看最后一条性质,其他性质中比较重要的就是性质三和性质四,我们可以用自己的话来解读性质三和性质四:

性质三的意思是没有连续红色结点

性质四的意思是每条路径下的黑色结点数量是相等的

大家思考一下,为什么满足了上面性质就能够保证红黑树中最长路径中节点个数不会超过最短路径节点个数的两倍?

我们可以从极限的条件下来判断:

最短路径是全黑,最长路径是红黑相间,由于要满足性质三和性质四所以最长路径除以最短路径最大也不会超过二倍。

a9f2643aad824b6b8511db274c39f0d2.png

我们再来看看最后一个性质,有些教科书上可能会有NIL结点的定义,并且把颜色定义为黑色,注意这里的NIL结点并不是一个真正有效的节点,而是一个空结点。通过每条空结点来标识每一条路径,如在上图中就存在着11条路径。

通过红黑树的性质我们也不难发现,其实红黑树的平衡并没有AVL树那么严格,因为红黑树只需要保证最长路径的结点个数不会超过最短路径节点个数两倍即可,但是AVL树要求着所有子树高度差绝对值不超过1。这就导致了红黑树的旋转条件是比AVL树更加苛刻的,也就是在同等条件下红黑树旋转次数是有较大机率低于AVL树的,那么红黑树的性能肯定是比AVL要好上一些的(旋转是有代价的),如果还没有了解过旋转,建议先看看博主的上一篇博客:

[AVL树的旋转]

3 红黑树的模拟实现

3.1 节点的定义

这个很简单,我们已经讲解过很多次了:

#pragma once
enum Corlor
{
  RED,
  BLACK
};
template<class K, class V>
struct RBTreeNode
{
  RBTreeNode<K, V>* _left;
  RBTreeNode<K, V>* _right;
  RBTreeNode<K, V>* _parent;
  pair<K, V> _kv;
  Corlor _corlor;
  RBTreeNode(const pair<K, V>& kv)
    :_left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _kv(kv)
    , _corlor(RED)
  {}
};
template<class K, class V>
class RBTree
{
  typedef RBTreeNode<K, V> Node;
private:
  Node* _root = nullptr;
};

这里问题就来了,新节点给的颜色是给红色还是给黑色呢?

给红色的话就可能违背性质三,给黑色结点就违背性质四。如果是你你想违背哪个性质?

这里给新节点的默认颜色为红色比较好,为什么呢?

给出红色结点的话,我们可能就只需要调整本路径下结点颜色,但是给黑色结点的话其他路径黑色结点就不相等了,调整的代价肯定更大。所以新节点的默认颜色给红色是比较合理的。


1aa3b32619e3485caa5e09f57986022a.png

我们观察上图,如果我们在11 或者15 下插入新节点,那么这就太好了,不需要进一步调整,插入后还是一颗红黑树,但是我们想要在6 22 27下面插入新节点的话,就要调整了,具体怎样调整我们下面会详细讲解。

3.2 分类

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

红黑树调整分类的关键就是看叔叔

3.2.1 情况一

cur为红,p为红,g为黑,u存在且为红


0a883e2065dc48629d9ee494cecfe5b8.png

老规矩,我们这里画的仍然是抽象图,表示无数种情况,但是他们都可以用同一种方法来解决。

此时我们只需要将p和u置黑,将g置红,然后接着往上调整。

为什么要继续往上调整呢?通过观察我们可以很容易分析出问题所在,这样一次调整了后各个路径上黑色结点数目并没有发生改变,但是有可能g结点的父亲结点是红色的,而导致又出现了连续红色结点(注意,调整了后有可能再次调整使用的方法不在是第一种情况)


ecba8616b3c54a5194d179ef42008082.png

继续向上调整的话,直到不满足连续红色结点或者已经调整到了根节点。

3.2.2 情况二

cur为红,p为红,g为黑,u不存在/u存在且为黑


e20ade4fce5c430aa6ad5f462024569d.png


我们先分析第一种情况:u不存在。

如果u不存在的话,那么cur一定是新增。此时光变色已经无法解决问题了,因为此时已经不满足最长路径中节点个数不会超过最短路径节点个数的两倍,就需要旋转处理:


2c42c45866ef4092bd487116f463d719.png

这里处理方式是:右单旋+p变黑,g变红。

也有可能p和cur都在右边且在一条直线上,所以处理方式可能是:左单旋+p变黑,g变红。

总结本次调整方法为:单旋+p变黑,g变红

同理,当u存在且为黑的时候仍然是同种处理方式:


62d372d7e34e480088d257e9bef10741.png

旋转变色完以后就不用再向上更新了

3.2.3 情况三

cur为红,p为红,g为黑,u不存在/u存在且为黑

等等,这种方式不就是第二种方式吗?

别着急,我们先来看看图:



2feb54497bda4345b2913ec0dd7a29ff.png

从图片中我们不难发现,g p cur 三者并没有在一条直线,而是一种折线关系,这种情况我们只是单旋就处理不了了,在这张图中我们先以p点进行左单旋,在以g点进行右单旋上。

(注意,我们在b和c下面插入结点导致引起上面这种关系的都可以用这种方式处理)


edc4bb3f42e44e00b901497fb6c2f3dd.png

d43d41edb48a41659b69c242e719b1b7.png

当然,不只是这一种情况,还可能是反着来,那么处理方式就可能是:

以p右单旋+以g左单旋+cur变黑,g变红。

所以这次情况处理方式是:双旋+cur变黑,g变红

旋转变色完以后就不用再向上更新了

3.3 代码实现

bool insert(const pair<K, V>& kv)
  {
    if (_root == nullptr)
    {
      _root = new Node(kv);
      _root->_corlor = BLACK;
      return true;
    }
    Node* cur = _root;
    Node* parent = nullptr;
    while (cur)
    {
      if (cur->_kv.first > kv.first)
      {
        parent = cur;
        cur = cur->_left;
      }
      else if (cur->_kv.first < kv.first)
      {
        parent = cur;
        cur = cur->_right;
      }
      else
      {
        return false;
      }
    }
    cur = new Node(kv);
    if (parent->_kv.first > kv.first)
    {
      parent->_left = cur;
      cur->_parent = parent;
    }
    else
    {
      parent->_right = cur;
      cur->_parent = parent;
    }
    while (parent && parent->_corlor == RED)
    {
      Node* grand = parent->_parent;
      assert(grand);
      assert(grand->_corlor == BLACK);
      if (grand->_left == parent)
      {
        Node* uncle = grand->_right;
        //情况一:uncle存在且为红
        if (uncle && uncle->_corlor == RED)
        {
          uncle->_corlor = parent->_corlor = BLACK;
          grand->_corlor = RED;
          //继续往上处理
          cur = grand;
          parent = cur->_parent;
        }
        else
        {
          //情况二和三:
          //1 :parent->_left==cur(右单旋+变色)
          //      g
          //   p     u
          //c
          if (parent->_left == cur)
          {
            RotateR(grand);
            parent->_corlor = BLACK;
            grand->_corlor = RED;
          }
          else
          {
            //2 :parent->_right==cur(左单旋+右单旋+变色)
            //      g
            //   p     u
            //      c
            RotateL(parent);
            RotateR(grand);
            cur->_corlor = BLACK;
            grand->_corlor = RED;
          }
          //此时已经旋转变色完成,可以break出去
          break;
        }
      }
      else//grand->_right=parent
      {
        Node* uncle = grand->_left;
        //情况一:uncle存在且为红
        if (uncle && uncle->_corlor == RED)
        {
          uncle->_corlor = parent->_corlor = BLACK;
          grand->_corlor = RED;
          //继续往上处理
          cur = grand;
          parent = cur->_parent;
        }
        else
        {
          //情况二和三:
          //1 :parent->_right==cur(左单旋+变色)
          //      g
          //   u     p
          //            c
          if (parent->_right == cur)
          {
            RotateL(grand);
            parent->_corlor = BLACK;
            grand->_corlor = RED;
          }
          else
          {
            //2 :parent->_left==cur(右单旋+左单旋+变色)
            //      g
            //   u     p
            //      c
            RotateR(parent);
            RotateL(grand);
            cur->_corlor = BLACK;
            grand->_corlor = RED;
          }
          //此时已经旋转变色完成,可以break出去
          break;
        }
      }
    }
    _root->_corlor = BLACK;
    return true;
  }

3.4 红黑树的验证

验证的话我们要从红黑树的性质开始着手,只要满足了红黑树的几个性质自然就没啥问题.

最主要的验证是性质三和性质四:

//bool prevCheck(Node* root, int blackCnt, int& benchmark)
  bool prevCheck(Node* root, int blackCnt, int benchmark)
  {
    if (root == nullptr)
    {
      /*if (benchmark == 0)
      {
        benchmark=blackCnt;
        return true;
      }*/
      if (benchmark != blackCnt)
      {
        cout << "每条路径上的黑色结点不相等" << endl;
        return false;
      }
      else
      {
        return true;
      }
    }
    if (root->_corlor == BLACK)
    {
      ++blackCnt;
    }
    if (root->_corlor == RED && root->_parent->_corlor == RED)
    {
      cout << "有连续的红色结点" << endl;
      return false;
    }
    return prevCheck(root->_left, blackCnt, benchmark)
      && prevCheck(root->_right, blackCnt, benchmark);
  }
bool isbalance()
  {
    if (_root && _root->_corlor == RED)
    {
      cout << "根节点不是黑色" << endl;
      return false;
    }
    int benchMark = 0;
    Node* cur = _root;
    while (cur)
    {
      if (cur->_corlor == BLACK)
        ++benchMark;
      cur = cur->_left;
    }
    int blackCnt = 0;
    return prevCheck(_root, blackCnt, benchMark);
  }

处理方式有很多种,像每条路径下的黑色节点我们可以一次性先算出来然后传参数即可,也可以不算出来,传参数引用来修改。具体方式大家可以自行选择。

大家测试时最好多用几组随机数测测。

5 总结

大家看到了这里相信也对红黑树有了一个谱,其实说难吧,感觉还没有刚学AVL的旋转难,关键是如何把图画好,跟着图一步一步的来,大概率是不会出错的。如果该文对你有帮助的话能不能一键三联支持博主呢

目录
相关文章
|
6月前
|
算法 C++
【数据结构与算法】—— 手撕红黑树
【数据结构与算法】—— 手撕红黑树
|
4月前
|
Java C++ Python
【C++】手撕红黑树
【C++】手撕红黑树
23 1
|
5月前
|
存储 算法 C++
【数据结构与算法】:带你手搓顺序表(C/C++篇)
【数据结构与算法】:带你手搓顺序表(C/C++篇)
|
6月前
|
存储 算法 Java
手撕Java集合——链表
手撕Java集合——链表
手撕红黑树
手撕红黑树
72 0
|
6月前
|
存储 Java C++
手撕双链表
手撕双链表
49 0
|
6月前
|
存储 Java C++
手撕单链表
手撕单链表
62 0
|
6月前
|
安全 Java C语言
手撕各种排序(上)
手撕各种排序
49 0
|
6月前
|
搜索推荐
手撕各种排序(中)
手撕各种排序(中)
64 0
|
6月前
|
算法 搜索推荐 索引
手撕各种排序(下)
手撕各种排序(下)
58 0