目录
- 0.树
0.1 一般树的定义
0.2 二叉树的定义 - 1.查找树ADT
- 2.查找树的实现
2.1 二叉查找树
2.2 AVL树
2.3 伸展树
2.3-1 自顶向下伸展树
2.4 B树
2.4-1 B+树
2.4-2 B*树
2.5 红黑树
2.6 跳跃表
2.7 trie树
0 树
0.1 一般树的定义
为什么引入树?
因为表的线性访问时间太慢。
1)定义
递归的方式定义:
一棵树是一些结点的集合。这个集合可以是空;若非空,则一棵树由称作根的结点r以及0个或多个非空的(子)树T1,T2,...,Tk组成,这些子树中每一棵的根都被来自根r的一条有向的边连接。
父亲、儿子:每一棵子树的根叫做根r的儿子,而r是每棵子树的根的父亲。
树叶:没有儿子的结点称为树叶;
兄弟:具有相同父节点为兄弟(silbing);
路径:从结点n1到nk的路径定义为结点n1, n2, ..., nk的一个序列,使得1 <= i < k,结点ni是ni+1的路径。一棵树中从根到每个结点恰好存在一条路径。
路径长:该路径上边的条数,n1到nk位k-1.
深度:对任意结点ni,ni的深度为从根到ni的唯一路径的长
高:是从ni到一片树叶的最长路径的长
祖先、后裔:如果存在从n1到n2的一条路径,那么n1是n2的一位祖先,而n2是n1的后裔;如果n1不等于n2,则各为真祖先、真后裔。
2)实现
树的结点声明:
一棵树的表示和实现:
3)树的遍历及应用
a.先序遍历:对结点的处理工作是在它的诸儿子结点被处理前进行的。
b.中序遍历:对结点的处理夹在左右儿子处理的中间
c.后续遍历:在后续遍历中,一个结点的处理工作是在处理完它的诸儿子结点后进行的
d.层序遍历:所有深度为D的结点要在深度D+1的结点前进行处理。层序遍历与其他类型遍历不同的地方在于它不是递归实施的,它用到队列,而不使用递归所默认的栈。
0.2 二叉树的定义
二叉树的每个结点都不能有多于两个儿子。
二叉树结点的声明:
二叉树在编译器领域的应用:
1)中序遍历:递归产生一个带括号的左表达式,然后打印出根处的运算符,再递归地产生一个带括号的右表达式,得到一个中缀表达式
2)后序遍历:递归打印出左子树、右子树,然后打印运算符(后缀表达式)
3)前序遍历:先打印出运算符,然后递归打印出左子树和右子树
将后缀表达式转变成表达式树:
step1.一次一个符号地读入表达式
step2.如果符号是操作数,那么就建立一个单节点树并将一个指向它的指针推入栈中;如果符号是操作符,那么我们就从栈中弹出指向两棵树T1和T2的那两个指针(T1先弹出)并形成一棵新的树。
a b + c d e + * *后缀表达式转换成表达式树的过程:
1 查找树ADT
支持SEARCH(Find)、MINIMUM(FindMin)、MAXIMUM(Find Max)、PREDECESSOR、SUCCESSOR、INSERT和DELETE等集合操作。
查找树既可以作为一个字典(字典的实现,作为一个专题?)又可以作为一个优先队列。
2 查找树的实现
2.1 二叉查找树
1)结构
一棵二叉查找树是以一棵二叉树来组织的。
中序遍历:树根位于左子树和右子树之间
先序遍历:树根位于左右子树之前
后序遍历:树根位于左右子树之后
2)操作
a.查找
b.最值
c.前驱和后继
证明:考虑一棵二叉搜索树T,其关键字互不相同。如果T中的一个结点x的右子树为空,且x有一个后继y,那么y一定是x的最底层祖先,并且其左孩子也是x的祖先。(每个结点都是它自己的祖先。)
如上图:
4的后继是6
9的后继是13
13的后继是15
step1.从结点x向上搜索,从右边一路向上,这些结点的key都要小于x
step2.直到找到一个结点,从左边向上,这是第一个大于所有左边结点的结点y
这里的关键是:结点是x是结点y的左子树最大者,因此x的后继是y
d.插入
e.删除
删除分为如下三种情况:
另外一种划分:
e-1.如果z没有左孩子,那么用其右孩子来代替z,这个右孩子可以是NIL,也可以不是。
e-2.如果z仅有一个孩子且是其左孩子,用左孩子来代替z
e-3.否则,z既有一个左孩子又有一个右孩子。我们要查找z的后继y,这个后继位于z的右子树中并且没有左孩子。现在需要将y移出原来的位置进行拼接,并替换树中的z
如果y是z的右孩子,那么用y替换z,并仅留下y的右孩子。
否则,y位于z的右子树中但并不是z的右孩子。此时先用y的右孩子替换y,然后再用y替换z。
这里涉及到一个证明:
如果一棵二叉搜索树中的一个结点有两个孩子,那么它的后继没有左孩子,它的前驱没有右孩子。
因为:结点有两个孩子,那么它的后继一定在右子树中,沿着左路径一直向下;如果这个后继还有左孩子,那么后继是这个孩子。
前驱同理。
TRANSPLANT用一棵以v为根的子树来替换一棵以u为根的子树:
3)二叉搜索树的平均情形性能——随机构建二叉搜索树
当一棵二叉搜索树同时由插入和删除操作生成时,我们对这棵树的平均高度了解甚少。
当树由插入操作单独生成时,分析就会容易得多。
定义n个关键字的一棵随机构建二叉搜索树为按随机次序插入这些关键字到一棵初始的空树中而生成的,这里的n!个排列中的每个都是等可能地出现。
证明:说明含有n个关键字的随机选择二叉搜索树的概念,这里每一棵n个结点的二叉搜索树是等可能地被选择,不同于本节中给出的随机构建二叉搜索树的概念。
从上图中可以看出,一棵二叉搜索树可能对应多个排列。因此,不同于本节中给出的随机构建二叉搜索树的概念。
证明:一棵有n个不同关键字的随机构建二叉搜索树的期望高度为O(lgn)。
该证明中涉及到的证明:
1)组合数的证明
2)证明:f(x) = 2的x次方是凸函数。
3)关于e的x次幂 >= 1+x
4)关于Jensen不等式(是凸函数定义的推广)
2.2 AVL树
1)结构
AVL(Adelson-Velskii和Landis)树是带有平衡条件的二叉查找树。
这个平衡条件必须容易保持,而且它必须保证树的深度是O(logn)。
a.最简单的方法是:要求左右子树具有相同的高度。(太松)
b.要求每个结点都必须要有相同高度的左子树和右子树。(太严)
一棵AVL树是其每个结点的左子树和右子树的高度最多差1的二叉查找树。(空树的高度定义为-1)
在高度为h的AVL树中,最少节点数S(h)由S(h) = S(h-1) + S(h-2) + 1给出(g(h) = S(h) + 1)。
对于h = 0, S(h) = 1;h = 1, S(h) = 2。函数S(h)与斐波那契数密切相关。
由此可以推出:n >= S(h),可以得到h的高度最高不超过1.44log(n + 2) - 1.328,也即O(lgn)。
2)操作
重点研究插入:
将关键字X的一个新结点插入到一棵AVL树T中,递归地将X插入到T的相应的子树(Tlr)。
如果Tlr的高度不变,那么插入完成。
否则,如果在T中出现高度不平衡,那么我们根据X以及T和Tlr中的关键做适当的单旋转或双旋转,更新这些高度(并做好与树的其余部分的连接),从而完成插入。
插入后,只有那些从插入点到根节点的路径上的结点的平衡可能被改变,因为只有这些结点的子树发生变换。
对第一个破坏了AVL条件的结点(即最深的结点)进行平衡,这一重新平衡保证整个树满足AVL特性。
该结点为α,由于任意结点最多有两个儿子,因此高度不平衡时,α点两棵子树的高度差2.这种不平衡可能出现下面四种情况:
- 情况1:对α的左儿子的左子树进行一次插入
- 情况2:对α的左儿子的右子树进行一次插入
- 情况3:对α的右儿子的左子树进行一次插入
- 情况4:对α的右儿子的右子树进行一次插入
情况1和情况4关于α点镜像对称,2和3关于α点镜像堆成。
2-1)情况1和情况4——单旋转
针对情况1:
插入之前k2满足AVL特性,插入后结点k2不满足AVL平衡特性,因为它的左子树(X已经长出一层)比右子树深2层(看虚线)。因此,有
a.Y不可能与新X在同一水平上,因为那样k2在插入之前就不平衡了
b.Y也不可能和Z在同一层,因为那样k1就是平衡被破坏的第一个结点
为了恢复平衡,把X上移一层,并把Z下移一层。调整后,整个树的新高度与插入前原树的高度相同。
情况4类似:
2-2)情况2和情况3——双旋转
针对情况2:
恰好B或C中有一棵比D深两层,但是不确定是哪一棵,因此画成比D低1.5层。
针对情况3,类似:
2.3 伸展树
2.3.1 结构
保证从空树开始任意连续M次对数的操作最多花费O(MlogN)。虽然这种保证不排除任意一次操作花费O(N)时间。一棵伸展树每次操作的摊还代价是O(logN)。
伸展树是基于这样的事实:对于二叉查找树,每次操作最坏情形时间O(N)并不坏,只要它相对不常发生即可。
因此:当一个结点被访问后,它就要经过一系列AVL树的旋转被放到根上。(因为许多应用中,当一个结点被访问,它就很可能不久再次被访问到,如果碰到O(N)的结点,没有被移动,那么很难得到O(lgN)摊还时间)
伸展树不要求保留高度或平衡信息。
2.3.2 操作
1)一个简单的想法——执行单旋转
如下是对树中k1进行一次访问后发生的情况:
k1和k2之间的单旋转:
k1和k3之间的单旋转:
k1和k4之间的单旋转:
k1和k5之间的单旋转:
这个选择效果是不够好的:将k1推向树根,却将k3几乎推向和k1之前一样的深度。
2)展开
X是访问路径上的一个非根结点。
a. X的父节点是树根,只要旋转X和树根
b. X有父亲P和祖父G
b-1. zig-zag情形
b-2. zig-zig情形
考虑之前的例子:
考虑下面这个例子:
(是否可以证明?)展开操作的效果:不仅将访问的结点移到根处,而且还把访问路径上的大部分结点的深度大致减少一半的效果(某些浅的结点最多向下推后两个层次)
3)删除
step1.访问该节点(移到根处)
step2.删除根节点(得到Tl和Tr)
step3.将Tl中的最大元素作为根,并且将Tr接到根的右边,作为右儿子
2.3-1 自顶向下伸展树
基本的伸展树展开操作:
直接实现需要从根沿树往下的一次遍历,以及而后的从底向上的一次遍历。
通过保存一些父指针来完成,或者通过将访问路径存储到一个栈中来完成。开销较大。
自顶向下伸展树的改进:
自顶向下展开在初始访问路径上施行一些旋转,结果得到在实践中更快的过程,只用O(1)的额外空间,但却保持了O(logN)的摊还时间界。
step1.自顶向下展开旋转
这个伸展过程不能处理在伸展树中不存在的元素,正确的做法如下:
自顶向下的展开过程为什么是正确的?
- 1.对于左边:总是将X放到左边,然后将X = X->Right;因此,左边的总是小于中间的X
- 2.对于右边,同理,总是将X放到右边,然后将X = X->Left,因此,右边总是大于中间的X
- 3.最后合并的时候:
LeftTreeMax->Right(左边最大) = X->Left(中间最小)
RightTreeMin->Left(右边最小) = X->Right(中间最大)
剩下的X大于左边最大,小于右边最小,正好居中 - 而对于查找,跟普通查找的本质是一样的,小于根则往根的左边走,大于根则往根的右边走
证明:自顶向下的展开过程的摊还界为O(lgN)
step2.最后一步:处理L、R和中间树以形成一棵树
示例如下
插入
该伸展树插入19:
- step1.先按19自顶向下展开
-
step2.然后将19插入
删除
该伸展树删除18:
- step1.先按18自顶向下展开
- step2.删除18,然后将18的左子树按18自顶向下展开(因为左子树都小于18,所以最后的根上面没有右子树)
-
step3.将根的右子树赋值为18的右子树
2.4 B树
B树是为磁盘或其他直接存取的辅助存储设备而设计的一种平衡搜索树。B树类似于红黑树。但它们在降低磁盘I/O操作数方面要好些。许多数据库系统使用B树或者B树的变种来存储信息。
用以下伪代码对磁盘操作进行建模:
在大多数系统中,一个B树算法的运行时间主要由它所执行的DISK-READ和DISK-WRITE操作的次数决定,所以希望这些操作能够读、写尽可能多的信息。一个B树结点通常和一个完整磁盘页一样大,并且磁盘页的大小限制了一个B树可以含有的孩子的个数。
2.4.1 结构
任何和关键字相联系的“卫星数据”将于关键字一样存放在同一个结点中(指针)。
B树的结点可以有很多孩子。B树以一种自然的方式推广了二叉搜索树。
如果B树的一个内部结点x包含x.n个关键字,那么结点x就有x.n+1个子域。结点x中的关键字就是分割点,它把结点x中所处理的关键字的属性分割成x.n+1一个子域,每一个子域由x的一个孩子处理。
一棵B树的定义如下:
B树的高度:
2.4.2 操作
采用两个约定:
- B树的根节点始终在主存中,无需对根做DISK-READ操作;然而,当根节点被改动后,需要对根节点做一次DISK-WRITE操作
- 任何被当做参数的结点在传递之前,都要对它们先做一次DISK-READ操作。
1)搜索——B-TREE-SEARCH
对每个内部结点x,做的是一个(x.n+1)路的分支选择。
性能分析(两个指标):访问磁盘页面数、CPU时间
2)创建——B-TREE-CREATE
3)插入——B-TREE-INSERT
在B树中,不能简答地创建一个新的叶节点,然后将其插入,因为这样得到的树将不再是合法的B树。(为什么?因为一个非根结点至少还有t-1个关键字。)
正确的做法:
将新的关键字插入一个已经存在的叶节点上。将关键字插入满的叶节点时,会引起分裂。2t-1个关键字的满结点y,按照中间关键字分裂为两个各含t-1个关键字的结点。中间关键字被提升到y的父节点。如果y的父节点是满的,分裂会沿着树向上传播。
处理根节点r满的情况:原来的根节点被分裂,一个新的结点s(有两个孩子)成为根,对根进行分裂是增加B树高度的唯一途径。(B树高度的增加发生在顶部而不是底部)
分裂操作如下:
输入是一个非满的内部结点x(假定在主存中),和一个是x.ci(也假定在主存中)为x的满子节点的下标i。
该过程将这个子节点分裂成两个,并调整x,使之包含多出来的孩子。
1-9行创建结点z,并将y的t-1娥关键字以及相应的t个孩子给它。
10行调整y的关键字个数
11-17行将z插入为x的一个孩子,并提升y的中间关键字到x来分割y和z
将关键字k插入以非满的根节点为根的树中:
B-TREE-INSERT-NOFULL完成将关键字k插入到以非满的根节点为根的树中。在需要时沿树向下递归,在必要时通过调研B-TREE-SPLIT-CHILD来保证任何时刻它所递归处理的结点都是非满的。
3-8处理x是叶节点的情况
9-17处理非叶节点
9-11决定向x的哪个子节点递归下降
13-16 处理一个满结点
14 分裂为两个非满的子节点
15-16确定向两个孩子中哪一个下降是正确的
17 递归至一个非满结点
时间分析:
4)删除——B-TREE-DELETE
B-TREE-DELETE从以x为根的子树中删除关键字k。必须保证何时,结点x递归调用自身是,x中关键字至少为最小度数t (这个条件比B树中的最少关键字个数多1个以上)。这个条件允许在一趟下降过程中,就可以将一个关键字从书中删除,无需“向上回溯”(有一个例外)。
如果根x成为一个不含任何关键字的内部结点,那么x就要被删除,x的唯一孩子x.c1就成为树的新根,从而树的高度降低1,同时也维持了树根必须包含至少一个关键字的性质(除非树是空的)。
B-TREE-DELETE(T,k)
1 r ← root[T]
2 if n[r] = 1
3 then DISK_READ(c1[r])
4 DISK_READ(c2[r])
5 y ←c1[r]
6 z ←c2[r]
7 if n[y] = n[z] = t-1 Cases 2c or 3b
8 then B-TREE-MERGE-CHILD(r, 1, y, z)
9 root[T] ← y
10 FREE-NODE(r)
11 B-TREE-DELETE-NONONE(y, k)
12 else B-TREE-DELETE-NONONE (r, k)
13 else B-TREE-DELETE-NONONE (r, k)
考虑到根结点的特殊性,对根结点为1,并且两个子结点都是t-1的情况进行了特殊的处理:
先对两个子结点进行合并,然后把原来的根删除,把树根指向合并后的子结点y。
这样B树的高度就减少了1。这也是B树高度唯一会减少的情况。
除了这种情况以外,就直接调用子过程B-TREE-DELETE-NONONE (x, k)。
B树的结点的合并基于如下情况调用:
内结点x的第i个子结点y和第i+1个子结点z的关键字数都是t-1,
此时需要把内结点x的第i个关键字下移与y和z的合并,形成一个结点y。
2.4-1 B+树
把所有的卫星数据都存储在叶节点中,内部结点只存放关键字和孩子指针,因此最大化了内部结点的分支因子。
2.4-2 B*树
2.5 红黑树
红黑树是许多“平衡”搜索树中的一种,可以保证在最坏情况下基本动态集合操作的时间复杂度为O(lgn).
2.5.1 结构
红黑树是一颗二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是red或black。
通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长处2倍,因而近似于平衡的。
结点属性:color key left right p
可以把NIL视为二叉搜索树的叶节点的指针,而把带关键字的结点视为树的内部结点。
一棵红黑树是满足下面红黑性质的二叉搜索树:
- 1.每个结点或是红色的,或是黑色的
- 2.根节点是黑色的
- 3.每个叶节点NIL时黑色的(这条性质可去掉)
- 4.如果一个结点是红色的,则它的两个子节点都是黑色的
- 5.对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点
黑高:从某个节点x出发(不含该节点)到达一个叶节点的任意一条简单路径上的黑色结点个数称为该节点的黑高,记为bh(x)。
2.5.2 操作
1)常规操作
SEARCH、MINIMUM、MAXIMUM、SUCCESSOR、PREDECESSOR可以在红黑树上在O(lgn)时间内执行。(这些操作跟二叉搜索树一样)
2)插入
step1.插入新结点z后,哪些红黑性质不能保持?
- 1.每个结点或是红色的,或是黑色的(成立)
- 2.根节点是黑色的(z为根节点,则不成立)
- 3.每个叶节点NIL时黑色的(成立)
- 4.如果一个结点是红色的,则它的两个子节点都是黑色的(如果z的父节点为红结点,则不成立)
- 5.对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点(成立)
step2.RB-INSERT-FIXUP的1~15行中while循环的目标
三个循环不变式:
- a.结点z是红结点
- b.如果z.p是根节点,则z.p是黑结点
- c.如果有任何红黑性质被破坏,则至多只有一条被破坏,或是2,或是4.如果是性质2被破坏,则z是根节点且是红结点;如果是性质4被破坏,则z和z.p都是红结点
证明三个循环不变式是成立的,step2证明初始化和终止部分,step3证明保持部分。
初始化:
在循环的第一次迭代之前,从一颗正常的红黑树开始,并新增一个红结点z。在RB-INSERT-FIXUP调用前:
a.结点z是新增的红结点
b.若z.p是根,则z.p是黑色的
c.五条性质的成立情况
- 1.每个结点或是红色的,或是黑色的(成立)
- 2.根节点是黑色的(如果违反了2,则根节点是z,且是唯一的结点,此时没有违反性质4)
- 3.每个叶节点NIL时黑色的(成立)
- 4.如果一个结点是红色的,则它的两个子节点都是黑色的(如果违反了性质4,因为加入之前没有违反其他性质(根节点是黑色的,不会违反性质2),加入之后只可能是其父节点z.p为红色)
- 5.对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点(成立)
终止:
循环终止时因为z.p是黑色的。(如果z是根节点,那么z.p是T.nil黑色的。)因此没有违反性质4。因此唯一可能不成立的是性质2.
第16行恢复了这个性质。
因此可知,RB-INSERT-FIXUP的作用如下:
1)while循环的作用主要是修复性质4
2)最后一行16行主要是修复性质2
step3.分析while循环体中的三种情况
保持:
- a.结点z是红结点
- b.如果z.p是根节点,则z.p是黑结点
- c.如果有任何红黑性质被破坏,则至多只有一条被破坏,或是2,或是4.如果是性质2被破坏,则z是根节点且是红结点;如果是性质4被破坏,则z和z.p都是红结点
需要分析while循环中的6种情况,其中三种与另外三种是对称的。这里给出z.p是z.p.p左孩子的情况。
根据b,可知结点z.p.p存在。因为只有z.p为红色才进入一次循环,所以z.p不可能是根节点,因此z.p.p存在。
情况1:z的叔结点y是红色
此时z.p和y都是红色,z.p.p是黑色,将z.p和与都着为黑色,z.p.p着为红色,z上移两层。
z'=z.p.p在下一次循环迭代开始会保持这个循环不表示:
- a.结点z是红结点(z'=z.p.p被着为了红色)
- b.如果z.p是根节点,则z.p是黑结点(z'.p是z.p.p.p结点的颜色不会改变,若是根节点,则依然为黑色)
- c.如果有任何红黑性质被破坏,则至多只有一条被破坏,或是2,或是4.如果是性质2被破坏,则z是根节点且是红结点;如果是性质4被破坏,则z和z.p都是红结点(保持1/3/5性质成立;如果z'是根节点,且已经修正了性质4,所以只有性质2被坏;如果z'不是根节点,则性质2依然保持,此时z'为红色,z'.p颜色不变,可能为红色,因此只可能破坏性质4)
情况2:z的叔结点y是黑色的且z是一个右孩子
通过左旋将情况2变成情况3
因为z和z.p都是红色,所以该旋转对结点的黑高和性质5都无影响。
- 1.每个结点或是红色的,或是黑色的
- 2.根节点是黑色的
- 3.每个叶节点NIL时黑色的
- 4.如果一个结点是红色的,则它的两个子节点都是黑色的(如果违反了性质4,因为加入之前没有违反其他性质
- 5.对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点
情况3:z的叔结点y是黑色的且z是一个左孩子
情况改变颜色后,需要通过右旋来保持性质5.
因为情况2和情况3的z.p已经为黑色,因此不需要再执行while循环。
情况2和情况3保持循环不变式:
- a.结点z是红结点(进入3后,z的颜色不变)
- b.如果z.p是根节点,则z.p是黑结点(3将z.p着成黑色,如果是根节点,则是黑色)
- c.如果有任何红黑性质被破坏,则至多只有一条被破坏,或是2,或是4.如果是性质2被破坏,则z是根节点且是红结点;如果是性质4被破坏,则z和z.p都是红结点(性质135在这两种情况得以保持,而且这两种情况对性质4起了修复作用,不会违反性质2)
时间分析:
插入和删除可能违反红黑树性质,必须通过改变树中某些结点的颜色和指针结构来维持。其中结构的修改通过旋转来完成。
在LEFT-ROTATE的伪代码中,假设x.right ≠ T.nil且根节点的父节点为T.nil。
证明1.为什么选择将结点z着为红色,而不是黑色?
因为一定会违反性质5。因为插入之前,根到各个叶子点上的黑色结点都相等。插入黑色结点后,是一定要调整的。
而插入红色结点,有可能是满足要求的(插入到黑色结点下面)。这样就只有违反性质4或者性质2的才需要调整,需要调整的情况比较少。
3)删除
3-1)普通二叉搜索树的删除
删除分为如下三种情况:
另外一种划分:
e-1.如果z没有左孩子,那么用其右孩子来代替z,这个右孩子可以是NIL,也可以不是。
e-2.如果z仅有一个孩子且是其左孩子,用左孩子来代替z
e-3.否则,z既有一个左孩子又有一个右孩子。我们要查找z的后继y,这个后继位于z的右子树中并且没有左孩子。现在需要将y移出原来的位置进行拼接,并替换树中的z
如果y是z的右孩子,那么用y替换z,并仅留下y的右孩子。
否则,y位于z的右子树中但并不是z的右孩子。此时先用y的右孩子替换y,然后再用y替换z。
这里涉及到一个证明:
如果一棵二叉搜索树中的一个结点有两个孩子,那么它的后继没有左孩子,它的前驱没有右孩子。
因为:结点有两个孩子,那么它的后继一定在右子树中,沿着左路径一直向下;如果这个后继还有左孩子,那么后继是这个孩子。
前驱同理。
TRANSPLANT用一棵以v为根的子树来替换一棵以u为根的子树:
3-2)红黑树的删除
红黑树的删除与二叉搜索树的删除结构是一致的,只不过多了对y和x的记录,y和x有可能导致红黑树性质的破坏。
- a. 结点y:当z的子节点少于2个时,y指向z,并被删除;当z有两个子节点,将y指向z的后继,y被移动到z的位置
- b. y-original-color存储了发生改变前y的颜色。如果是黑色,那么删除或移动y会引起红黑性质的破坏
- c. 结点x会移至y的原始位置
- d. 当z是y的原始父节点(z有两个孩子,且y是z的右孩子时),使x.p指向y;其他情况,x.p总是设置指向y父节点的原始位置。
- e. 如果结点y是黑色的,就有可能已经引入了一个或多个红黑性质被破坏。如果y是红色,当y被删除或移动时,红黑性质仍然保持,原因:
1)树中的黑高没有变化
2)不存在两个相邻的红结点:y的新位置(无论是z还是z的后继)都不可能有两个新红结点(因为y占据了z的位置,并且是z的颜色,无论是z还是z的后继都成立);另外,如果y不是z的孩子,则y的原右孩子x代替y。如果y是红色,则x是黑色,所以x处也不可能是两个红结点相邻。
3)如果y是红色,就不可能是根节点,所以根节点仍然是黑色的
如果y是黑色的,则会产生三个问题:
- 若y是原来的根节点,而y的一个红色的孩子成为了新的根节点,违反性质2(此时RB-DELETE-FIXUP最后一行将x.color置位BLACK,且黑高是恢复的;其余情况都不会违反性质2)
- 若x和x.p是红色,则违反了性质4(此时RB-DELETE-FIXUP最后一行将x.color置位BLACK,恢复了性质4,且黑高是恢复的)
- 在树中移动y将导致先前包含y的任何简单路径上黑结点个数少1.因此y的任何祖先都不满足性质5.(将占有y原来位置的结点x视为还有一重额外的黑色:则x可能既不是红色,又不是黑色,违反了性质1)
因此RB-DELETE-FIXUP的1-22行主要处理性质1的情况,23行处理性质2和4的情况。
while循环的目的是将额外的黑色沿树上移,直到:
- x指向红黑结点,此时23行将x着色为(单个)黑色
- x指向根节点,此时可以简单地“移除”额外的黑色
- 执行适当的旋转和重新着色,退出循环
在while循环中,x总是指向一个具有双重黑色的非根节点。因此w不可能是T.nil,否则,从x.p至叶子w(T.nil为单黑色)的简单路径上的黑结点个数就会小于从x.p到x的简单路径上的黑色结点树(双重黑色)。
关键思想是在每种情况中,从子树的根(包括根)到每棵子树αβγδεζ之间的黑结点个数(包括x的额外黑色)并不被变换改变。
- 情况1:x的兄弟节点是红色
此时,各个子树的黑结点并未变换;且通过修改颜色和左旋转换为情况2、3、4 - 情况2:x的兄弟节点w是黑色的,而且w的两个子节点都是黑色的
将x和w上去掉一重黑色,x只有一重黑色而w为红色。并在x.p上新增一重额外的黑色,将x.p作为新的x来重复while循环。
如果是情况1进入到情况2,因为x.p是红色,则新x是红黑色,循环直接终止,将新x着为(单一)黑色。 - 情况3:x的兄弟节点w是黑色,w的左孩子是红色,w的右孩子时黑色
通过交换w和w.left的颜色,对w进行右旋,将情况3转换成了情况4 - 情况4:x的兄弟w是黑色,且w的右孩子是红色的
此种情况可以去掉x的额外的黑色(无论x.p的颜色是黑色还是红色,都成立)
时间分析:
2.6 跳跃表
参考//www.greatytc.com/p/4ea55495e365