【高阶数据结构】手撕红黑树(超详细版本)

简介: 【高阶数据结构】手撕红黑树(超详细版本)

一. 红黑树的概念😎


红黑树也是一种二叉搜索树,但是每个节点都存储了一个颜色,该颜色可以为黑可以为红,因此也叫红黑树


红黑树和AVL树的区别就是:红黑树是近似平衡,但不是完全平衡,没有和AVL树一样 的通过平衡因子来控制高度差,而是通过每条路径上对红黑节点的控制,来达到确保最长路径长度不超过最短路径长度的 2 倍。


0a2653c851af460fa595bd959398a8f1.png


二. 五大特性


根节点一定为黑色

每个节点只能是 红或黑

节点为红色,则该节点的两个子节点都为黑色(也就是树中没有连续的红色节点)

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

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

如果是空树,刚好是空节点为黑色,也符合第一条规则


那么问题来了,仅仅依靠这五大特性是如何确保最长路径长度 <= 最短路径长度的 2 倍?


根据红黑树的性质3可以得出,红黑树当中不会出现连续的红色结点,而根据性质4又可以得出,从某一结点到其后代叶子结点的所有路径上包含的黑色结点的数目是相同的


所以我们不妨假设极端场景,最短的路径无非就是全黑的情况了,假设此时有 n 个节点,长度就为 n


2d65d23f6d4748949b924e4057485923.png


而最长路径就是:一红一黑排列的,如果有 n 个黑色节点,那么红色节点数目与黑色相同,则长度为 2n,所以不超过两倍!


三. 节点的定义


cv工程师登场!此处我们还是定义成三叉链,只不过把平衡因子替换成了节点颜色,因为节点的颜色就两种,直接枚举就好了


//枚举颜色
enum  Colour
{
  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;//存储键值对
  Colour _col;//节点颜色
  RBTreeNode(const pair<K, V>& kv)
  :_left(nullptr)
  , _right(nullptr)
  , _parent(nullptr)
  , _kv(kv)
    , _col(RED)
  {}
};


此处有个问题:为什么构造结点时,默认将结点的颜色设置为红色?


此处我们知道插入黑色的节点是一定违反了性质4;某条路径的黑色节点一定会增加一个,那为了维护结构,我们岂不是要在其他的路径是不是也要增加一个呢?

但此时如果新增的是一个红色的节点呢;如果根节点为红色,那么又会破坏性质 3 出现了连续的红色节点,但是如果根节点为黑色,就不需要进行调整。 也就是说新增red节点不一定会破坏结构,但新增Black节点就一定会破坏。


0a2653c851af460fa595bd959398a8f1.png


四. 红黑树插入


此处插入的前半部分和AVL树的插入一样:


按二叉搜索树的插入方法,找到待插入位置

将待插入结点插入到树中

若插入结点的父结点是红色的,则需要对红黑树进行调整

红黑树的关键就在这第三步中!大有来头


⚡模型


我们先给出一个基本模型:(可以是子树也可以是一棵完整的树)


2d65d23f6d4748949b924e4057485923.png


cur :当前节点

p:parent,父节点

g:grandfather,祖父节点

u:uncle,叔叔节点

a,b,c,d,e:子树


顺便科普一下树的路径是从根节点一路走到空节点(NIL 节点)才算一条路径,而不是走到叶子就停下来了



所以上面这棵树的有效路径有9条,而不是4条


🥑情况一:u 存在且为红

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


2d65d23f6d4748949b924e4057485923.png


首先我们知道红黑树的关键是看叔叔uncle;节点的颜色是固定为黑色的;因为不能有两个相同的红色节点,所以我们开始调整!首先将parent变成黑色;又为了不违反性质4,所以uncle的节点也要变成黑色;同时也要将grandparent节点变红,不要忘了这可能只是一颗子树;为了维持每条路径上黑色节点的数量;祖父必须变红,不然会多出一个黑色节点。



最后不要忘了将祖父当成 cur 节点继续向上调整,直到g是根,最后才将变成黑色!


具体代码:


while (parent && parent->_col == RED)
  {
    Node* grandfather = parent->_parent;
    assert(grandfather);
    assert(grandfather->_col == BLACK);
    //关键看叔叔  ~ 判断叔叔的位置
    if (parent == grandfather->_left)
    {
    Node* uncle = grandfather->_right;
    //情况1:uncle存在且为红  + 继续往上处理
    if (uncle && uncle->_col = RED)
    {
      //变色:p和u变黑,g变红
      parent->_col = uncle ->_col = Black;
      grandfather->_col = RED;
      //继续往上调整
      cur = grandfather;
      parent = cur->_parent;
    }
    else  //情况2 
    {}
    }
    else  //parent == grandfather->_right
    {
    Node* uncle = grandfather->_left;
    //情况1:uncle存在且为红 + 继续往上处理
    if (uncle&& uncle->_col = RED)
    {
      //变色:p和u变黑,g变红
      parent->_col = uncle->_col = Black;
      grandfather->_col = RED;
      //继续往上调整
      cur = grandfather;
      parent = cur->_parent;
    }
    else  //情况2 
    {}
    } 
  }
  _root->_col = BLACK;//不管什么,最后根要变黑
  return true;
  }


🥑情况二:

💥具体情况1️⃣:u不存在

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


0a2653c851af460fa595bd959398a8f1.png


如果叔叔不存在,那么a/b/c/d/e都是空树,cur是新增节点!因为叔叔不存在的话,parent下也不可能挂上黑节点!


此处也违背了性质4,所谓单纯的变色也无法处理了,那就旋转试试看吧,

祖孙三代在一条直线上偏左,一波右单旋安排,接着根节点变成 parent 后调整颜色,父亲变黑,祖父变红,一波操作下来黑节点数目没边,根节点也是黑色不用再向上调整了


2d65d23f6d4748949b924e4057485923.png


💥具体情况2️⃣:u存在且为黑

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


4cebaac233b3433da32a72337a77fc60.png


b和c可以是空树或者是一个红节点,a可以是根也可以是黑节点的子树,下面四种的的任意一种


6de278e6d6694ce5bb08e7e842b7e74b.png


以下的情况一定是由情况一往上调整过程中才会出现的!即这种情况下的cur结点一定不是新插入的结点,而是上一次情况一调整过程中的祖父结点,如下图:


8ec4f2997fb246878c34ecd6d122b7c6.png


此上的情况必定是左边的的多一个黑色节点,如上图所示, 假设祖父上面有 x 个黑节点,叔叔下有y个节点,那么左子树(含祖父)现在是 x +1 个,右子树(含祖父)是 x + 2 + y 个,很明显 x + 2 + y > x + 1,因此在插入结点前就已经不满足要求了,所以说叔叔结点存在且为黑这种情况,一定是由情况一往上调整过程中才会出现的!


此处单纯的变色已经无法处理,况且还违背了性质4;这时我们需要进行旋转+变色处理


0a2653c851af460fa595bd959398a8f1.png


如果是祖父、父亲、cur 都在同一条直线上,那么只需要单旋即可


💥双旋是怎么样产生的?

若祖孙三代的关系是折线(cur、parent、grandfather这三个结点为一条折现),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理


抽象图如下:


2d65d23f6d4748949b924e4057485923.png


以上情况的完整代码:


//如果插入节点的父节点是红色的,则需要对红黑树进行操作
  while (parent && parent->_col == RED)
  {
    Node* grandfather = parent->_parent;
    assert(grandfather);
    assert(grandfather->_col == BLACK);
    //关键看叔叔  ~ 判断叔叔的位置
    if (parent == grandfather->_left)
    {
    Node* uncle = grandfather->_right;
    //情况1:uncle存在且为红  + 继续往上处理
    if (uncle && uncle->_col == RED)
    {
      //变色:p和u变黑,g变红
      parent->_col = uncle ->_col = BLACK;
      grandfather->_col = RED;
      //继续往上调整
      cur = grandfather;
      parent = cur->_parent;
    }
    else  //情况2 + 情况3:uncle不存在 + uncle存在且为黑
    {
      //情况二:单旋 + 变色
      //    g
      //  p   u
      //c            
      if (cur = parent->_left)
      {
      RotateR(grandfather);//右旋
      //颜色调整
      parent->_col = BLACK;
      grandfather->_col = RED;
      }
      else//cur == parent->_right
      {
      //情况三:左右双旋 + 变色
      //    g
      //  p   u
      //    c 
      RotateL(parent);
      RotateR(grandfather);
      //调整颜色
      cur->_col = BLACK;
      grandfather->_col = RED;
      }
      break;
    }
    }
    else  //parent == grandfather->_right
    {
    Node* uncle = grandfather->_left;
    //情况1:uncle存在且为红 + 继续往上处理
    if (uncle&& uncle->_col == RED)
    {
      //变色:p和u变黑,g变红
      parent->_col = uncle->_col = BLACK;
      grandfather->_col = RED;
      //继续往上调整
      cur = grandfather;
      parent = cur->_parent;
    }
    else  //情况2 + 情况3:uncle不存在 + uncle存在且为黑
    {
      //情况二:单旋 + 变色
      //    g
      //  u   p
      //        c            
      if (cur = parent->_right)
      {
      RotateL(grandfather);//左单 旋
      //颜色调整
      parent->_col = BLACK;
      grandfather->_col = RED;
      }
      else//cur == parent->_left
      {
      //情况三:右左双旋 + 变色
      //    g
      //  u   p
      //    c 
      RotateR(parent);
      RotateL(grandfather);
      //调整颜色
      cur->_col = BLACK;
      grandfather->_col = RED;
      }
      break;
    }
    } 
  }


大总结


无论是情况一还是情况二,cur为红,p为红,g为黑这三个条件是固定的


情况一:叔叔存在且为红:


1️⃣单纯的变色:p和u变黑,g变红

2️⃣把g继续当成cur,g不是根继续往上处理(继续往上处理有可能变成情况2);g是根就变成黑色

情况二:叔叔不存在 or 叔叔存在且为黑


1️⃣旋转:具体情况来判断是什么旋转(祖孙三代是折线关系就是双旋,直线就是单旋)

2️⃣变色:p变黑, g变红


五. 验证红黑树


还是老套路中序遍历来验证:


void Inorder()
{
  _Inorder(_root);
}
void _Inorder(Node* root)
{
  if (root == nullptr)//空树也是红黑树
  return;
  _Inorder(root->_left);
  cout << root->_kv.first << " ";
  _Inorder(root->_right);
}


那怎么样验证它是红黑树呢?从五大特性下手!


bool IsBalance()
  {
  if (_root == nullptr)
  {
    return true;
  }
  if (_root->_col == RED)
  {
    cout << "根节点不是黑色" << endl;
    return false;
  }
  // 黑色节点数量基准值
  int benchmark = 0;
  Node* cur = _root;
  while (cur)
  {
  if (cur->_col == BLACK)
  ++benchmark;
  cur = cur->_left;//以最左的路径进行
  }
  return PrevCheck(_root, 0, benchmark);
  }
  bool PrevCheck(Node* root, int blackNum, int& benchmark)
  {
  if (root == nullptr)
  {
    //cout << blackNum << endl;
    //return;
    if (benchmark == 0)
    {
    benchmark = blackNum;
    return true;
    }
    if (blackNum != benchmark)
    {
    cout << "某条黑色节点的数量不相等" << endl;
    return false;
    }
    else
    {
    return true;
    }
  }
  if (root->_col == BLACK)
  {
    ++blackNum;
  }
        //检测当前节点以及父亲
  if (root->_col == RED && root->_parent->_col == RED)
  {
    cout << "存在连续的红色节点" << endl;
    return false;
  }
  return PrevCheck(root->_left, blackNum, benchmark)
    && PrevCheck(root->_right, blackNum, benchmark);
  }


六. 红黑树的性能


红黑树的删除本节不做讲解,有兴趣的可参考:《算法导论》或者《STL源码剖析》


参考地址


七. 红黑树的性能


AVL 树和红黑树都是平衡二叉树的两个老大哥,他们增删查改的时间复杂度都O(logN),到底谁更技高一筹?


其实在大数据的场景下,比如百万级量化数据,AVL 需要构建大约 20 多层,同时红黑树需要构建大约 40 多层,毕竟红黑树是近似平衡的二叉搜索树。


但是我们知道 20 和 40 在 CPU 运算速度面前并没有什么差别,虽然 AVL 树在效率上会略胜红黑树一点点,但是生活中红黑树的运用却比 AVL 树更为广泛,因为 AVL 树的效率是有代价的,是充分牺牲结构进行不断旋转得到的,而红黑树大大降低了旋转次数会更安全因此,换来了更优的性能


相关文章
|
算法 C++
【数据结构与算法】—— 手撕红黑树
【数据结构与算法】—— 手撕红黑树
|
1月前
|
存储 Java 数据库
手撕红黑树 - 聊聊这个基本却又重要的数据结构
手撕红黑树 - 聊聊这个基本却又重要的数据结构
21 0
|
3月前
|
存储 关系型数据库 数据库
【数据结构】—红黑树(C++实现)
【数据结构】—红黑树(C++实现)
|
6月前
|
存储 Java 数据库
【红黑树数据结构及其应用】
【红黑树数据结构及其应用】
|
6月前
|
存储
数据结构之二叉查找树(Binary Search Tree)和红黑树(Red Black Tree)
二叉查找树又可以称之为 : 二叉搜索树 , 二叉排序树 , 它或者是一棵空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势 , 下图中这棵树,就是一棵典型的二叉查找树
110 1
|
6月前
|
关系型数据库
|
1月前
从0开始回顾数据结构---红黑树
红黑树 1、什么是红黑树 红黑树是一种不严格的平衡二叉树,插入、删除、查找的最坏时间复杂度都为 O(logn),避免了二叉树最坏情况下的O(n)时间复杂度。 红黑树特性如下: 1. 根节点是黑色 2. 每个节点要么是黑色要么是红色 3. 每个红色节点的两个子节点一定都是黑色 4. 每个叶子节点(NIL)都是黑色 5. 任意一个节点的路径到叶子节点所包含的黑色节点的数量是相同的---这个也称之为黑色完美平衡 6. 新插入的节点必须是红色->为什么?如果新插入的节点是黑色,那不管是在插入到那里,一定会破坏黑色完美平衡的,因为任意一个节点的路径到叶子节点的黑色节点的数量肯定不一样了。 2、为什么需
|
6月前
|
C++
【数据结构】红黑树封装map和set(上)
【数据结构】红黑树封装map和set(上)
|
3月前
数据结构——红黑树
数据结构——红黑树
28 0
|
8月前
|
存储 算法 Java
深入解析 Java 数据结构:红黑树的特点与应用
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它在 Java 编程中扮演着重要的角色,用于高效地存储和管理数据。红黑树的特点使其在查找、插入和删除操作中保持相对平衡,从而提供了稳定且高效的性能。本文将深入探讨红黑树的特点、用法、实现方式以及在实际应用中的优势。