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

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

一. 红黑树的概念😎


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


红黑树和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 树的效率是有代价的,是充分牺牲结构进行不断旋转得到的,而红黑树大大降低了旋转次数会更安全因此,换来了更优的性能


相关文章
|
4月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
110 1
【数据结构】红黑树——领略天才的想法
【数据结构】红黑树——领略天才的想法
|
2月前
|
Java C++
【数据结构】探索红黑树的奥秘:自平衡原理图解及与二叉查找树的比较
本文深入解析红黑树的自平衡原理,介绍其五大原则,并通过图解和代码示例展示其内部机制。同时,对比红黑树与二叉查找树的性能差异,帮助读者更好地理解这两种数据结构的特点和应用场景。
34 0
|
7月前
【数据结构】红黑树的原理及其实现
【数据结构】红黑树的原理及其实现
|
4月前
|
存储 Java 程序员
"揭秘HashMap底层实现:从数组到链表,再到红黑树,掌握高效数据结构的秘密武器!"
【8月更文挑战第21天】HashMap是Java中重要的数据结构,采用数组+链表/红黑树实现,确保高效查询与更新。构造方法初始化数组,默认容量16,负载因子0.75触发扩容。`put`操作通过计算`hashCode`定位元素,利用链表或红黑树处理冲突。`get`和`remove`操作类似地定位并返回或移除元素。JDK 1.8优化了链表转红黑树机制,提升性能。理解这些原理能帮助我们更高效地应用HashMap。
49 0
|
6月前
|
算法 架构师 NoSQL
【数据结构之红黑树】深入原理与实现
意节点的左子树和右子树的层高差不大于1,为了维护树的平衡,我们介绍了树的左右旋转。但是,AVL树维护平衡的代价是比较大的。所以,我们又介绍了红黑树这种数据结构,这是因为红黑树插入的效率相对AVL树是比较高的,在统计意义上来讲红黑树在插入和查找综合上效率是比较高的,这也是为什么红黑树为什么广泛应用在计算机各个方面。
60 2
|
5月前
|
存储 算法 C++
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
67 0
|
5月前
|
存储 Linux 数据库
【数据结构】二叉搜索树——高阶数据结构的敲门砖
【数据结构】二叉搜索树——高阶数据结构的敲门砖
|
6月前
|
C++
数据结构===红黑树
数据结构===红黑树
|
7月前
|
存储 算法 Java
数据结构/C++:红黑树
数据结构/C++:红黑树
43 3