数据结构(八)高级搜索树

AVL树是典型的适度平衡的二叉搜索树,为每个节点定义引入平衡因子的指标,平衡银子绝对值小于等于1,虽然和理想平衡相比,已经放松了限制,但条件仍显苛刻,还要在动态调整中保持这种特性。

一、伸展树

局部性

Locality:刚被访问的数据,极有可能很快地再次被访问,这一现象在信息处理过程中屡见不鲜。

BST就是这样的一个例子

BST:刚刚被访问过的节点,极有可能很快地再次被访问,下一将要访问的节点,极有可能就在刚被访问过节点的附近。

 

 

 

 利用局部性,能否更快?

列表

 

 

 将访问的元素放到列表前端

BST的顶部元素访问的效率更高

将经常访问的元素移送到更加靠近树根的位置,即降低深度

 

逐层伸展

节点v一旦被访问,随机被转移到树根

 访问333

 

 

 

 

 

 

 

 

  

 

 

 

 

 

 经过若干次zigzag的组合,最终到达了树根,对333的访问几乎只需要常数时间

该节点上升的过程是左右摇摆不断伸展的过程

一步一步往上爬

自上而下,逐层单旋

直到v最终被推送至树根

最坏的情况下效率不佳

 

双层伸展

 

 

 

 

 构思的精髓:向上追溯两层,而非一层

反复考察祖孙三代:g=parent(p), p=parent(v), v

根据它们的相对位置,经两次旋转使得v上升两层,成为(子)树根

 

zig-zag/zag-zig

 

 与AVL树双旋完全等效

与逐层伸展别无二致

 

zig-zig/zag-zag

 

 

 对祖父g进行zig

 

 

 

 

 

 对祖父旋转

 

 

 对父亲旋转

 

 

 再对新祖父005zig旋转

 

 

 对父亲004zig旋转

继续上面的过程

最终

 

 

 树的高度变为原来的一半

继续访问最低点,会继续优化调整

树的高度又缩减了一半

 

 

 具有路径折叠效果

折叠效果:一旦访问坏节点,对应路径的长度将随即减半

最坏情况不致持续发生

单趟伸展操作,分摊O(logn)时间

 

 

 实现

 

 

 

 

 

 zig-zig情况

 

 

 查找算法

 

 

 伸展树的查找操作,与常规BST::search()不同,很可能会改变树的拓扑结构,不再属于静态操作

 

插入算法

直观方法:调用BST标准的插入算法,再将新节点伸展至根,其中,首先需要调用BST::search()

重写后的splay::search()已集成了splay()操作,查找(失败)之后,_hot即是根节点

 

 

 首先调用重写之后的search接口,不失一般性,查找是失败的,记失败之前最终的节点为t,即此前所说的_hot

集成在search接口内部的splay操作,自然会将_hot推送到树根的位置

在逻辑上将t这棵树一分为二,比如将t与其右子树分离开

引入节点v,并将t及其后代作为左子树,原先从t分离出的右子树作为树的右子树重写接入树中、

 

删除算法

直观方法:调用BST标准的删除算法,再将_hot伸展至根

同样的,Splay::search()查找(成功)之后,目标节点即是树根

既然如此,何不随即就在树根附近完成目标节点的摘除

首先进行查找,对待删除的节点进行定位,定位成功v

紧随其后,集成在search接口内的伸展操作之后,待删除节点v被推送到树根

将该节点释放,从逻辑上,左右节点彼此分离

 有很多方法重新合并它们,比如

在右子树中找到最小的节点,相对于左子树则是最大的

将原来的左子树作为左子树连接上去即可

 

二、B-树

严格讲,B-树并发BST,在物理上B-树的每个节点都可能包含多个分支,但逻辑上等价BST。

 

1.动机

高效I/O

 

 越来越小的内存

 

事实上,系统存储容量的增长速度,远远小于问题规模的增长速度。

 

 

 

 为什么不把内存做的更大?

物理上,存储器的容量越大/小,访问速度就越慢/快

 

高速缓存

事实1:不同容量的存储器,访问速度差异悬殊

 

 为避免1次外存访问,宁愿访问内存10次、100次,甚至更多

多数存储系统,都是分级组织的---Caching

最常用的数据尽可能放在更高层、更小的存储器中,实在找不到,才向更低层、更大的存储器索取

 

 在该系统中,相对于任何一个存储级别,如果希望向更低的存储级别写入,或反过来从更低的存储级别读入数据,都称为输入和输出,简称I/O。

对于更上层的存储级别而言,对更低层的访问都可以叫做外存访问。

应尽可能减少I/O

 

事实2:从磁盘中读写1B,与读写1KB几乎一样快

批量式访问:以页(page)或快(block)为单位,使用缓冲区 // <stdio.h>...

 

 

B-Tree

 

 

 

所谓m阶B-树,即m路平衡搜索树(m>=2)

外部节点的深度统一相等,等效于,所有叶节点的深度统一相等

B-Tree的外部节点是叶节点数值为空,其实并不存在的孩子

与通常的BST不同,B-Tree的高度是相对于外部节点,而非叶子节点而言的

 

内部节点各有

不超过m-1个关键码:

不超过m个分支:

 

 以n表示节点中所含的关键码数,因此拥有n个关键码的节点对应于n+1个分支

 

内部节点的分支数n+1也不能太少,具体的

树根:2<=n+1

其余:

 

 故亦称作

 

 m=5,称作(3, 5)-树

m=6,称作(3, 6)-树

(2, 4)树

 

紧凑表示

 

BTNode

3.B-树:查找

 B树中容纳的词条极多,甚至不能完全容纳在内存中,相对的只能放在速度更慢的外存之中

B树查找的诀窍在于只载入必须的节点,尽可能减少I/O操作

对于一棵处于活跃状态的B树而言,不妨假设根节点已经常驻于内存

假设需要查找特定的关键字key

首先会在常驻于内存的根节点进行查找

每个节点中的关键码都已经存成了一个向量,因此实施的无非是一个顺序查找,如果能在某个位置命中,查找随即以成功告终

查找失败于特定位置,在特定位置应该预先已经记录了引用,该引用将会指向B树中的下一层的某一个节点,可以沿着该引用找到下层的节点,并将其载入到内存之中

查找深入一层,代价是做了一次读入性的IO操作

既然已经搜索到这样一个节点,就可以断定,如果目标关键码的确存在这棵树中,就必然存在于这个节点所对应的子树中

 

 继续在新载入的节点中进行查找,只需顺序查找

 

 同样,如果查找以失败告终,此时,也会停止于某个位置,该位置预先记录了引用,可以据此找到B树的下一个节点

同样可以断定,如果目标关键码的确存在这棵B树中,就必然存在于这个节点所对应的子树中

因此,再次IO,将下层节点载入内存,在新载入的节点中做顺序查找

 

 反复上述步骤,可能最终会到达叶节点

 

 依然需要对目标关键码进行顺序查找,失败,会根据引用查找到下一层的节点,即外部节点

 

 此时,会宣告查找以失败告终

 

当然,还有另一种情况,外部引用实际上指向的是相对而言更低层此的B树,借助外部节点可以将存放于不同级别上的B树串接起来,构成更大的B树

 

假设查找确实以失败告终

所谓B树查找不过是一系列在内存中顺序查找和一系列IO操作相间隔组成的操作序列

 

5阶B树,(3, 5)树

 

 查找69,先查找内存中的根节点

 

 将下层的节点读入内存

 

 在其中进行顺序查找,找到69

 

实现

 

 

复杂度

 

最大树高

含N个关键码的m阶B-树,最大高度=

 

 

 

 

最小树高

 

 

 

 

分裂

设上溢节点中的关键码依次为k0,...,km-1

 

 关键码ks上升一层,并分裂split:以所得的两个节点作为左、右孩子

 

 

再分裂

 

 

 

插入555

 

 插入到紧邻556的左侧

 

 顺利结束

插入444

插到435的右侧

 

 发生上溢

修复上溢,以中位数关键码为界,一分为二

中位数关键码提升一层,纳入到父节点的适当位置

 

插入500

 

 插入到482右侧

上溢

修复

 

 仍上溢

再修复

 

 再分裂

 

 树长高了一层

5.B-树:删除

算法

 

 

旋转

 

 

删除249

 

 发生下溢

左顾右盼,有兄弟有四个关键码,可以借出一个

先向父亲借268,右兄弟给父亲315用来填补

 

 删除619

 

 发生了下溢

左顾右盼,没有左兄弟,右兄弟没有足够的关键码

无法旋转,只能合并

从父节点中找到介于该节点和右兄弟的关键码,也就是703

取出下溢,作为粘合剂将该节点和右兄弟合并

但是父亲发生了下溢

在父亲处左顾右盼,没有右兄弟,而左兄弟处于下溢的临界状态,无法借出关键码

合并

从父节点也就是树根中,找到下溢节点和兄弟之间的关键码

根节点只有一个关键码528

将该关键码取出并下溢,作为粘合剂将下溢节点和兄弟结合起来

包含唯一节点的根节点成为空的,删除,用新的节点做根节点即可

B树高度降低了一层

 

三、红黑树

1.动机

 

 

 

 这些结构每当经过一次动态操作,使得其中的逻辑结果发生变化后,会随机完全转入新的状态,同时将此前的状态完全遗忘,因此称作ephemeral structure

 

 除了目标关键码,还要同时指定版本号ver

蛮力实现:每个版本独立保存,个版本入口自成一个搜索结构

 

空间上不可接受 

利用相邻版本之间的关联性

 

O(1)重构

大量共享,少量更新:每个版本的新增复杂度,仅为O(logn)

为此,就树形结构的拓扑而言,相邻版本之间的差异不能超过O(1)

AVL树的删除操作不满足

需要一种BST:

 

 红黑树是具有该特性的变种

 

 

 

 由红、黑两类节点组成的BST // 亦可给边染色

(统一增设外部节点NULL,使之成为真二叉树)

(1)树根:必为黑色

(2)外部节点:均为黑色

(3)其余节点:若为红,则只能有黑孩子 // 红之子、之父必黑

(4)外部节点到根:途中黑节点数目相等 // 黑深度

lifting变换

变换之前

变换之后

 

 (2, 4)树==红黑树

提升各红节点,使之于其(黑)父亲等高--于是每棵红黑树,都对应于一棵(2, 4)-树

将黑节点与其红孩子视作(关键码并合并为)超级节点...

无法四种组合,分别对应于4阶B-树的一类内部节点 //反过来呢?

 

 

接口

 

 

 3.红黑树:插入

 

 

 

 

 

 

 

提升变换,所有指向红色节点的虚边收缩起来,从B树来看,局部的四个节点,合并为包含四个关键码的超级节点,对应于五个分支,非法的

红黑树中修复双红缺陷,不如说是在B树中修复上溢缺陷

b -> b' -> c' -> c

 

 g左右至少有一个黑色的关键码,也可能有红色,导致双红缺陷,根据上述方法再进行解决,问题所发生的位置会逐渐上升

 

双红修正:复杂度

重构、染色均属常数时间的局部操作,故只需统计其总次数

修正算法流程图

 

 

 

 

4.红黑树:删除

 

 

 

 

 

 如此,红黑树性质在全局得以恢复--删除完成 //zig-zag等类似

在对应的B树中,以上操作等效于...

 

 

 

 

 

 

 

 

 

 

 

 复杂度

 

posted on 2019-09-24 16:54  AI数据  阅读(866)  评论(0编辑  收藏  举报

导航