概要
红黑树是许多“平衡”搜索树中的一种,可以保证在最坏情况下基本动态集合操作的时间复杂度是O(logn)。
13.1红黑树的性质
一概而言之,总共分为四个性质:
-
每个结点或是红色,或是黑色。
-
根结点是黑色的,叶结点也是黑色的。
-
红结点的两个子结点必然是黑色的。
-
从一个结点(不包含该结点)到其后代叶结点的简单路径上,均包含相同数目的黑色结点(即黑高度相同)。
书中有引理13.1说道:
一棵有n个内部结点的红黑树的高度至多为2lg(n+1)。
对于此引理的证明书中的是先归纳证明1个根结点x至少包含2bh(x)-1个内部结点(证明略),由此得出x为根结点的子树至少包含(2bh(x)-1)+(2bh(x)-1)+1个内部结点。
再由性质4得出bh(x)>=h/2,结合上条归纳证明完成引理证明。
**注:此处说的内部结点是包含根结点但不包含叶结点的所有结点。bh(x)为结点x的黑高度。
另一种对于该引理的证明出自视频讲解,视频中给出的证明法需要先归纳证明n个内部结点拥有n+1个叶子结点,其次通过将红黑树改为2-3-4树得出了2h(x)<=叶子结点数<=4h(x)的结论,由此同样能证明引理13.1。
完成引理的证明,可以得到动态集合操作SEARCH、MINIMUM、MAXIMUM、SUCCESSOR、PREDECESSOR可在红黑树上在O(lgn)时间内完成。至于DELETE、INSERT操作的消耗时间在后面做讨论。
(链接: 视频讲解)
13.1 练习
13.6-1 在一棵黑高为k的红黑树中,内部结点最多可能有多少个?最少可能有多少个?
这里结合视频讲解的思路来解答。
红黑树转为2-3-4树后,2k<=叶子结点的个数<=4k
又已知叶子结点个数=内部结点数+1
因此最少有2k-1个,最多有22k-1个内部结点。
13.1-7 试描述一棵含有n个关键字的红黑树,使其红色内部结点个数与黑色内部结点个数的比值最大。这个比值是多少?该比值最小的树又是怎样呢?比值是多少?
先讨论比值最小的情况,此时比值为0即整棵树都是黑结点。
最大情况为2:1,还是借用2-3-4树来解答,每个黑结点在吸收自己的下层红色结点时最多可以吸收两个红结点即该黑色结点的左右子结点均为红色结点,那么比值最大情况就是一棵树的内部结点中所有黑色结点均吸收两个红色子结点,此时比值就是2:1。
13.2 旋转
无外乎左旋转和右旋转
这一节主要用代码实现左旋转和右旋转的具体操作,个人认为没有必要想太复杂,对着书上的这张小图直接理清思路写就行了。
#region "左旋转和右旋转"
/// <summary>
/// 左旋转
/// </summary>
/// <param name="t">目标树</param>
/// <param name="x">待旋转结点</param>
public static void Left_Rotate(Tree<int> t, Node<int> x)
{
var y = x.Right;
x.Right = y.Left;
if (y.Left != t.Tnil)
{
y.Left.Parent = x;
}
y.Parent = x.Parent;
if (x.Parent == t.Tnil)
{
t.Root = y;
}
else if (x.Parent.Left == x)
{
x.Parent.Left = y;
}
else
{
x.Parent.Right = y;
}
y.Left = x;
x.Parent = y;
}
/// <summary>
/// 右旋转
/// </summary>
/// <param name="t">目标树</param>
/// <param name="x">待旋转结点</param>
public static void Right_Rotate(Tree<int> t, Node<int> x)
{
var y = x.Left;
x.Left = y.Right;
if (y.Right != t.Tnil)
{
y.Right.Parent = x;
}
y.Parent = x.Parent;
if(x.Parent == t.Tnil)
{
t.Root = y;
}
else if(x.Parent.Left == x)
{
x.Parent.Left = y;
}
else
{
x.Parent.Right = y;
}
y.Right = x;
x.Parent = y;
}
#endregion
这里要注意的还是操作的执行顺序,尽量不要在替换操作之前就重新确立x和y结点的父子关系,因为从逻辑上来说要是先重新确立了x和y的父子关系很有可能会影响x和x父节点的原先关系被遗忘。
还有一点就是关于y的判空处理,从代码不难看出无论左旋转还是右旋转都没有对y进行判空,个人认为是旋转操作默认了y结点是一定存在,要是y不存在就根本不会去执行旋转操作。
Left_Rotate、Right_Rotate都在O(1)时间内运行完成。
13.2 练习
13.2-1 证明:在任何一棵有n个结点的二叉树搜索树中,恰有n-1种可能的旋转。
网上比较多的证明方法是通过归纳法来证明:
当n=1时,1个结点没办法做旋转所以为0种可能;
假设n=k-1时,有k-2种旋转可能;
当n=k+1时候,等于是在k个结点的一棵树种插入了一个新结点,新结点作为叶子结点和父结点存在旋转可能,所以为k-2+1=k-1种可能。
这种证明总感觉怪怪的,以下是个人的看法:
还有n个结点的一棵搜索二叉树,其必然有n-1条边(一条边链接的两个结点就代表一种大小关系),那么n-1条边就能理解成有n-1种旋转可能吧。
13.2-4 证明:任何一棵含有n个结点的二叉搜索树可以通过O(n)次旋转,转变为其他任何一颗含n个结点的二叉搜索树。(提示:先证明至多n-1次右旋足以将树旋转变为一条右侧伸展的链。)
按照提示先证明:至多n-1次右旋足以将树旋转变为一条右侧伸展的链
先说一下将一棵树通过右旋变为右侧链的算法:从根结点开始遍历现有右侧链的每个结点(根结点也是右侧链的结点)对每个遍历到的结点反复右旋直到该结点没有左子树为止,遍历完成后即可得到一条右侧伸展链。在这个过程中至多进行n-1次旋转,毕竟n结点拥有n-1条边。
证明完这点后就可以得出一个结论,任意二叉树经过n-1次旋转变为一条右侧伸展的链,一条右侧伸展的链经过至多n-1次左旋也可以变为任意二叉树。
通过上述结论就可以解得含有n个结点的任意两颗二叉树转换至多需要2(n-1)次旋转,也就是O(n)次旋转。
*13.2-5 如果能够使用一系列的RIGHT-ROTATE调用把一个二叉搜索树T1变为二叉搜索树T2,则称T1可以右转成T2。试给出一个例子表示两棵树T1,其中T1不能够右转成T2。然后,证明:如果T1可以右转成T2,那么它可以通过O(n2)次RIGHT-ROTATE调用来实现右转。
先讲一下第一问,举个例子说明T1不能右转成T2,只要T2的右侧伸展链结点数小于T1的右侧伸展链结点数,T1必定不能右转成T2。
再证明一下第二问,一开始把这个问题想复杂了。
从本质上来说,一棵只能做右旋操作的树,它的每一条边连接的两个结点必然只能做一次右旋操作即存在x1,x2两个数作为一条边在做右旋的边连接的两个结点,当若x1>x2,则右旋完后x1和x2不会再有比较的机会,我们就是找出所有的比较组合,每一种至多出现一次,也就是n!/2!*(n-2)! = O(n2)。
13.3 插入
13.1中提到在普通二叉树中的插入删除操作都是通过O(lgn)完成,但是在红黑书中插入删除并不支持普通二叉树的插入删除算法。
得出红黑书的插入算法消耗时间,首先完成红黑书的插入算法:
#region "红黑树的插入"
/// <summary>
/// 红/黑枚举
/// </summary>
public enum COLOR
{
RED,
BLACK
}
/// <summary>
/// 红黑树结点
/// </summary>
/// <typeparam name="T"></typeparam>
public class RbNode<T>
{
public T key { get; set; }
public RbNode<T> Parent { get; set; }
public RbNode<T> Left { get; set; }
public RbNode<T> Right { get; set; }
public RbNode<T> Successor { get; set; }
public COLOR Color { get; set; }
public RbNode()
{
Parent = null;
Left = null;
Right = null;
Successor = null;
Color = COLOR.BLACK;
}
public RbNode(T k)
{
key = k;
Parent = null;
Left = null;
Right = null;
Successor = null;
Color = COLOR.RED;
}
}
/// <summary>
/// 红黑树
/// </summary>
/// <typeparam name="T"></typeparam>
public class RbTree<T>
{
public RbNode<T> Root { get; set; }
public RbTree()
{
Root = null;
}
}
/// <summary>
/// 红黑树的插入以及修复
/// </summary>
/// <param name="t">目标树</param>
/// <param name="z">待插入结点</param>
public static void Rb_Insert(RbTree<int> t, RbNode<int> z)
{
RbNode<int> y = t.Tnil;
var x = t.Root;
while (x != t.Tnil)
{
y = x;
x = x.key > z.key ? x.Left : x.Right;
}
z.Parent = y;
if (y == t.Tnil)
{
t.Root = z;
}
else if (y.key > z.key)
{
y.Left = z;
}
else
{
y.Right = z;
}
z.Left = t.Tnil;
z.Right = t.Tnil;
z.Color = COLOR.RED;
Rb_Insert_Fix(t, z);
}
/// <summary>
/// 红黑书的插入修正
/// </summary>
/// <param name="t"></param>
/// <param name="z"></param>
public static void Rb_Insert_Fix(RbTree<int> t, RbNode<int> z)
{
while (z.Parent.Color == COLOR.RED)
{
if (z.Parent.Parent.Left == z.Parent)
{
var y = z.Parent.Parent.Left;
// case1
if (z.Parent.Parent.Right.Color == COLOR.RED)
{
y.Color = COLOR.BLACK;
z.Parent.Parent.Right.Color = COLOR.BLACK;
z.Parent.Parent.Color = COLOR.RED;
z = z.Parent.Parent;
}
// case2
else if (z.Parent.Right == z)
{
z = z.Parent;
Left_Rotate(t, z);
}
// case3
z.Parent.Color = COLOR.BLACK;
z.Parent.Parent.Color = COLOR.RED;
Right_Rotate(t, z.Parent.Parent);
}
else
{
var y = z.Parent.Parent.Left;
// case1`
if (y.Color == COLOR.RED)
{
y.Color = COLOR.BLACK;
z.Parent.Color = COLOR.BLACK;
z.Parent.Parent.Color = COLOR.RED;
z = z.Parent.Parent;
}
// case2`
else if (z.Parent.Left == z)
{
z = z.Parent;
Right_Rotate(t, z);
}
// case3`
z.Parent.Color = COLOR.BLACK;
z.Parent.Parent.Color = COLOR.RED;
Left_Rotate(t, z.Parent.Parent);
}
}
t.Root.Color = COLOR.BLACK;
}
#endregion
其实红黑书本身的实现个人认为不是难点,只要脑海里面对case1,case2,case3(包括对称的case1`等)三种插入结点后的情况有个清晰的画面,依照不同的情况实现代码分支即可。
我觉得比较关键的在于书中对于该循环不变式(即Rb_Insert_Fix函数)的证明,书中对于这块的说明也非常详细,这周若是有时间会补上一些自己的理解和看法。
13.3 练习
13.3-5 考虑一棵用RB-INSERT插入n个结点而成的红黑树。证明:如果n>1,则该树至少有一个红结点。
分两种情况来考虑,假如在拥有n-1个结点的红黑树中插入一个结点。(设结点为x)
第一种情况,结点x的父节点x.parent为黑色,那么不用对该红黑树做调成,x作为红结点保存在树中。
第二种情况,结点x的父节点x.parent为红色,即要对红黑树做调整。我们知道对一课红黑树做调整,如果只经过case1情况,那么x结点不管父节点,祖父结点怎么变色,自身永远是保持红色。如果经过case2(case3)情况,也是对x结点的父节点x.parent进行旋转,旋转完后x.parent也是无论其他结点怎么变色,自身一定保持红色。
综上两种情况,该树都至少有一个红结点。
13.3-6 说明如果红黑树的表示中不提供父指针,应当如何有效地实现RB-INSERT。
在插入时候,对插入结点x的路径上所有经过的结点用栈进行存储,当插入完成后,栈顶的元素就是x的父节点,其后便是x的祖父结点,以此类推。
以下是代码实现:
public class RbTree<T>
{
public RbNode<T> Root { get; set; }
public RbNode<T> Tnil { get; set; }
}
public class RbNode<T>
{
public T Key { get; set; }
public RbNode<T> Left { get; set; }
public RbNode<T> Right { get; set; }
public COLOR Color { get; set; }
public RbNode(T key)
{
Key = key;
}
}
public static void RB_Insert(RbTree<int> t, RbNode<int> z, Stack<RbNode<int>> s)
{
RbNode<int> y = t.Tnil;
var x = t.Root;
while (x != t.Tnil)
{
y = x;
s.Push(y);
if (x.Key > z.Key)
{
x = x.Left;
}
else
{
x = x.Right;
}
}
if (y == t.Tnil)
{
t.Root = z;
}
else if (y.Key > z.Key)
{
y.Left = z;
}
else
{
y.Right = z;
}
z.Right = t.Tnil;
z.Left = t.Tnil;
z.Color = COLOR.RED;
RB_Insert_Fix(t, s, z);
}
private static void RB_Insert_Fix(RbTree<int> t, Stack<RbNode<int>> s, RbNode<int> x)
{
if (s.Count() <= 1)
{
t.Root.Color = COLOR.BLACK;
return;
}
while (s.Count >= 2
&& s.Peek().Color != COLOR.RED)
{
var y = s.Pop();
var z = s.Pop();
if (z.Left == y)
{
// case1
if (z.Right.Color == COLOR.RED)
{
z.Color = COLOR.RED;
z.Right.Color = COLOR.BLACK;
y.Color = COLOR.BLACK;
}
else
{
// case2
if (y.Right == x)
{
Left_Rotate(t, y, z);
y = x;
}
// case3
var z_p = s.Count() < 1 ? null : s.Peek();
z.Color = COLOR.RED;
y.Color = COLOR.BLACK;
Right_Rotate(t, z, z_p);
}
}
else
{
// case1`
if (z.Left.Color == COLOR.RED)
{
z.Color = COLOR.RED;
z.Left.Color = COLOR.BLACK;
y.Color = COLOR.BLACK;
}
else
{
// case2`
if (y.Left == x)
{
Right_Rotate(t, y, z);
y = x;
}
// case3`
var z_p = s.Count() < 1 ? null : s.Peek();
z.Color = COLOR.RED;
y.Color = COLOR.BLACK;
Left_Rotate(t, z, z_p);
}
}
}
t.Root.Color = COLOR.BLACK;
}
public static void Right_Rotate(RbTree<int> t,
RbNode<int> x, RbNode<int> p)
{
var y = x.Left;
x.Left = y.Right;
if (p == t.Tnil)
{
t.Root = x;
}
else if (p.Key > y.Key)
{
p.Left = y;
}
else
{
p.Right = y;
}
y.Right = x;
}
public static void Left_Rotate(RbTree<int> t,
RbNode<int> x, RbNode<int> p)
{
var y = x.Right;
x.Right = y.Left;
if (p == t.Tnil)
{
t.Root = x;
}
else if (p.Key > y.Key)
{
p.Left = y;
}
else
{
p.Right = y;
}
y.Left = x;
}
}
13.4 删除
删除操作从代码上看和普通搜索二叉树类似,但是这里还是踩到了一个很小的坑。在写删除代码时候同样先 写红黑树的RB_Transplant处理。
先附上代码:
public static void RB_Transplant(RbTree<int> t,
RbNode<int> u, RbNode<int> v)
{
if (u.Parent == t.Tnil)
{
t.Root = v;
}
else if (u.Parent.Left == u)
{
u.Parent.Left = v;
}
else
{
u.Parent.Right = v;
}
v.Parent = u.Parent;
}
这里和普通二叉树的删除操作不一样的是,普通二叉树在判断u.parent为空时候代码为u.parent == null,这里引入了哨兵结点,所以判断结果不一样。最后的父结点设置,普通二叉树删除是要做v==null的判断,此处无论v是否为哨兵结点,都视为对该结点赋值。
以下是删除代码:
#region "红黑树的删除"
/// <summary>
/// 求最小结点
/// </summary>
/// <param name="x">头结点</param>
/// <returns></returns>
public static RbNode<int> Tree_Minimum(RbNode<int> x)
{
RbNode<int> y = null;
while (x != null)
{
y = x;
x = x.Left;
}
return y;
}
/// <summary>
/// 转移
/// </summary>
/// <param name="t">红黑树</param>
/// <param name="u">替换结点</param>
/// <param name="v">转移结点</param>
public static void RB_Transplant(RbTree<int> t,
RbNode<int> u, RbNode<int> v)
{
if (u.Parent == t.Tnil)
{
t.Root = v;
}
else if (u.Parent.Left == u)
{
u.Parent.Left = v;
}
else
{
u.Parent.Right = v;
}
v.Parent = u.Parent;
}
/// <summary>
/// 删除
/// </summary>
/// <param name="t">红黑树</param>
/// <param name="z">删除结点</param>
public static void RB_Delete(RbTree<int> t, RbNode<int> z)
{
var y = z;
var y_oraginal_color = y.Color;
RbNode<int> x;
if (z.Left == t.Tnil)
{
x = z.Right;
RB_Transplant(t, z, z.Right);
}
else if (z.Right == t.Tnil)
{
x = z.Left;
RB_Transplant(t, z, z.Left);
}
else
{
y = Tree_Minimum(z.Right);
y_oraginal_color = y.Color;
x = y.Right;
if (y.Parent == z)
{
x.Parent = y;
}
else
{
RB_Transplant(t, y, y.Right);
y.Right = z.Right;
y.Right.Parent = y;
}
RB_Transplant(t, z, y);
y.Left = z.Left;
y.Left.Parent = y;
y.Color = z.Color;
if (y_oraginal_color == COLOR.BLACK)
{
RB_Delete_Fixup(t, x);
}
}
}
#endregion
RB_Delete_Fixup函数的代码在下文放出,这里说到的小坑主要是出现在y = Tree_Minimum(z.Right);这句话后,当y结点就是被删除结点的子结点时,对此时的x结点有一个x.Parent = y;的操作。这在普通二叉树删除代码中是不出现的,其目的主要是考虑到当x为哨兵结点时,给当前x结点设置父结点。(因为我们在插入一个结点后,从来都不会为一个哨兵结点去设置父结点)。
接着是 RB_Delete_Fixup函数的代码:
/// <summary>
/// 删除操作后的结点修复
/// </summary>
/// <param name="t">红黑树</param>
/// <param name="x">追踪到的x结点</param>
private static void RB_Delete_Fixup(RbTree<int> t, RbNode<int> x)
{
while (t.Root != x && x.Color == COLOR.BLACK)
{
if (x.Parent.Left == x)
{
var w = x.Parent.Right;
// case1
if (w.Color == COLOR.RED)
{
x.Parent.Color = COLOR.RED;
w.Color = COLOR.BLACK;
Left_Rotate(t, x.Parent);
w = x.Parent.Right;
}
// case2
else if (w.Left.Color == COLOR.BLACK
&& w.Right.Color == COLOR.BLACK)
{
w.Color = COLOR.RED;
x = x.Parent;
}
// case3
else if (w.Right.Color == COLOR.BLACK)
{
w.Color = COLOR.RED;
w.Left.Color = COLOR.BLACK;
Right_Rotate(t, w);
w = x.Parent.Right;
}
// case4
w.Color = x.Parent.Color;
x.Parent.Color = COLOR.BLACK;
w.Right.Color = COLOR.BLACK;
Left_Rotate(t, x.Parent);
x = t.Root;
}
else
{
// case1`
var w = x.Parent.Left;
if (w.Color == COLOR.RED)
{
x.Parent.Color = COLOR.RED;
w.Color = COLOR.BLACK;
Right_Rotate(t, x.Parent);
w = x.Parent.Left;
}
// case2`
else if (w.Left.Color == COLOR.BLACK
&& w.Right.Color == COLOR.BLACK)
{
w.Color = COLOR.RED;
x = x.Parent;
}
// case3`
else if (w.Left.Color == COLOR.BLACK)
{
w.Color = COLOR.RED;
w.Right.Color = COLOR.BLACK;
Left_Rotate(t, w);
w = x.Parent.Left;
}
// case4`
w.Color = x.Parent.Color;
x.Parent.Color = COLOR.BLACK;
w.Left.Color = COLOR.BLACK;
Right_Rotate(t, x.Parent);
x = t.Root;
}
}
x.Color = COLOR.BLACK;
}
删除操作里面同样分1,2,3,4四种情况,第一个要注意的点是四种情况从转换前到转换后,子树的黑高度依然要保持原状,第二个要注意的点是这里的x,正如书里面所说的是双重颜色(黑黑或红黑),当x在重新赋值(比如x=x.parent)时候要意识到新的x结点同样是双重颜色,只有当x为红黑色时候,我们才跳出循环。