【数据结构】红黑树的原理及其实现

简介: 【数据结构】红黑树的原理及其实现

红黑树的特性

红黑树最常用的平衡二叉搜索树。跟AVL树不同的是,红黑树是依靠节点的颜色来维护平衡的。虽然任意节点不具备严格平衡,但是数据的查找、插入、删除等操作效率依旧出色。下面是红黑树的一些特性:

  1. 每个节点的颜色要么是红色要么是黑色
  2. 根节点的颜色是黑色
  3. 如果一个节点的颜色是红色的,那么它的两个孩子节点的颜色一定是黑色的
  4. 任意节点到其所能到达的叶子节点之间的黑色节点的数量相同
  5. 叶节点是黑色的空节点

根据以上红黑树的特性,我们可以总结出以下结论:

结论:红黑树中最长路径的节点数量不超过最短路径节点数量的2倍

证明:假设每条路径黑色节点的数量为n(假设不包括空叶子节点),则红色节点的数量最多是n。任意路径节点数量最少为n(只有黑节点),最多为2*n

这样一来,任意一条路径的长度之差都保证在了一个有限的范围内,这也是红黑树具有平衡性的原因。

红黑树的时间复杂度

由于红黑树底层还是一颗二叉搜索树,根据二叉搜索树的特性:查找的效率取决于树的高度

推导:

现假设红黑树的某一路径黑色节点数量为bh。由于任意路径黑色节点数目相同,我们可以把路径上所有的红色节点删除,将删除节点的父节点和其子节点相连,于是我们得到了一颗纯黑色节点的树(四叉树),如下图:

这颗黑树的高度显然就是bh,且我们可以得到之前红黑树的节点数最少为2^bh-1(最少就是全是黑色)。假设这棵红黑树的节点总数为N,则N>=2^bh-1,两边取对数得bh<=log(N+1)(以2为底)。

设h为红黑树得最大高度,则有h=2*hb=2log(N+1)(最长路径节点数是2*bh)

结论

红黑树的高度最大为2log(N+1),N表示树的节点总数。这个证明基于红黑树的性质。

得到了节点数与树的高度的关系,我们也就能得到红黑树查找数据的效率为O(logN)。此外,红黑树的插入效率和删除效率取决于查找效率,也是O(logN)。

红黑树与AVL树比较

相对来说,红黑树的内部实现细节没有AVL树这么复杂,虽然AVL树保证具备严格平衡性,但是其插入元素时调整平衡的操作相对也就繁琐。插入和删除的时候,AVL树可能会进行更多的旋转操作来维持平衡,导致实际的执行时间可能高于红黑树

总的来说,红黑树适用于读写均衡的场景,插入和删除操作较为高效。而AVL树适用于查询频繁的场景,查询效率更高,但是插入和删除的维护成本较高。即使是这样,两者的差别依旧不大,查找、删除、插入的时间复杂度都是OlogN。

红黑树的插入

上面我们已经了解了红黑树的特性,接下来谈谈红黑树插入数据时是如何维护上述特性的。

红黑树的节点定义

与AVL实现类似,红黑树的节点依旧有三个指针分别指向父节点、左孩子节点、右孩子节点。且有一个变量表示当前节点的颜色。初始默认是红色。为什么默认设置为红色呢?这是因为插入节点时,新节点一定与空叶子节点相邻。而根据红黑树的性质,空叶子节点的颜色是黑色,这就使得,如果新插入节点是黑色,那么每插入一次这条路径就一定会多出来一个黑色节点,即一定要调整。虽然新节点是红色也可能会需要调整,但影响会少很多。

template<class K,class V>
struct RBTreeNode {
  RBTreeNode<T, V>* _parent;
  RBTreeNode<T, V>* _left;
  RBTreeNode<T, V>* _right;
  pair<K, V> _kv;//键值对

  Colour _col;//颜色

  RBTreeNode(const pair<K, V>& kv)
    :_kv(kv)
    ,_parent(nullptr)
    ,_left(nullptr)
    ,_right(nullptr)
    ,_col(RED)
  {

  }
};

调整策略

如果新插入一个节点,破坏了红黑树的性质,那么我们需要进行调整。具体需要调整的情况如下:

  1. 如果插入节点是根节点:那么只需要将根节点变黑
  2. 插入节点的叔叔节点是红色:父亲节点和叔叔节点变成黑色,爷爷变成红色,且爷爷节点变成新插入节点
  3. 插入节点的叔叔节点是黑色(为空也是黑色):需要先旋转,然后变色

上述2,3情况一定是出现两个相邻节点是红色。

思考情况2:

为什么我们要把父亲节点和叔叔节点变成黑色,爷爷变成红色,且爷爷节点变成新插入节点?为什么这样做一定能维护红黑树的性质呢?

  • 首先,把父亲节点变黑是因为相邻两个节点是红色,那为什么要把叔叔节点也变色呢?

这是因为新节点插入的这条路径可能会多一个黑色节点。为了让所有路径黑色节点数都能加一,每次调节父节点颜色的同时,也要调节叔叔节点颜色。这样一来,即使最后新节点插入的这条路径黑色节点数加一,其它所有路径的黑色节点也能同步加一。

  • 其实将爷爷节点变色也是为了维护所有路径黑色节点数一致这一特性。

设想我们不将爷爷节点变成红色,此时整个红黑树确实没有出现“相邻的红色节点”,但是经过爷爷节点的路径的黑色节点数目就会比其它路径黑色节点数目多(parent和uncle都是变黑了)。除非此时爷爷节点是根节点(所有路径都经过根节点)。所以我们选择继续向上调整,把爷爷节点变成红色,并更新cur指针指向grandparent节点,看是否还会出现“红红”的俩节点。把爷爷节点变红是基于贪心思路:先不让路径的黑色节点数加一试试能不能平衡。

什么时候停止调整呢?父节点的颜色是黑色为止。此时红黑树平衡

思考情况3:

如果叔叔节点本来就是黑色的呢?(空节点也是黑色)此时无论如何调节叔叔节点颜色,都有可能使得其它路径(比如叔叔这条路径)黑色节点数少于当前新节点插入路径的黑色节点数。此时考虑旋转。旋转的方式和AVL树是一致的。

旋转一共有四种方式:左旋、右旋、左右双旋、右左双旋。

细分情况3,一共有以下四种情况:

1.cur是parent的左孩子,parent是grandparent的左孩子。

将grandparent节点右旋转,旋转之后parent顶替了原来的grandparent,并变成黑色。grandparent成为parent的右孩子,变成红色

旋转是不会改变二叉搜索树的特性的。那么思考这样一个问题,旋转之后还是平衡的红黑树吗?答案是–是的。因为我们观察旋转之后经过parent的所有路径的黑色节点数压根就没有变。

2.cur是parent的右孩子,parent是grandparent的右孩子

这种情况和情况1一样,只不过方向相反,是左旋。不做过多徐述。

3.curparent孩子,parentgrandparent孩子

此时这种情况对grandparent节点进行左单旋还是右单旋都不能保证红黑树平衡,于是考虑双旋,即旋转两次。

先对parent节点左旋,发现左旋之后就变成了情况1

此时我们就可以像情况1一样,对grandparent进行右旋,对cur节点和grandparent节点进行变色

这样红黑树就平衡了,整个过程并没有增加路径的黑色节点的数量,因此也就不需要继续向上调整。

  1. curparent孩子,parentgrandparent孩子
    思路跟情况3一致,只不过旋转方向相反。向对parent节点进行右旋,再对grandparent节点进行左旋,再对cur和grandparent节点进行变色。

代码实现

myBTRee.h

封装了红黑树的类模板,包括各种功能的声明于定义

#pragma once

#include<vector>
#include<iostream>
using namespace std;

enum Colour{
  RED,
  BLACK
};

template<class K,class V>
struct RBTreeNode {
  RBTreeNode<K, V>* _parent;
  RBTreeNode<K, V>* _left;
  RBTreeNode<K, V>* _right;
  pair<K, V> _kv;//键值对

  Colour _col;//颜色

  RBTreeNode(const pair<K, V>& kv)
    :_kv(kv)
    ,_parent(nullptr)
    ,_left(nullptr)
    ,_right(nullptr)
    ,_col(RED)
  {

  }
};

template<class K,class V>
class RBTree {
  typedef RBTreeNode<K, V> Node;
  typedef Node* pNode;
public:
  bool Insert(const pair<K, V>& kv) {
    if (_root == nullptr) {
      _root = new Node(kv);
      _root->_col = BLACK;
      return true;
    }
    //找到一个合适的插入位置
    pNode cur = _root;
    pNode 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为nullptr
    cur = new Node(kv);
    if (cur->_kv.first < parent->_kv.first) {//插入节点
      parent->_left = cur;
      cur->_parent = parent;
    }
    else {
      parent->_right = cur;
      cur->_parent = parent;
    }

    //插入之后需要调节颜色平衡

    while (parent && parent->_col == RED) {
      //找叔叔节点
      pNode grandparent = parent->_parent;//祖父节点一定不为空,因为父节点为红色
      if (parent == grandparent->_left) {//如果叔叔在右边
        pNode uncle = grandparent->_right;
        //叔叔存在且为红,直接都变成黑色
        if (uncle && uncle->_col == RED) {
          parent->_col = BLACK;
          uncle->_col = BLACK;
          grandparent->_col = RED;
          //继续往上处理
          cur = grandparent;//cur往上跳两个节点
          parent = cur->_parent;
        }//叔叔节点不存在或者为黑色
        else {
          if (cur == parent->_left) {
            RotateR(grandparent);
            parent->_col = BLACK;
            grandparent->_col = RED;
          }
          else if (cur == parent->_right) {
            RotateL(parent);
            RotateR(grandparent);
            cur->_col = BLACK;
            grandparent->_col = RED;
          }
          break;
        }
      }
        //如果叔叔在左边
      if (parent == grandparent->_right) {
        pNode uncle = grandparent->_left;
        //叔叔存在且为红,直接都变成黑色
        if (uncle && uncle->_col == RED) {
          parent->_col = BLACK;
          uncle->_col = BLACK;

          //继续往上处理
          grandparent->_col = RED;
          cur = grandparent;//cur往上跳两个节点
          parent = cur->_parent;
        }//叔叔节点不存在或者为黑色
        else {
          if (cur == parent->_left) {
            RotateR(parent);
            RotateL(grandparent);
            cur->_col = BLACK;
            grandparent->_col= RED;
          }
          else if (cur == parent->_right) {
            RotateL(grandparent);
            parent->_col = BLACK;
            grandparent->_col = RED;
          }
          break;
        }
      }
    }
    _root->_col = BLACK;
    return true;
  }

  void RotateR(Node* parent)//右旋
  {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    if (subLR)
      subLR->_parent = parent;

    subL->_right = parent;

    Node* ppNode = parent->_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;
    }
  }

  void RotateL(Node* parent)//左旋
  {
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    parent->_right = subRL;
    if (subRL)
      subRL->_parent = parent;

    subR->_left = parent;
    Node* ppNode = parent->_parent;

    parent->_parent = subR;

    if (parent == _root)
    {
      _root = subR;
      _root->_parent= nullptr;
    }
    else
    {
      if (ppNode->_right == parent)
      {
        ppNode->_right = subR;
      }
      else
      {
        ppNode->_left = subR;
      }
      subR->_parent = ppNode;
    }
  }
  bool IsBalance() {
    if (_root->_col == RED)return false;

    int targetnum = 0;
    pNode cur = _root;
    while (cur) {
      if (cur->_col == BLACK)targetnum++;
      cur = cur->_left;
    }
    return _Check(_root, targetnum, 0);
  }
  void InOrder() {
    _InOrder(_root);
  }
private:

  bool _Check(pNode root,int targetnum,int blacknum) {
    if (root == nullptr) {
      if (blacknum != targetnum) {//路径的黑色节点数不相等
        return false;
      }
      else return true;
    }
    if (root->_left && root->_col == RED && root->_left->_col == RED)return false;
    if (root->_right && root->_col == RED && root->_right->_col == RED)return false;
    
    if (root->_col == BLACK)blacknum++;

    return _Check(root->_left,targetnum,blacknum) && _Check(root->_right,targetnum,blacknum);
  }

  void _InOrder(pNode root) {
    if (root == nullptr) {
      return;
    }
    _InOrder(root->_left);
    cout << root->_kv.first << " " << root->_kv.second << endl;
    _InOrder(root->_right);
  }

  pNode _root=nullptr;

};

test.cpp

用于测试代码的正确性。给出一组随机数,插入到红黑树中之后进行平衡检查。平衡检查内容为,检查是否出现连个相邻的红色节点,且所有路径的黑节点·数目是否相等。

代码:

void TestRBTree2()
{
  const int N = 100000;
  vector<int> v;
  v.reserve(N);
  srand((unsigned int)time(0));

  for (size_t i = 0; i < N; i++)
  {
    v.push_back(rand() + i);
    //cout << v.back() << endl;
  }

  size_t begin2 = clock();
  RBTree<int, int> t;
  for (auto e : v)
  {
    t.Insert(make_pair(e, e));
  }
  size_t end2 = clock();
  cout << t.IsBalance() << endl;
}

相关文章
|
4月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
112 1
|
6月前
|
存储 NoSQL Redis
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
85 1
【数据结构】红黑树——领略天才的想法
【数据结构】红黑树——领略天才的想法
|
6月前
|
存储 消息中间件 缓存
Redis系列学习文章分享---第十七篇(Redis原理篇--数据结构,网络模型)
Redis系列学习文章分享---第十七篇(Redis原理篇--数据结构,网络模型)
101 0
|
3月前
|
设计模式 安全 Java
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
假如有T1、T2两个线程同时对某链表扩容,他们都标记头结点和第二个结点,此时T2阻塞,T1执行完扩容后链表结点顺序反过来,此时T2恢复运行再进行翻转就会产生环形链表,即B.next=A;采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。JDK8中,HashMap采用尾插法,扩容时链表节点位置不会翻转,解决了扩容死循环问题,但是性能差了一点,因为要遍历链表再查到尾部。例如15(即2^4-1)的二进制为1111,31的二进制为11111,63的二进制为111111,127的二进制为1111111。
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
|
2月前
|
消息中间件 存储 Java
数据结构之 - 深入探析队列数据结构: 助你理解其原理与应用
数据结构之 - 深入探析队列数据结构: 助你理解其原理与应用
40 4
|
2月前
|
搜索推荐 索引
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(二)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
2月前
|
搜索推荐 C++
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(一)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
2月前
|
Java C++
【数据结构】探索红黑树的奥秘:自平衡原理图解及与二叉查找树的比较
本文深入解析红黑树的自平衡原理,介绍其五大原则,并通过图解和代码示例展示其内部机制。同时,对比红黑树与二叉查找树的性能差异,帮助读者更好地理解这两种数据结构的特点和应用场景。
38 0
|
2月前
|
人工智能 搜索推荐 算法
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(三)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理