赞
踩
AVL树解决了二叉搜索树退化为单支树而引发的效率问题,是一种绝对平衡的二叉搜索树,其性质(每个节点的左右子树高度差绝对值都不超过1)使其在对数据进行搜索时始终能保持高效(详见【数据结构】平衡树之AVL树)。但是,当涉及一些结构上的修改场景(例如增删),因为要频繁通过旋转来维护其绝对平衡的结构特性,代价较高,使得其在这样的场景中性能十分低下。
为了继续优化效率问题,后来又不断有大佬提出新的解决方案。1972年,Rudolf Bayer发明了最初红黑树(当时被称为平衡二叉B树)。而后在1978年, Leo J. Guibas 和 Robert Sedgewick 将其修改成了如今的红黑树。
红黑树(RBTree)是一种特殊的二叉平衡搜索树,通过一种特殊的“手法”来控制着二叉搜索树的平衡,使其效率与AVL树相当,且在应用和维护等方面整体优于AVL树。
对于平衡的控制,红黑树放弃了AVL树中调整平衡因子的方式(任一节点的左右子树高度差的绝对值不大于1),而是在树的每个节点上增加了一个用于表示节点的颜色(可以是Red或Black)的存储位,通过对任意一条从根到某个叶子节点的路径上,各个节点着色方式的限制,确保最长路径不超过最短路径的2倍,以此来使一整棵树接近平衡。
红黑树的应用十分的广泛,例如Java的集合框架 (HashMap、TreeMap、TreeSet)、C++ 的 STL(map/set、mutil_map/mutil_set)、linux内核等等,都将红黑树作为底层结构来使用过。
本篇博客将通过,对红黑树性质的梳理和主要功能的模拟实现,来帮助读者更加全面地了解红黑树。
目录
1.控制节点的红和黑,怎么就做到了“最长路径不超过最短路径的2倍”?
红黑树是因其维护平衡的手段而得名的,通过对树节点颜色的控制,来实现“最长路径不超过最短路径的2倍”的近似平衡。它主要具备以下性质(或者说是它的平衡规则),且这些性质与维护平衡息息相关:
- 任意一个树节点的颜色非红即黑;
- 根节点的颜色必为黑;
- 任意一个颜色为红的树节点,其孩子节点均为黑,双亲节点为黑(这意味着任意路径上没有连续的红色节点);
- 对于任意一条从(不为空的叶节点下的)空节点通往根节点的路径,每条路径上黑色节点数量均相同;
- 每个空节点默认为黑色。
(ps:NIL节点指的是空节点 )
- //用枚举体来标识节点的颜色
- enum Colour
- {
- RED, //0
- BLACK, //1
- };
-
- //创建一个树节点(三叉链结构)
- 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; //放弃了AVL树的平衡因子,改用颜色标识来控制平衡
-
- //一个树节点的构造函数
- RBTreeNode(const pair<K, V>&kv)
- : _left(nullptr)
- , _right(nullptr)
- , _parent(nullptr)
- , _kv(kv)
- , _col(RED) //默认一个新节点为红色
- {}
- };
-
- //创建一棵红黑树
- template<class K, class V>
- class RBTree
- {
- typedef RBTreeNode<K, V>Node;
- private:
- //根节点
- Node*_root = nullptr;
- };

红黑树可以看作是引入了颜色标识的二叉搜索树,与AVL树类似(详见【数据结构】平衡树之AVL树),它插入过程也大致分为两步:
- 按照二叉搜索树的方式插入新节点:利用插入的值创建一个新的树节点。树为空,就直接将新节点赋给根节点的指针;树不为空,就按二叉搜索树的性质查找到合适的插入位置,在合适位置插入新节点。
- 控制树的平衡:调整颜色+旋转。
- bool Insert(const pair<K, V>& kv)
- {
- //1.按照二叉搜索树的方式插入新节点
-
- if (_root == nullptr)
- {
- _root = new Node(kv);
- _root->_col = BLACK;
- return true;
- }
-
- Node* parent = nullptr;
- Node* cur = _root;
- while (cur)
- {
- if (cur->_kv.first < kv.first)
- {
- parent = cur;
- cur = cur->_right;
- }
- else if (cur->_kv.first > kv.first)
- {
- parent = cur;
- cur = cur->_left;
- }
- else
- {
- return false;
- }
- }
-
- cur = new Node(kv);
- cur->_col = RED;
- if (parent->_kv.first < kv.first)
- {
- parent->_right = cur;
- }
- else
- {
- parent->_left = cur;
- }
-
- cur->_parent = parent;
-
-
- //2.控制树的平衡
-
- //...
-
-
- }

因为默认一个新插入的节点为红色(原因见下文“一些迷思”),所以插入后就存在两种情况:
- 新插入节点的双亲为黑色,则仍满足红黑树的平衡性质,无需控制平衡;
- 新插入节点的双亲为红色,则违背平衡性质三,需控制平衡。
而当新插入节点的双亲为红色,该如何控制平衡呢?
插入节点的双亲一定是红色的,双亲的双亲一定是黑色的,这两个节点的颜色是始终确定的。 插入的节点为红色,插入后出现了两个连续的红色节点,此时就需要调整节点的颜色。
调整颜色既是控制树平衡的手段,也是检验是否需要旋转的途径。调整颜色的对象不是新插入的节点,而是其双亲、双亲的兄弟和双亲的双亲。
详情见下图:
【Tips】新节点插入后,平衡的控制具体取决于其双亲的兄弟节点:
- 双亲的兄弟节点存在且为红色,调整颜色后,继续向上更新;
- 双亲的兄弟节点不存在,或存在且为黑色,需先旋转,然后再调整颜色。
(关于将特殊情景下的结论推广到一般性结论的说明,以及旋转分类讨论的图解,见下文“一些迷思”)
【Tips】旋转的分类讨论:
设:c为当前节点,p为其双亲节点,g为其祖父节点,u为其双亲的兄弟节点,并且cur为红色,p为红色,g为黑色,u不存在或u存在且为黑色。
1、p为g的左孩子,c为p的左孩子,则针对g做右单旋转;
2、p为g的左孩子,c为p的右孩子,则针对p做左单旋转,再针对g做右单旋转;
3、p为g的右孩子,c为p的右孩子,则针对g做左单旋转;
4、p为g的右孩子,c为p的左孩子,则针对p做右单旋转;
- bool Insert(const pair<K, V>& kv)
- {
- //1.按照二叉搜索树的方式插入新节点
-
- //...(见前文)
-
-
-
- //2.控制平衡
-
- while (parent && parent->_col == RED)//其双亲存在且双亲为红,才需要处理
- {
- Node* grandfather = parent->_parent;
- if (parent == grandfather->_left)//parent是grandfather的左孩子
- {
- Node* uncle = grandfather->_right;//则uncle是grandfather的右孩子
- //uncle存在且为红
- if (uncle && uncle->_col == RED)
- {
- //变色
- parent->_col = uncle->_col = BLACK;
- grandfather->_col = RED;
-
- //继续向上处理
- cur = grandfather;
- parent = cur->_parent;
- }
- //uncle不存在,或存在且为黑
- else
- {
- if (cur == parent->_left)
- {
- // g
- // p
- // c
- //旋转
- RotateR(grandfather);
- //变色
- parent->_col = BLACK;
- grandfather->_col = RED;
-
- }
- else
- {
- // g
- // p
- // c
- //旋转
- RotateL(parent);
- RotateR(grandfather);
- //变色
- cur->_col = BLACK;
- grandfather->_col = RED;
- }
-
- break;
- }
- }
- else //parent == grandfather->_right //parent是grandfather的右孩子
- {
- Node* uncle = grandfather->_left;//则uncle是grandfather的左孩子
- // uncle存在且为红
- if (uncle && uncle->_col == RED)
- {
- //变色
- parent->_col = uncle->_col = BLACK;
- grandfather->_col = RED;
-
- //继续向上处理
- cur = grandfather;
- parent = cur->_parent;
- }
- //uncle不存在,或存在且为黑
- else
- {
- if (cur == parent->_right)
- {
- // g
- // p
- // c
- //旋转
- RotateR(grandfather);
- //变色
- parent->_col = BLACK;
- grandfather->_col = RED;
- }
- else
- {
- // g
- // p
- // c
- //旋转
- RotateL(parent);
- RotateR(grandfather);
- //变色
- cur->_col = BLACK;
- grandfather->_col = RED;
- }
-
- break;
- }
- }
- }
-
- _root->_col = BLACK;//无论什么情况,根始终要置为黑
-
-
- return true;
- }

(旋转的细节详见【数据结构】平衡树之AVL树)
- //左单旋
- void RotateL(Node* parent)
- {
- Node* cur = parent->_right; //cur是parent的右孩子
- Node* curleft = cur->_left; //curleft是cur的左孩子
-
- //将parent及其左子树整体旋转下来,并将parent与cur、curleft与parent、cur与ppnode一一正确链接
-
- parent->_right = curleft;
- if (curleft)
- {
- curleft->_parent = parent; //curleft可能为空,若为空则无需将curleft->_parent与parent链接
- }
- cur->_left = parent;
-
- //需要旋转的树,可能是一个局部的子树
- //cur可能需要跟parent的双亲节点ppnode链接
- Node* ppnode = parent->_parent;
- parent->_parent = cur;
-
- if (parent == _root /*ppnode==nullptr*/) //旋转点在根,cur无需跟parent的双亲节点链接
- {
- _root = cur;
- cur->_parent = nullptr;
- }
- else //旋转点不在根,cur需要跟parent的双亲节点链接
- {
- if (ppnode->_left == parent) //parent是ppnode的左孩子,就让cur代替其成为左孩子
- {
- ppnode->_left = cur;
- }
- else //parent是ppnode的右孩子,就让cur代替其成为右孩子
- {
- ppnode->_right = cur;
- }
-
- cur->_parent = ppnode; //将cur的双亲节点置为ppnode
- }
- }
-
-
- //右单旋
- void RotateR(Node* parent)
- {
- Node* cur = parent->_left; //cur是parent的左孩子
- Node* curright = cur->_right; //curright是cur的右孩子
-
- //将parent及其右子树整体旋转下来,并将parent与cur、curleft与parent、cur与ppnode一一正确链接
-
- //链接curright与parent
- parent->_left = curright;
- if (curright)
- {
- curright->_parent = parent;
- }
-
- //链接cur与parent、cur与parent的双亲ppnode
- Node* ppnode = parent->_parent;
- cur->_right = parent;
- if (ppnode == nullptr)
- {
- _root = cur;
- cur->_parent = nullptr;
- }
- else
- {
- if (ppnode->_left == parent)
- {
- ppnode->_left = cur;
- }
- else
- {
- ppnode->_right = cur;
- }
-
- cur->_parent = ppnode;
- }
- }
-

- #pragma once
-
- #include<iostream>
- using namespace std;
-
-
- 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)
- {}
- };
-
- template<class K, class V>
- struct RBTree
- {
- typedef RBTreeNode<K, V> Node;
- public:
- bool Insert(const pair<K, V>& kv)
- {
- if (_root == nullptr)
- {
- _root = new Node(kv);
- _root->_col = BLACK;
- return true;
- }
-
- Node* parent = nullptr;
- Node* cur = _root;
- while (cur)
- {
- if (cur->_kv.first < kv.first)
- {
- parent = cur;
- cur = cur->_right;
- }
- else if (cur->_kv.first > kv.first)
- {
- parent = cur;
- cur = cur->_left;
- }
- else
- {
- return false;
- }
- }
-
- cur = new Node(kv);
- cur->_col = RED;
- if (parent->_kv.first < kv.first)
- {
- parent->_right = cur;
- }
- else
- {
- parent->_left = cur;
- }
-
- cur->_parent = parent;
-
- //红黑树插入的关键是uncle
- //1、uncle存在且为红,则变色+继续向上更新
- //2、uncle不存在,或uncle存在且为黑,则旋转+变色
-
- while (parent && parent->_col == RED)
- {
- Node* grandfather = parent->_parent;
- if (parent == grandfather->_left)
- {
- Node* uncle = grandfather->_right;
- //uncle存在且为红
- if (uncle && uncle->_col == RED)
- {
- //变色
- parent->_col = uncle->_col = BLACK;
- grandfather->_col = RED;
-
- //继续向上处理
- cur = grandfather;
- parent = cur->_parent;
- }
- //uncle不存在,或存在且为黑
- else
- {
- if (cur == parent->_left)
- {
- // g
- // p
- // c
- RotateR(grandfather);
- parent->_col = BLACK;
- grandfather->_col = RED;
-
- }
- else
- {
- // g
- // p
- // c
- RotateL(parent);
- RotateR(grandfather);
-
- cur->_col = BLACK;
- grandfather->_col = RED;
- }
-
- break;
- }
- }
- else // parent == grandfather->_right
- {
- Node* uncle = grandfather->_left;
- // uncle存在且为红
- if (uncle && uncle->_col == RED)
- {
- //变色
- parent->_col = uncle->_col = BLACK;
- grandfather->_col = RED;
-
- //继续向上处理
- cur = grandfather;
- parent = cur->_parent;
- }
- //uncle不存在,或存在且为黑
- else
- {
- if (cur == parent->_right)
- {
- // g
- // p
- // c
- //旋转+变色
- RotateR(grandfather);
-
- parent->_col = BLACK;
- grandfather->_col = RED;
- }
- else
- {
- // g
- // p
- // c
- //旋转+变色
- RotateL(parent);
- RotateR(grandfather);
-
- cur->_col = BLACK;
- grandfather->_col = RED;
- }
-
- break;
- }
- }
- }
-
- _root->_col = BLACK;
-
-
- return true;
- }
-
- //左单旋
- void RotateL(Node* parent)
- {
- ++_rotateCount;
-
- Node* cur = parent->_right;
- Node* curleft = cur->_left;
-
- parent->_right = curleft;
- if (curleft)
- {
- curleft->_parent = parent;
- }
-
- cur->_left = parent;
-
- Node* ppnode = parent->_parent;
-
- parent->_parent = cur;
-
-
- if (parent == _root)
- {
- _root = cur;
- cur->_parent = nullptr;
- }
- else
- {
- if (ppnode->_left == parent)
- {
- ppnode->_left = cur;
- }
- else
- {
- ppnode->_right = cur;
-
- }
-
- cur->_parent = ppnode;
- }
- }
- //右单旋
- void RotateR(Node* parent)
- {
- ++_rotateCount;
-
- Node* cur = parent->_left;
- Node* curright = cur->_right;
-
- parent->_left = curright;
- if (curright)
- curright->_parent = parent;
-
- Node* ppnode = parent->_parent;
- cur->_right = parent;
- parent->_parent = cur;
-
- if (ppnode == nullptr)
- {
- _root = cur;
- cur->_parent = nullptr;
- }
- else
- {
- if (ppnode->_left == parent)
- {
- ppnode->_left = cur;
- }
- else
- {
- ppnode->_right = cur;
- }
-
- cur->_parent = ppnode;
- }
- }
-
-
- //验树的平衡
- bool CheckColourNum(Node* root, int blackNum,int benchmark)//递归查询连续的红色节点和验证黑色节点数量
- {
- if (root == nullptr)
- {
- if (blackNum != benchmark)//若某一条路径上的黑色节点数量与基准值不符,则说明树不平衡(无论基准值本身是否正确)
- {
- return false;
- }
- return true;
- }
-
- //递归记录某一条路径上的黑色节点数量
- if (root->_col == BLACK)
- {
- ++blackNum;
- }
-
- //出现两个连续的红色节点,则说明树不平衡
- if (root->_col == RED && root->_parent && root->_parent->_col == RED)
- {
- cout << root->_kv.first << "RED show up" << endl;
- return false;
- }
-
- //递归至左子树和右子树里去验证平衡
- return CheckColourNum(root->_left,blackNum,benchmark)
- && CheckColourNum(root->_right, blackNum, benchmark);
- }
- bool IsBalance()//封装一次“bool IsBalance(Node* root)”
- {
- return IsBalance(_root);
- }
- bool IsBalance(Node* root)//验树的平衡
- {
- //空树是平衡的
- if (root == nullptr)
- return true;
-
- //根节点不是黑色的,则说明树不平衡
- if (root->_col != BLACK)
- {
- return false;
- }
-
- //取最左路径上的黑色节点数量作为基准值
- Node* cur = _root;
- int benchmark = 0;
- while (cur)
- {
- if (cur->_col == BLACK)
- {
- ++benchmark;
- }
- cur = cur->_left;
- }
- //查询连续的红色节点和验证黑色节点数量
- return CheckColourNum(root,0,benchmark);//参数:根节点,当前路径的黑节点数量,黑节点数量的基准值
- }
-
- //求树的高度
- int Height()
- {
- return Height(_root);
- }
- int Height(Node* root)
- {
- if (root == nullptr)
- return 0;
-
- int leftHeight = Height(root->_left);
- int rightHeight = Height(root->_right);
-
- return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
- }
-
- private:
- Node* _root = nullptr;
- };

如下图,是一棵满足“最长路径不超过最短路径的2倍”的合法红黑树,它的最长路径为4(例如13->17->25->22),最短路径为3(例如13->8->1)。
同样的,下图中是一种极端情况下的红黑树,但它仍是合法的。它的最长路径为4(例如13->17->25->22),最短路径为2(例如13->8)。
由此可见,控制节点的红和黑,保证最短路径全黑、最长路径是一黑一红相间的(或每条路径上黑色节点的总数占整棵树节点总数的1/2),就满足了“最长路径不超过最短路径的2倍”。
平衡树 | 特点 | 查找效率大致为 | 创建一棵树的数据量 | 创建后树的高度 | 所需时间约为 |
AVL树 | 高度差的绝对值不超过1 | O(logN) | 1000个值 | 10 | 0.000001s |
红黑树 | 最长路径不超过最短路径的2倍 | O(2*logN) | 1000个值 | 20 | 0.000002s |
对于红黑树与AVL树,两者的性能在同一量级上,效率差距并不大,虽然在维护平衡时均需旋转,但AVL树因控制平衡更加严格,需在插入和删除时频繁通过旋转维护其结构,会付出较大的代价,故红黑树整体优于AVL树。
当插入的节点为黑色,此时在所插入的路径上,黑色节点的数量比其他路径上多1,就违背了红黑树的性质四(对于任意一条从空节点通往根节点的路径,每条路径上黑色节点数量均相同)。
当插入的节点为红色,若此时所插入位置的双亲为红色,即出现了连续的红色节点,就违背了红黑树的性质三(任意路径上没有连续的红色节点)。
也就是说,插入的节点无论是什么颜色的,都会对红黑树的平衡有潜在的影响。
不过具体而言,若插入的节点为黑色,则是“一定会因违背性质四而破坏平衡”,此时是必须对红黑树进行调整的,且要对多条路径进行调整;而插入的节点为红色,则是“可能会因性质三而破坏平衡”,此时既可能要进行调整,也可能不进行调整,且调整时仅对一条路径进行调整。
基于这样的利弊,插入节点为红色时维护平衡的代价在理论上更小,故使插入的新节点/新创建的节点默认为红色。
设:cur为当前节点,p为其双亲节点,g为其祖父节点,u为其双亲的兄弟节点。
情景一:cur为红,p为红,g为黑,u存在且为红。cur和p均为红,违背了性质三。解决方法为,将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
当a/b/c/d/e为空树,cur是新插入的节点,情况较为简单,上述解决方法是适用的。
而当a/b/c/d/e不为空树,cur不是新插入的节点,尽管情况较为复杂,但上述解决方法仍适用。
更复杂的情况又例如,c/d/e是每条路径都有两个黑色节点的子树...
情景二:cur为红,p为红,g为黑,u不存在/u存在且为黑。
u不存在,做旋转的处理。
u存在且为黑,做旋转和变色的处理。
【Tips】旋转的分类讨论:
1、p为g的左孩子,cur为p的左孩子,则针对g做右单旋转;
2、p为g的左孩子,cur为p的右孩子,则针对p做左单旋转,再针对g做右单旋转;
3、p为g的右孩子,cur为p的右孩子,则针对g做左单旋转;
4、p为g的右孩子,cur为p的左孩子,则针对p做右单旋转;
升序构建红黑树
降序构建红黑树
随机插入构建红黑树
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。