-
棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或BLACK。
-
树中每个结点包含5个属性:color、key、left、right和p。
-
如果一个结点没有子节点或父节点,则该节点相应指针属性值为NIL。可将这些NIL视为指向二叉搜索树的叶节点(外部结点)的指针,而把带关键字的结点视为树的内部节点。
红黑树的性质
- 每个结点或是红色,或是黑色。
- 根节点是黑色的
- 每个叶节点(NIL)是黑色的
- 如果一个结点是红色的,则它的两个子节点都是黑色的。(一个红结点的父节点和子节点都是黑色)
- 对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点。
为了便于处理,使用一个哨兵来代表NIL。对于红黑树T,T.nil是一个空节点,其color属性为黑色,其他属性为任意值。所有指向NIL的指针均使用哨兵T.nil替换。
一棵有n个内部节点的红黑树的高度至多为2lg(n+1)。
红黑树的初始化
-
红黑树
public class RedBlackTree { RedBlackTreeNode root; RedBlackTreeNode nil; public RedBlackTree() {} public RedBlackTree(RedBlackTreeNode root) { this.root = root; } }
-
树节点
public class RedBlackTreeNode { String color; int key; RedBlackTreeNode left; RedBlackTreeNode right; RedBlackTreeNode p; public RedBlackTreeNode() {} public RedBlackTreeNode(String color, int key, RedBlackTreeNode left, RedBlackTreeNode right, RedBlackTreeNode p) { this.color = color; this.key = key; this.left = left; this.right = right; this.p = p; } }
红黑树的常规操作
指针结构的修改:旋转
这是一种能保持二叉搜索树性质的搜索树局部操作。
-
左旋
public static void leftRotate(RedBlackTree T,RedBlackTreeNode x) { RedBlackTreeNode y = x.right; //把x结点和其新子节点都连接好 x.right = y.left; if (y.left != T.nil) { y.left.p = x; } //把x的根节点和y连接好 y.p = x.p; if (x.p == T.nil) { T.root = y; } else if (x == x.p.left) { x.p.left = y; } else { x.p.right = y; } //把y和x连接好 y.left = x; x.p = y; }
-
右旋
public static void rightRotate(RedBlackTree T,RedBlackTreeNode x) { RedBlackTreeNode y = x.left; //把y结点和其新子节点都连接好 x.left = y.right; if (y.right != T.nil) { y.right.p = x; } //把y的根节点和x连接好 y.p = x.p; if (x.p == T.nil) { T.root = y; } else if (x == x.p.left) { x.p.left = y; } else { x.p.right = y; } //把x和y连接好 y.right = x; x.p = y; }
左旋和右旋的时间复杂度均为O(1)。在旋转操作中,只有指针改变,其他所有属性均保持不变。
插入
-
RB-Insert(在红黑树T内插入结点z)
public static void RBInsert(RedBlackTree T,RedBlackTreeNode z) { RedBlackTreeNode y = T.nil; RedBlackTreeNode x = T.root; while (x != T.nil) { y = x; //y用来记录x的父节点 if (z.key < x.key) { x = x.left; } else { x = x.right; } } z.p = y; if (y == T.nil) { T.root = z; } else if (z.key < y.key) { y.left = z; } else { y.right = z; } z.left = T.nil; z.right = T.nil; z.color = "RED"; RBInsertFixup(T,z)//对结点重新着色并旋转 }
仅插入的时间复杂度为O(lgn)。
插入新节点z的时候,将着为红色,原因是因为结点z代替了黑色的哨兵,若z是黑色则必然导致其所有祖先结点的黑高度发生变化。而z是红结点时,其两个子节点都是哨兵T.nil,可使黑高度不变,即性质5仍然成立。
此时,插入操作仅可能破坏的性质就是性质2(根节点需要为黑色)和性质4(一个红结点不能有哄孩子)。
- 如果违反了性质2,则红色根节点一定是新增节点z,它是树中唯一的内部节点。此时不会违反性质4。
- 如果违反了性质4,必然是因为z和z.p都是红色的。
-
RB-Insert-Fixup(对结点重新着色并旋转)
public static void RBInsertFixup(RedBlackTree T,RedBlackTreeNode z) { while (z.p.color == "RED") { //z.p是红色的 if (z.p == z.p.p.left) { RedBlackTreeNode y = z.p.p.right; if (y.color == "RED") { //情况1 z.p.color = "BLACK"; y.color = "BLACK"; z.p.p.color = "RED"; z = z.p.p; } else if (z == z.p.right) { //情况2 z = z.p; leftRotate(T,z); } else { //情况3 z.p.color = "BLACK"; z.p.p.color = "RED"; rightRotate(T,z.p.p); } } else { RedBlackTreeNode y = z.p.p.left; if (y.color == "RED") { //情况1 z.p.color = "BLACK"; y.color = "BLACK"; z.p.p.color = "RED"; z = z.p.p; } else if (z == z.p.left) { //情况2 z = z.p; rightRotate(T,z); } else { //情况3 z.p.color = "BLACK"; z.p.p.color = "RED"; leftRotate(T,z.p.p); } } } T.root.color = "BLACK"; }
分为三种情况:区别在于z父亲的兄弟结点(或称为叔结点)的颜色不同
-
情况1:z的叔结点y是红色的。
因为z.p.p的颜色为黑色,故将z.p和y都着为黑色,以此解决z和z.p都是红色的问题,将z.p.p着为红色以保持性质5。把z.p.p作为新节点z来重复while循环。
-
情况2:z的叔结点y是黑色的且z是一个右孩子
-
情况3:z的叔结点y是黑色的且z是一个左孩子
情况2可以通过左旋变为情况3,因为z和z.p均为红色,该旋转不会影响其黑高和性质5。情况3中改变某些结点的颜色并做一次右旋,以保持性质5。
仅当情况1发生,然后指针沿着树上升两层,while循环才会重复执行,故while循环可能被执行的总次数为O(lgn)。其中旋转从不超过2次,因为只要执行了情况2或情况3,while循环就结束了。
-
整个插入操作(RBInsert和RBInsertFixup)的时间复杂度为O(lgn)。
删除
-
RB-Delete
public static void RBDelete(RedBlackTree T,RedBlackTreeNode z) { RedBlackTreeNode y = z; RedBlackTreeNode x; //x指向y的唯一子节点或指向哨兵T.nil, x.p总是指向y父节点的原始位置 String y_original_color = y.color; //发生改变前的y颜色 if (z.left == T.nil) { x = z.right; RBTransplant(T,z,z.right); } else if (z.right == T.nil) { x = z.left; RBTransplant(T,z,z.left); } else { y = treeMinimum(T,z.right); y_original_color = y.color; x = y.right; if (y.p == z) { x.p = y; } else { RBTransplant(T,y,y.right); y.right = z.right; y.right.p = y; } RBTransplant(T,z,y); y.left = z.left; y.left.p = y; y.color = z.color; } if (y_original_color == "BLACK") { //如果y是黑色就有可能引入一个或多个红黑性质被破坏的情况 RBDeleteFixup(T,x);//恢复红黑性质 } } public static void RBTransplant(RedBlackTree T,RedBlackTreeNode u,RedBlackTreeNode v) { if (u.p == T.nil) { T.root = v; } else if (u == u.p.left) { u.p.left = v; } else { u.p.right = v; } v.p = u.p; } public static RedBlackTreeNode treeMinimum(RedBlackTree T,RedBlackTreeNode x) { while (x.left != T.nil) { x = x.left; } return x; }
仅删除操作的时间复杂度为O(lgn)。
-
RB-Delete-Fixup
public static void RBDeleteFixup(RedBlackTree T,RedBlackTreeNode x) { RedBlackTreeNode w; while (x != T.root && x.color == "BLACK") { if (x == x.p.left) { w = x.p.right;//指向右边兄弟结点 if (w.color == "RED") { //情况1 w.color = "BLACK"; x.p.color = "RED"; leftRotate(T,x.p); w = x.p.right; } if (w.left.color == "BLACK" && w.right.color == "BLACK") { //情况2 w.color = "RED"; x = x.p; } else if (w.right.color == "BLACK") { //情况3 w.left.color = "BLACK"; w.color = "RED"; rightRotate(T,w); w = x.p.right; } else { //情况4 w.color = x.p.color; x.p.color = "BLACK"; w.right.color = "BLACK"; leftRotate(T,x.p); x = T.root; } } else { w = x.p.left; if (w.color == "RED") { //情况1 w.color = "BLACK"; x.p.color = "RED"; rightRotate(T,x.p); w = x.p.left; } if (w.left.color == "BLACK" && w.right.color == "BLACK") { //情况2 w.color = "RED"; x = x.p; } else if (w.left.color == "BLACK") { //情况3 w.right.color = "BLACK"; w.color = "RED"; leftRotate(T,w); w = x.p.left; } else { //情况4 w.color = x.p.color; x.p.color = "BLACK"; w.left.color = "BLACK"; rightRotate(T,x.p); x = T.root; } } } x.color = "BLACK"; }
只有当y为黑色时才会执行RBDeleteFixup。因为如果y为红色,当y被删除或移动时,红黑性质仍然保持。原因如下:
- 树中的黑高没有发生变化
- 不存在两个相邻的红结点。y的新位置不可能有两个相邻的红结点,因为z本来就是满足性质的。如果y是红色,则x一定是黑色,用x替代y不可能使两个红结点相邻。
- 如果y是红色,就不可能是根节点
如果y是黑色的,会产生三个问题:
-
如果y是原来的根节点,而y的一个红色孩子成为新的根节点,违反了性质2.
-
如果x和x.p是红色的,违反了性质4.
-
在树中移动y将导致先前包含y的任何简单路径上的黑结点个数少1。y的任何祖先都不满足性质5.
解决办法:
假设将占有y的x的颜色视为还有一层额外的黑色。这样性质5仍然成立。x此时为红黑色或双重黑色,x的属性仍然为RED或BLACK。
恢复红黑性质分三种情况:
-
情况1:x的兄弟结点w是红色的
将x.p着为红色,w着为黑色,对x.p做一次左旋而不违反红黑树任何性质。现在x的新兄弟结点w是旋转之前w的某个子节点,其颜色为黑色,故将情况1转换为了情况2、3、4。
-
情况2:x的兄弟结点w是黑色的,而且w的两个子节点都是黑色的
将x和w去掉一重黑色,则x只有一重黑色,w为红色,为了补偿去掉的这重黑色,将它们的父节点x.p加上一重黑色,并将x.p作为新的x来循环。如果是通过情况1进入情况2,则此时新x红黑色的,因为之前x.p是红色的。新x的color为RED,测试循环条件后循环终止。
-
情况3:x的兄弟结点w是黑色的,而w的左孩子是红色的,右孩子是黑色的
交换w和其左孩子的颜色,对w进行右旋而不违反红黑树的任何性质。此时情况3转换为情况4。
-
情况4:x的兄弟结点w是黑色的,而w的右孩子是红色的
将w和其父节点的颜色交换,将右孩子的颜色变为黑色,左旋不违法红黑树的任何性质,将x设置为根后,当while测试循环条件时终止。
在RBDeleteFixup中,情况1、3和4在各执行常数次数的颜色改变和至多三次旋转后便终止。情况2是while循环可以重复执行的唯一情况,然后指针x沿树上升至多O(lgn)次,且不做任何旋转。故RBDeleteFixup要花费O(lgn)时间,做至多3次旋转。
整个删除操作的时间复杂度为O(lgn)。
数据结构的扩张
顺序统计树(order-statistic tree)
顺序统计树T只是简单地在每个结点上存储附加信息的一棵红黑树。
红黑树的结点:
public class RedBlackTreeNode {
String color;
int key;
int size; //包含以该节点为根的子树(包括该节点)的内结点数,即这棵子树的大小
RedBlackTreeNode left;
RedBlackTreeNode right;
RedBlackTreeNode p;
public RedBlackTreeNode() {}
public RedBlackTreeNode(String color, int key, RedBlackTreeNode left, RedBlackTreeNode right, RedBlackTreeNode p) {
this.color = color;
this.key = key;
this.left = left;
this.right = right;
this.p = p;
}
}
对于结点x,x.size = x.left.size + x.right.size + 1;
在一棵顺序统计树中,并不要求关键字各不相同。定义一个元素的秩为在中序遍历时输出的位置。
通过size这个附加信息,可以查找具有给定秩的元素和确定一个元素的秩。
-
查找具有给定秩的元素
OS-Select
public static OS_RedBlackTreeNode OsSelect(OS_RedBlackTreeNode x,int i) { int r = x.left.size + 1; if (i == r) { return x; } else if (i < r) { return OsSelect(x.left,i); } else { return OsSelect(x.right,i - r); } }
每次递归都沿树下降一层,整个运行时间为O(lgn)。
-
确定一个元素的秩
OS-Rank
public static int OsRank(OS_RedBlackTree T,OS_RedBlackTreeNode x) { int r = x.left.size + 1; OS_RedBlackTreeNode y = x; while (y != T.root) { if (y == y.p.right) { r = r + y.p.left.size + 1; } y = y.p; } return r; }
返回对T中序遍历对应的线性序中x的位置。
while的每次迭代沿树上升一层,最坏情况的运行时间为O(lgn)。
对size的维护
插入操作
分两个阶段
-
第一阶段:维护子树的规模
对由根至叶子的路径上遍历的每个结点x,都增加x.size的属性。新增结点的size为1。由于一条遍历的路径上共有O(lgn)个结点,故维护size属性的额外代价为O(lgn)。
-
第二阶段:参加旋转的两个结点的size维护。
旋转次数至多为2,代价为O(1)。
旋转
-
左旋
public static void leftRotate(OS_RedBlackTree T, OS_RedBlackTreeNode x) { OS_RedBlackTreeNode y = x.right; //把x结点和其新子节点都连接好 x.right = y.left; if (y.left != T.nil) { y.left.p = x; } //把x的根节点和y连接好 y.p = x.p; if (x.p == T.nil) { T.root = y; } else if (x == x.p.left) { x.p.left = y; } else { x.p.right = y; } //把y和x连接好 y.left = x; x.p = y; y.size = x.size; x.size = x.left.size + x.right.size + 1; }
-
右旋
public static void rightRotate(OS_RedBlackTree T, OS_RedBlackTreeNode x) { OS_RedBlackTreeNode y = x.left; //把y结点和其新子节点都连接好 x.left = y.right; if (y.right != T.nil) { y.right.p = x; } //把y的根节点和x连接好 y.p = x.p; if (x.p == T.nil) { T.root = y; } else if (x == x.p.left) { x.p.left = y; } else { x.p.right = y; } //把x和y连接好 y.right = x; x.p = y; y.size = x.size; x.size = x.left.size + x.right.size + 1; }
顺序统计树的插入元素所需要的总时间为O(lgn)。
删除操作
分两个阶段:
-
第一阶段:维护子树的规模
遍历一条由结点y(从他的原始位置开始)至根的简单路径,并减少路径上每个结点的size属性的值。代价为O(lgn)。
-
第二阶段:参加旋转的两个结点的size维护
至多三次旋转,代价为O(1)。
顺序统计树的删除元素所需要的总时间为O(lgn)。
如何扩张数据结构
分为4个步骤:
- 选择一种基本的数据结构
- 确定基础数据结构中要维护的附加信息
- 检验基础数据结构上的基本修改操作能否维护附加信息
- 设计一些新操作
区间树(interval tree)
区间树是一种对动态集合进行维护的红黑树,其中每个元素x都包含一个区间x.int。
两区间i和j,存在三种情况:
- 区间i和区间j重叠 (i.low <= j.high 且j.low <= i.high)
- i在j的左边 (i.high<j.low)
- i在j的右边 (j.high < i.low)
-
基础数据结构
选择一棵红黑树,每个结点x包含一个区间属性x.int。且x的关键字为区间低端点x.int.low。该数据结构按中序遍历列出的就是按低端点的次序排列的各区间。
-
附加信息
每个结点还包含一个值x.max,它是以x为根的子树中所有区间的端点的最大值。
-
对信息的维护
x.max = max(x.int.high,x.left.max,x.right.max)
-
设计新操作
Interval-Search,用来找出树T中与区间i重叠的那个结点。
区间树结点
-
public class INT_RedBlackTreeNode { String color; int key; int max; Interval i; INT_RedBlackTreeNode left; INT_RedBlackTreeNode right; INT_RedBlackTreeNode p; } class Interval { int low; int high; }
-
Interval-Search
public static INT_RedBlackTreeNode intervalSearch(INT_RedBlackTree T,Interval i) { INT_RedBlackTreeNode x = T.root; while (x != T.nil && (i.high < x.i.low || x.i.high < i.low)) { if (x.left != T.nil && x.left.max >= i.low) { x = x.left; } else { x = x.right; } } return x; }
intervalSearch耗费O(lgn)时间