红黑树
来源:互联网 发布:淘宝打折工具有什么用 编辑:程序博客网 时间:2024/06/06 14:19
前言:我们知道,对于高度为h的二叉搜索树(BST),其插入、删除、查找、最大、最小等动态集合操作的时间复杂度为O(h);n个节点随机构造的二叉搜索树,其期望高度为logn,最大高度为n(此时树退化为链),所以在最坏情况下,二叉搜索树动态集合操作的时间复杂度为O(n),也不比链表好。那么能否构造一种二叉搜索树使其最坏高度为(logn)级别呢?答案是红黑树。
红黑树的基本概念
红黑树是一种二叉搜索树,在每一个节点上增加一个域表示该节点的颜色,可以是RED或者BLACK;通过对由当前节点到叶子节点的简单路径上的节点颜色进行约束,红黑树确保没有一条路径会比其它路径长出两倍,因此是近似于平衡的。进而确保树的动态集合操作是O(logn)级别。
那么问题来了,它是如何约束简单路径上的节点颜色使得树的最坏高度仍然是O(logn)级别的呢?红黑树满足了以下五条性质:
1. 红黑树的每一个节点颜色要么是黑色的要么是红色的;2. 红黑树的根节点是黑色的;3. 红黑树的叶子节点是黑色的;4. 如果一个节点的颜色是红色的,那么其两个孩子节点的颜色的黑色的;5. 由当前节点到其后代中所有叶子节点的简单路径上,黑色节点的个数是相同的。
正是有了这样五条性质约束,红黑树的高度确保在O(logn)级别,也就保证了动态集合操作的复杂度;下面说几条关于红黑树的细节知识点:
- 从某个节点x出发(不含当前节点)到达一个叶子节点的所有简单路径上黑色节点的数目称之为该节点的黑高(Black Height,Bh)
- 一个含有n个节点的红黑树,其高度至多为2*log(n+1)。因为,一个含有n个节点的红黑树,至少含有n/2个节点的黑色节点(一红两黑,对消原理),一个以节点x为根的子树中至少含有2^Bh(x)-1个节点;故,n>=2^(h/2)-1,即可推出结论;
红黑树的C++描述:
enum COLOR{RED,BLACK};//定义颜色枚举类型template<class KeyType>class RbNode //红黑树节点类{public: RbNode() : plchild(NULL),prchild(NULL),pParent(NULL) {} ~RbNode() {}public: COLOR m_Color;//红黑节点的颜色域 KeyType m_Data;//红黑节点的数据域 RbNode *plchild,*prchild,*pParent;//红黑节点的三个指针};template<class KeyType> //红黑树类型class Rbtree{public: Rbtree() //构造函数 { pNullNode=new RbNode<KeyType>; pNullNode->m_Color=BLACK;//哨兵节点的颜色是黑色 pRoot=pNullNode; pNullNode->plchild=pRoot; pNullNode->prchild=pRoot; pNullNode->pParent=pRoot;//哨兵节点的指针域不重要 } const Rbtree& operator=(const Rbtree &OtherRbtree) {} //重载赋值运算符 ~Rbtree() {}public: RbNode<KeyType> *pNullNode;//哨兵节点 RbNode<KeyType> *pRoot;//红黑树的根节点}
树的旋转操作——左旋和右旋
在对红黑树进行插入和删除操作时,可能会破坏红黑树的五条性质。为了继续保持这些性质,必须对节点进行重新着色或者对树进行旋转操作(只修改指针,即修改树的结构)。树的旋转分为左旋和右旋,这是红黑树插入和删除操作的基础,下面分别进行介绍:
树的左旋基本思想:
如果节点A的右子树不为空,则将节点B的左子树作为A的右子树,A作为B的左子树。其余保持不变,如果有父亲域,还要修改父亲域。
树的右旋基本思想:
如果节点B的左子树不为空,则将节点A的右子树作为B的左子树,节点B作为A的右子树。其余保持不变,如果有父亲域,还要修改父亲域。
伪代码如下图所示:
LeftRoate(T, x) y ← x.right x.right ← y.left //1. 建立y的左孩子和x之间的关系 if y.left ≠ T.nil y.left.p ← x if x.p = T.nil //2. 建立y和x父亲节点之间的关系 T.root ← y else if x = x.p.left x.p.left ← y else x.p.right ← y y.p ← x.p y.left ← x //3. 建立y和x之间的关系 x.p ← y
右旋的伪代码与此相似,不再给出,下面给出左旋和右旋代码的C++描述:
bool LeftRotate(RbNode<KeyType> *pNode) //在指定节点处左旋操作{ if (pNode==pNullNode || pNode->prchild==pNullNode) return false; //1. 左子树作为pNode节点的右子树 RbNode<KeyType>* pRight=pNode->prchild; pNode->prchild=pRight->plchild; if (pRight->plchild!=pNullNode) pRight->plchild->pParent=pNode; //2. 建立pRight和pNode父亲节点之间的联系 if (pNode->pParent==pNullNode) pRoot=pRight; else if (pNode==pNode->pParent->plchild) pNode->pParent->plchild=pRight; else pNode->pParent->prchild=pRight; pRight->pParent=pNode->pParent; //3. 建立pRight和pNode之间的联系 pRight->plchild=pNode; pNode->pParent=pRight; return true;}
红黑树在指定节点处右旋操作的C++代码:
bool RightRotate(RbNode<KeyType> *pNode) //在指定节点处右旋操作{ if(pNode==pNullNode || pNode->plchild==pNullNode) return false; RbNode<KeyType>* pLeft=pNode->plchild; //1. 将pLeft的右孩子作为pNode的左孩子 pNode->plchild=pLeft->prchild; if (pLeft->prchild!=pNullNode) pLeft->prchild->pParent=pNode; //2. 建立pLeft和pNode父亲节点之间的联系 if (pNode->pParent==pNullNode) //根节点 pRoot=pLeft; else if (pNode==pNode->pParent->plchild) pNode->pParent->plchild=pLeft; else pNode->pParent->prchild=pLeft; pLeft->pParent=pNode->pParent; //3. 建立pLeft和pNode节点之间的联系 pLeft->prchild=pNode; pNode->pParent=pLeft; return true;}
红黑树的左旋和右旋操作是对称的,只需要修改指针,不修改颜色,可以在O(1)的时间内完成。
下面,我们将进一步剖析红黑树的左旋和右旋操作对红黑树性质的影响:
下图,图一中节点A和节点B的颜色都是RED,违反了红黑性质的第四条,节点C的左右子树黑高相同,则用方框框起来的三个子树黑高也是相同的,不违反红黑性质的第五条。我们的目标是使得该子树满足红黑性质。由于A的叔叔节点是黑色的,无法通过调整颜色达到目的,只能借助于旋转操作。
首先,将节点A的父亲节点颜色涂成黑色,如图二所示。此时,C的左子树的黑高比右子树的黑高大1,所以不满足红黑性质的第五条。对于违反第五条的情况,可以借助于旋转操作完成,将图二以节点C的轴右旋一次,可得到图三。在图三中,如果节点C的颜色还是保持黑色的,则又会导致右子树的黑高比左子树的黑高大1,因此,节点C的颜色必须修改为红色。伪代码如下:
//p指向节点Ap->parent->Color=BLACK;//修改父亲节点为黑色p->parent->parent->Color=RED;//修改祖父节点为红色RightRotate(p->parent->parent);//以祖父节点为轴右旋一次
对于图1和图3可以发现,红黑树的黑高在调整前后保持不变,只是A和B的颜色冲突调整好了,但整体树的结构发生了变化。
红黑树的插入操作
前面已经介绍了,红黑树在本质上是一棵二叉查找树,因此,其插入操作就是在二叉查找树的插入操作的基础上略作修改而来。因为插入的新节点可能破坏红黑性质,因此,必须对插入后的树进行维护操作。
C++源代码如下:
bool Rb_Insert(KeyType key) //红黑树的插入操作{ //1. 查找到待插入的位置 RbNode<KeyType> *pInsertPoint=pNullNode;//保存插入点 RbNode<KeyType> *pWalkNode=pRoot;//从根节点开始查找 while(pWalkNode!=pNullNode) { pInsertPoint=pWalkNode; if (pWalkNode->m_Data==key) return false; //元素不互异,返回false else if (pWalkNode->m_Data>key) pWalkNode=pWalkNode->plchild; else pWalkNode=pWalkNode->prchild; } //2. 创建新的节点,插入到合适的位置 RbNode<KeyType> *pNewNode=new RbNode<KeyType>; pNewNode->m_Color=RED; pNewNode->m_Data=key; pNewNode->plchild=pNullNode; pNewNode->prchild=pNullNode; //3. 建立新节点和父亲节点之间的关系 if (pInsertPoint==pNullNode) //树是空的 { pRoot=pNewNode; pNewNode->pParent=pNullNode; } else if (pInsertPoint->m_Data > key) pInsertPoint->plchild=pNewNode; else pInsertPoint->prchild=pNewNode; pNewNode->pParent=pInsertPoint; //4. 对插入新节点以后的红黑树进行维护操作 Rb_Insert_Fixup(pNewNode);//与传统BST_Insert()不同的地方 return true;}
红黑树插入的关键在于插入以后对红黑树进行维护操作,即Rb_Insert_Fixup(),过程比较复杂,具体描述如下:
我们知道插入的新节点颜色被涂成了红色,如果其父亲节点颜色是黑色,则不违反第四条一红生两黑和第五条黑高的性质,不需要做出调整。需要进行调整的情况是父亲节点是红色的,此时违反了第四条一红生两黑的性质。必须对红黑树进行着色修改和结构修改。分为以下两种情况:
如果叔叔节点是红色的,如下图图一所示:
首先由于新插入的节点B的父亲节点A是红色的,节点A和节点B违反了红黑性质的第四条。所以,我们必须修改父亲节点A的颜色为黑色。但是这样就会导致节点C左子树的黑高比右子树的黑高大1,违反了红黑性质的第五条,然而由于节点B的叔叔节点D是红色的,给我们留下了一个可修改的余地,可以将D的颜色改为黑色,这样节点C的右子树的黑高也增大了1,左右子树的黑高相同。但是仍然可能导致C的祖父节点左右子树黑高不同,所以我们需要进一步将C涂成红色,并作为当前节点对其祖先节点颜色进行修改。原则是最好不要破坏黑高性质。
如果叔叔节点是黑色的,这样我们无法通过上述的修改颜色的办法达到目的。只能通过前述的指针旋转的方式。具体原则:如果新插入节点的父亲节点是祖父节点的左孩子,而且新插入的节点是父亲节点的右孩子,则需要以父亲节点为轴左旋一次(将新插入节点旋转到左边),然后以祖父节点为轴整体右旋一次。如果新插入的节点的父亲节点是祖父节点的右孩子,而且新插入的节点是父亲节点的左孩子,则以父亲节点为轴右旋一次(将新插入的节点旋转到右边),然后以祖父节点为轴整体左旋一次。
伪代码如下所示:
RB-INSERT-FIXUP(T, z) while z.p.color == RED if z.p == z.p.p.left //在祖父节点的左子树上 then y ← z.p.p.right if y.color == RED then z.p.color ← BLACK ▹ Case 1 y.color ← BLACK ▹ Case 1 z.p.p.color ← RED ▹ Case 1 z ← z.p.p ▹ Case 1 else { if z == z.p.right z ← z.p ▹ Case 2 LEFT-ROTATE(T, z) ▹ Case 2 z.p.color ← BLACK ▹ Case 3 z.p.p.color ← RED ▹ Case 3 RIGHT-ROTATE(T, z.p.p) } // else (same as then clause with "right" and "left" exchanged) else //在祖父节点的右子树上 y←z.p.p.left;//叔叔节点 if y.color==RED //如果是红色,修改颜色的方式 then z.p.color ← BLACK; y.color ← BLACK; z.p.p.color ← RED; z ← z.p.p; else //不是红色,只能通过旋转的方式 { if z == z.p.left; z ← z.p; RIGHT-ROTATE(T, z); z.p.color ← BLACK z.p.p.color ← RED LEFT_ROTATE(T, z.p.p); }T.root.color ← BLACK
C++源码如下所示:
/*对违反红黑性质的红黑树进行维护*/void Rb_Insert_Fixup(RbNode<KeyType>* pRoot,RbNode<KeyType> *pInsertNode){ while (pInsertNode->pParent->m_Color==RED) //循环条件是父亲是红色的 { if (pInsertNode->pParent==pInsertNode->pParent->pParent->plchild) //是祖父节点的左孩子 { RbNode<KeyType> *pUncle=pInsertNode->pParent->pParent->prchild;//叔叔节点 if (pUncle->m_Color==RED) //叔叔节点是红色,修改颜色实现 { pInsertNode->pParent->pParent->m_Color=RED; //改成一红生两黑模式 pInsertNode->pParent->m_Color=BLACK; pUncle->m_Color=BLACK; pInsertNode=pInsertNode->pParent->pParent;//祖父作为当前节点 } else //叔叔节点已经是黑色的,只能通过旋转实现 { if (pInsertNode==pInsertNode->pParent->prchild)//如果是右孩子,需要先左旋一下 { pInsertNode=pInsertNode->pParent; LeftRotate(pInsertNode); } pInsertNode->pParent->m_Color=BLACK; pInsertNode->pParent->pParent->m_Color=RED; RightRotate(pInsertNode->pParent->pParent);//整体右旋 } } else //是祖父节点的右孩子 { RbNode<KeyType> *pUncle=pInsertNode->pParent->pParent->plchild;//叔叔节点 if (pUncle->m_Color==RED) //叔叔节点是红色,修改颜色实现 { pInsertNode->pParent->pParent->m_Color=RED; //改成一红生两黑模式 pInsertNode->pParent->m_Color=BLACK; pUncle->m_Color=BLACK; pInsertNode=pInsertNode->pParent->pParent;//祖父作为当前节点 } else //叔叔节点已经是黑色的,只能通过旋转实现 { if (pInsertNode==pInsertNode->pParent->plchild) //如果是左孩子,需要先右旋一下 { pInsertNode=pInsertNode->pParent; RightRotate(pInsertNode); } pInsertNode->pParent->m_Color=BLACK; pInsertNode->pParent->pParent->m_Color=RED; LeftRotate(pInsertNode->pParent->pParent); } } } pRoot->m_Color=BLACK; //将树的根节点颜色修改为黑色}
对于红黑树的删除操作,比较复杂,参考《算法导论》吧。
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- 红黑树
- DSP using MATLAB中的函数学习
- 【Python】Learn Python the hard way, ex32 for循环
- 对内核添加cramfs支持
- 静态内部类与成员内部类
- 千里之行,始于足下。
- 红黑树
- 黑马程序员——JavaSE之集合框架一
- Easier Done Than Said?
- JavaScript————BOM
- C++中的c_str()函数用法
- linux(centos)下ext4硬盘格式误删文件后的恢复(testdisk与photorec的使用)
- leetcode Move Zeroes
- 处理select下拉框默认选中
- String StringBuffer StringBuilder之间的区别