[Dr.Lib]Note:Algorithms – LLRB——红黑树的现代实现

Via http://www.cnblogs.com/Seiyagoo/archive/2013/10/13/3366290.html

一、本文内容

以一种简明易懂的方式介绍红黑树背后的逻辑实现2-3-4树,以及红黑树的插入、删除操作,重点在2-3-4树与红黑树的对应关系上,并理清红黑树相关操作的来龙去脉。抛弃以往复杂的实现,而分析红黑树的一种简单实现LLRB。
 
 

二、算法应用

红黑树,给人以强烈的第一听觉冲击力——红与黑,好像很高端的感觉。事实上的确如此,红黑树是一种高级数据结构,在C++、Java的标准库里作为set、map的底层数据结构实现,以及linux中进程的公平调度。
 
 

三、2-3-4树

标题是红黑树,为什么讲2-3-4树?因为红黑树就是2-3-4树的一种等价形式,更准确地来说,我们用红黑树来完成2-3-4树的各种操作(如插入、删除)。原因就是2-3-4树的实现即维护太麻烦。所以理解2-3-4树才能真正理解红黑树。而历史就是这么发展的,了解过去,现在的一切才有了意义。算法导论关于红黑树这一节就忽略了这一点,让人知其然而不知其所以然。
 
OK,暂时先忽略复杂的红黑树,从简单的2-3-4树开始。
 
1、定义
 
2-3-4树是一种泛化的BST,它的每个结点允许1,2或者3个键(key),那么对应的有三种结点:
2-node:一个key,两个孩子;
3-node:二个key,三个孩子;
4-node:三个key,四个孩子。
注:k-node表示有k个链接(link)。泛化的BST还有2-3树,B树等。
 
从图中可以看出2-3-4树的另一个性质:它是完全平衡的(等高),即从根结点到叶子结点距离相等。
 
2、插入操作
2-3-4树本身就是一种查找树(中序遍历有序),故其查找操作同二叉查找。
 
2-3-4树的插入操作类似二叉查找树,先是查找操作失败(从根结点查找到叶子结点),然后在底部的叶子结点插入。
因为2-3-4树的结点有三种类型,所以操作有点差异。对于2-node和3-node,分别直接插入可变成3-node,4-node;但是对于4-node若直接插入则违反了定义。在4-node插入之前,先分裂4-node成2个2-node,再将待插入的key插入对应的2-node。 如下图,H查找失败,在H插入4-node(由三个key F、G、J组成)之前,先对该4-node分裂(将三个key的中间值提上父节点,剩余的二个key分别作为中间key的左右孩子),然后再将H插入2-node J中。这样操作的结果是查找到达底部叶子结点时,始终是2-node或者3-node。
 
插入算法思想:自下而上的算法由原作者Bayer在1972年提出,自上而下的算法由Guibas-Sedgewick(红黑树这个名字来源于他们)在1978年提出,然后30年后也就是2008年Sedgewick教授又改进了红黑树的操作,也就是后面要介绍的LLRB。
 
自上而下的算法思路是,从根结点向下的查找过程中,遇到4-node就分裂,最后在底部的叶子结点插入。
那么为什么遇到4-node就分裂呢?4-node不是2-3-4树的一种合法结点类型吗?
答案可以从后面LLRB的算法思路可以得出。
 
因为遇到4-node就分裂就保证了当前结点不是4-node,则分裂孩子的4-node有两种情形:
分裂4-node的case 1
 
 
分裂4-node的case 2

注:上面的变换在树中任意位置都成立。
 
 
下面两张图是完整的插入过程(只有分裂结点类型为4-node的根结点才会导致树高增1):
 
3、平衡性分析
2-3-4树的树高在最坏情况下为lgN(所有结点都是2-node型),最好情况下为lg4 N = 1/2 lgN(所有结点都是4-node型),2-3-4树的查找、插入操作都是lgN。
 
 

四、红黑树

 
终于到了高富帅——红黑树。。。
从2-3-4树的介绍可以看出,对2-node、3-node、4-node的不同数据类型进行转换,但所涉及的大部分任务使用这种直接的表示方法来实现并不方便。所以可以用
一种统一的方式完成转换,而只需很小的开销。这就是红黑树存在的意义,既有BST的标准搜索过程,又有2-3-4树的简单插入平衡过程。
 
下面介绍LLRB(Left-leaning red-black trees),而不是标准的红黑树。

1、定义

LLRB有三个特点:
(1)用BST来表示2-3-4树;
(2)用红边(红链接)来连接2-node来表示3-node和4-node(如下图);
(3)3-node必须是向左倾斜的(两者的大者作为根)。
 
LLRB相对于标准的RB多了特点3,在标准的RB中右向倾斜的红链接是允许的。对于特点2,在物理上用一个bit(红或黑)来存储以表示指向该结点的红链接。
红链接来连接3-node或者4-node的内部key,而黑链接则连接外部的key;为了理解,可以消除红链接并将它们连接的结点都折叠起来(即将看做红链接连
接的点缩为一个点),则可以看出黑链接个数不变。
2-3-4树与红黑树是一一对应的关系
 
 
且上下关系中不允许2个连续的红边
 
由特点3可以推出LLRB的一个特性,红黑树与2-3-4树一一对应。
 

2、插入算法

同样地,在LLRB中查找操作同BST。
在插入之前要知道一个操作:旋转。它有两种情况:左旋,右旋。
 
 
左旋 右旋
 
插入算法思路:即前面介绍的2-3-4树
具体实现时,插入一个结点时,始终是红结点,即用红边链接该结点。对于2-node、3-node直接插入(k-node有k个插入点),如违反上面的左红链接和连续的红链接,则旋转作调整。对于4-node(左右都为红链接),先分裂,物理实现是一个翻转(左右红链接变黑,父链接变红)。
2-node插入的两种case
 
 
3-node插入的三种case
 
 
 
4-node分裂操作
 
 
由4-node的分裂可知黑高度不变,分裂操作即翻转在图片上对应为红链接向上传递。
在介绍2-3-4树时,4-node分裂操作有两种情况,4-node的parent是2-node和3-node;再结合k-node有k个插入点,则总共有6种情况。
4-node的分裂case 1
 
 
 
4-node的分裂case 2
 
 
看了上面两幅图后,也许会让人觉得红黑树太复杂了,这么多case,其实不然,在LLRB实现中只有两种操作:旋转、翻转。旋转的目的是保持平衡,翻转的目的是分裂4-node。
看了下面的LLRB插入算法,你就会明白上面4-node的翻转、旋转其实是分开的两个过程(翻转自上而下,旋转自下而上),只是为了统一这个完整的过程而画在了一起,才会有那么多case。
 
LLRB的插入算法:
首先结合2-3-4树的插入算法思路,先从上至下查找(遇到4-node则翻转),然后在底部叶子结点插入,因为在从上至下的过程中,可能会产生不满足LLRB的性质的情况,故插入结点后需要从下至上调整以恢复LLRB性质。
下图是插入算法的核心代码,第2是分裂即翻转,第1是插入操作,第3、4是调整。
 
从插入算法可以看出,如果自下而上再分裂4-node,则会出现它的parent也可能是4-node,祖父结点也可能是4-node;我们可以一直向上分裂,这也正是上面提到的自上而下的思路;而更简单的方法是,在沿树向下的过程中,遇到4-node就分裂,这也正是自上而下与自下而上的区别。
插入算法的核心代码
 
上图的核心代码按照从上而下和从下而上的顺序放入BST的插入(递归版本)操作中即得到下图的完整的插入算法。
注:分裂(即翻转)是自上而下,所以放在递归之前;调整(即旋转)是自上而下,所以放在递归之后。
完整的插入代码
 
 
如果将分裂操作放到递归之后,也就是先自上而下查找,插入结点,然后自下而上调整也可同样完成插入操作而不破坏LLRB的性质。
2-3树的插入操作
 
其实上述描述的就是2-3树的插入操作,它与2-3-4树的插入的区别在于:2-3树先插入,再分裂(down)、调整(up);2-3-4树先分裂(down),再插入、调整(up)。又因为插入总是在最后一层进行,故翻转的位置决定了对应树的实现。
这也是为什么2-3-4树叫top-down,而2-3树叫bottom-up。
 

3、删除算法

LLRB的删除类似于插入,只不过处理刚好相反。插入、删除都有临界点:插入4-node,删除2-node,对临界点的操作都会引起突变,因为它们会破坏LLRB的性质(黑高度)。
所以同插入一样,先从上至下查找,如果查找在3-node或4-node结束,则直接删除;
3-node和4-node的删除
 
对于2-node的删除同4-node的插入相反,2-node的删除是先合并2个2-node为1个4-node,然后再安全地删除对应的2-node中的key。
同样地,因为parent不为2-node(遇到即合并),再结合兄弟结点的2、3、4-node,则删除总共有6种情况。同样,实际的删除实现也
没这么复杂。
2-node的删除
 
在介绍删除任意一个结点时,先分析删除树中最小的结点。因为它是删除任意结点的一部分,后面可以看出来。
首先,为了保证可以直接删除最小的某个结点,需要假设当前结点h或者h.left是红色链。
然后从上而下查找过程中,2个2-node要变为1个4-node,则需反向翻转(红色父链接变黑,黑色子链接变红),
为了将红链从上向左子树传递(删除红结点,不改变黑高度),需保证h为红,h.left和h.left.left为黑;
当h.left和h.left.left都为黑时,
如果h.right.left为红,则要从右边借兄弟(下图case 2),如果h.right.left为黑,则不需要(下图case1)。
注:在翻转的同时,右子树可能会产生连续的红链,则需调整。
case 1
 
 
 
case 2
 
 
 
红链向左移动 红链向左移动对应的example
 
deleteMin的实现
 
 
 
deleteMin的example
 
 
完成了deleteMin就完成了LLRB的删除操作的一大半。现在是删除LLRB的任意一个key,
自上而下查找过程中,左边查找用moveRedLeft;右边查找用moveRedRight;直到最后的底部叶子结点,直接删除即可;同样,自下而上调整。
 
怎样将delete操作归约到delteMin去呢?算法导论提供的一个技巧是:replace,deleteMin(即用后继的key代替当前的key,再删除右孩子的最小结点)。
删除技巧
 
 
 
完整删除代码
 
 
 
参考:
《算法导论》
《algorithm in c》

[Dr.Lib]Note:Algorithms – 四边形不等式

若w[a,c]+w[b,d] \(\leq\) w[b,c]+w[a,d] (a<b<c<d)

则w满足凸四边形不等式

若w[i,j] \(\leq\) w[i,j] ([i,j]\(\subseteq\) [i',j'])

则w满足区间单调

2D/1D:d[i,j]=opt{d[i,k-1]+d[k,j]}+w[i,j] (1 \(\leq\) i < k \(\leq\) j \(\leq\) n)

d满足凸四边形不等式 k[i,j-1] \(\leq\) k[i,j] \(\leq\) k[i+1,j]

w为凸 \(\Leftrightarrow\) w[i,j]+w[i+1,j+1] \(\leq\) w[i+1,j]+w[i,j+1]

1D/1D:f[i]=opt{f[j]+w[j,i]} (b[i] \(\leq\) j \(\leq\) i-1)

满足决策单调性,可用单调队列优化决策

 

[Dr.Lib]Note:Coding – 日常模板 Manual Ver-0.1

Via http://www.shuizilong.com/house/archives/%E6%97%A5%E5%B8%B8%E6%A8%A1%E6%9D%BF-manual-ver-0-1/

Orz岛娘……

 

[Dr.Lib]Note:Algorithms – GarsiaWachs

Via FHQ神犇http://fanhq666.blog.163.com/blog/static/81943426201062865551410/

石子合并(每次合并相邻的两堆石子,代价为这两堆石子的重量和,把一排石子合并为一堆,求最小代价)是一个经典的问题。dp可以做到O(n*n)的时间复杂度,方法是:

朴素DP做法

设f[i,j]为合并从i到j的石子所用最小代价。

f[i,j]=min(sum(i,j)+f[i,k]+f[k+1,j])对所有i<=k<j,其中sum(i,j)表示从i到j的石子重量之和。

设上式取等时k的值为w[i,j],有神牛证明过:w[i,j]>=w[i,j-1],w[i,j]<=w[i+1,j]

这样,枚举k的时候,就有了一个上下界,从而搞掉了一维。

GarsiaWachs算法

而GarsiaWachs算法可以把时间复杂度压缩到O(nlogn)。

具体的算法及证明可以参见《The Art of Computer Programming》第3卷6.2.2节Algorithm G和Lemma W,Lemma X,Lemma Y,Lemma Z。

只能说一个概要吧:

设一个序列是A[0..n-1],每次寻找最小的一个满足A[k-1]<=A[k+1]的k,(方便起见设A[-1]和A[n]等于正无穷大)
那么我们就把A[k]与A[k-1]合并,之后找最大的一个满足A[j]>A[k]+A[k-1]的j,把合并后的值A[k]+A[k-1]插入A[j]的后面。

有定理保证,如此操作后问题的答案不会改变。

举个例子:

186 64 35 32 103

因为35<103,所以最小的k是3,我们先把35和32删除,得到他们的和67,并向前寻找一个第一个超过67的数,把67插入到他后面

186 64(k=3,A[3]与A[2]都被删除了) 103

186 67(遇到了从右向左第一个比67大的数,我们把67插入到他后面) 64 103

186 67 64 103 (有定理保证这个序列的答案加上67就等于原序列的答案)

现在由5个数变为4个数了,继续!

186 (k=2,67和64被删除了)103

186 131(就插入在这里) 103

186 131 103

现在k=2(别忘了,设A[-1]和A[n]等于正无穷大)

234 186

420

最后的答案呢?就是各次合并的重量之和呗。420+234+131+67=852,哈哈,算对了。

证明嘛,基本思想是通过树的最优性得到一个节点间深度的约束,之后 证明操作一次之后的解可以和原来的解一一对应,并保证节点移动之后他所在的 深度不会改变。详见TAOCP。

实现

具体实现这个算法需要一点技巧,精髓在于不停快速寻找最小的k,即维护一个“2-递减序列”

朴素的实现的时间复杂度是O(n*n),但可以用一个平衡树来优化(好熟悉的优化方法),使得最终复杂度为O(nlogn)

 

[Dr.Lib]Note:Algorithms – Segment Tree 线段树

Via http://en.wikipedia.org/wiki/Segment_tree

In computer science, a segment tree is a tree data structure for storing intervals, or segments. It allows querying which of the stored segments contain a given point. It is, in principle, a static structure; that is, its content cannot be modified once the structure is built. A similar data structure is the interval tree.

A segment tree for a set I of n intervals uses O(n log n) storage and can be built in O(n log n) time. Segment trees support searching for all the intervals that contain a query point in O(log n + k), k being the number of retrieved intervals or segments.

Applications of the segment tree are in the areas of computational geometry, and geographic information systems.

The segment tree can be generalized to higher dimension spaces as well.

 

 

Graphic example of the structure of the segment tree. This instance is built for the segments shown at the bottom.
 

继续阅读