跳表:一种平衡树的概率性替代品
来源:互联网 发布:python运维实战 pdf 编辑:程序博客网 时间:2024/05/29 23:22
跳表是一种可以替代平衡树的数据结构。跳表追求的是概率性平衡,而不是严格平衡。因此,跟平衡二叉树相比,跳表的插入和删除操作要简单得多,执行也更快。
二叉树可以用来实现字典和有序表等抽象数据结构。在元素随机插入的场景,二叉树可以很好应对。然而,在有序插入的情况下,二叉树就退化了(链表),性能非常差。如果有办法对待插入元素进行随机排列,二叉树大概率可以运行良好。大部分情况下,插入是在线进行的,因此随机排列并不具有可行性。平衡树在操作时对树结构进行调整以满足平衡条件,因此获得理想性能。
跳表是一种概率性可行的平衡二叉树替代数据结构。跳表通过一个随机数生成器实现平衡。虽然跳表最坏情况下(worst-case
)性能也很差,但是没有任何输入序列必然会导致最坏情况发生(这点类似划分元素(pivot point
)随机选定的快排)。跳表极度不平衡发生的概率非常低(一个包含250
个元素的字典,一次查找需要花3
倍期望时间的概率小于百万分之一)。跳表平衡概率跟随机插入的二叉树差不多,好处是插入顺序不要求随机。
实现概率性平衡比严格控制平衡要简单得多。对很多应用来说,跳表用起来比平衡树更自然,而且算法更简单。跳表算法简单性意味着更容易实现,而且与平衡树和自适应树相比有常数倍数的性能提升。跳表在空间上也比较高效。平均每个元素只需要额外耗费个2指针(甚至可以配置得更低),并不需要在每个节点上都存与平衡和优先级相关的数据。
结构
搜索一个链表时,我们需要遍历每个节点(如图 1a)。如果列表是有序的,偶数节点另存一个指向下一个偶数节点的指针(如图 1b),我们只需要检查最多(n/2)+1
个节点(n
是链表规模)。如果序号为4
的倍数的节点都有一个往前跳4
步的节点,那么最多只需要检查(n/4)+2
次。如果,序号为2^i
的节点有一个向前跳2^i
步的指针,那么则需要检查log2 n
次了!这种数据结构可以用来做快速搜索,但是插入和删除并没有可行性。
有k
个前进指针的节点成为k
层节点。如果第2^i
个节点有一个向前跳2^i
步的指针,那么每层节点数满足以下关系:第1
层有50%
的节点;第2
层有25%
的节点;第3
层有12.5%
的节点;以此类推。假设每层的比例还是一样,但是节点随机选择,会怎样呢(图 1e)?节点第i
个前进指针不严格跳2^i
步,而是可以跳任意步。由于不需要维持特殊条件,插入节点层数随机生成,插入和删除只需要做局部修改。极端情况下,有些层次分布会导致极差的性能,不过接下来我们会看到这种情况非常罕见。这种数据结构在链表的基础上加上额外指针以跳过一些中间节点,因此命名为跳表。
算法
这小节介绍用于搜索、插入、删除的算法。搜索操作返回与给定键(key
)关联的值(value
),键不存在时则失败。插入操作将给定键关联到新的值,如果键不存在则插入新的节点。删除操作删除给定键。另外,类似最小键和下一键这类操作实现起来也非常简单。
每个元素由一个节点表示,层次由节点在插入时随机选定,与已有元素无关。层次为i
的节点拥有i
个前进指针,下标分别是1
至i
。节点不需要存储层数。选定一个合适的常量MaxLevel
,层数在这个范围内。跳表的层数时当前所有节点层数的最大值,或者当跳表为空是,层数为1
。用一个头向量存储从层次1
到MaxLevel
的向前指针。指针高于当前跳表层数的部分直接指向NIL
。
初始化
约定NIL
元素,其键比所有合法建都大(上限)。跳表的任意层都以NIL
结尾。新的跳表初始化成层数只有1
,并且所有表头所有前进指针都指向NIL
。
查找
查找某个元素时,需要逐层遍历所有键不超过给定键的节点。如果当前层前进节点已经不符合条件了,往下一层开始遍历。当遍历进行到第1
层时,下一个节点就是目标节点(如存在)。
Search(list, searchKey) x := list->header for i := list->level downto 1 do while x->forward[i]->key < searchKey do x = x->forward[i] x := x->forward[1] if x->key = searchKey then return x->value else return failure
插入/删除
插入或者删除节点,只需先执行搜索操作(图 3),然后视情况重新拼接。伪代码如下所示:
Insert(list, searchKey, newValue) local update[1..MaxLevel] x := list-header for i := list->level downto 1 do while x->forward[i]->key < searchKey do x := x->forward[i] update[i] := x x := x->forward[i] if x->key = searchKey then x->value := newValue else lvl := randomLevel() if lvl > list->level then for i := list->level+1 to lvl do update[i] := list->header list->level = lvl x := makeNode(lvl, searchKey, value) for i := 1 to lvl do x->forward[i] = update[i]->forward[i] update[i]->forward[i] := x
图3展示了搜索过程。注意到,搜索的过程中维护了一个名为update
的向量,在每次降层搜索时更新。搜索完成后,update
刚好记录了各层在操作位置(图中环)左边最近的节点:
如果插入时生成了一个比当前最大层更大的层数,则需要更新跳表层数并且初始化update
向量对应部分。
接下来,看看删除操作的伪代码:
Delete(list, searchKey) local update[1..MaxLevel] x := list-header for i := list->level downto 1 do while x->forward[i]->key < searchKey do x := x->forward[i] update[i] := x x := x->forward[i] if x->key < searchKey then for i := 1 to list->level do if update[i]->forward[i] != x then break update[i]->forward[i] = x->forward[i] free(x) while list->level > 1 and list->header->forward[list->level] = NIL do list->level := list->level - 1
在每次删除时,需要检查被删除节点是否是最大层节点。如果是,需要对跳表层数做对应调整。
随机函数
接下来,需啊确定一个随机数生成函数,其概率分布使得第i
层中有50%
的节点同时数据第i+1
层。先抛开具体数值,我们在讨论一个分数p
,对于有i
层指针的节点中p
部分,同时拥有i+1
层指针。以下便是一个非常理想的随机数生成函数,随机层数生成与跳表元素及规模无关:
randomLevel() lvl := 1 while random() < p and lvl < MaxLevel do lvl := lvl + 1 return lvl
- 跳表:一种平衡树的概率性替代品
- 深度理解跳跃链表:一种基于概率选择的平衡树
- 跳表(SkipList) -- 基于概率的平衡二叉树
- AVL树,最老的一种平衡查找树
- SBT(Size Balanced Tree平衡树的一种)
- AVL树(一种二叉平衡搜索树)
- 控制随机数生成概率的一种解决方法
- 控制随机数生成概率的一种解决方法
- 控制随机数生成概率的一种解决方法
- 信号调理IC:一种低成本ASIC替代品
- CVS的替代品:SVN
- [转载]JAXP的替代品
- integrit - Tripwire 的替代品
- JSON(XML的替代品)
- 寻找greenbrowser的替代品
- new()的替代品
- 寻找Matlab的替代品
- dmtracedump 的替代品
- JS判断手机端访问页面并跳转到指定页面
- 2017 上半年最酷,最受欢迎的30 个 Android 库
- mysql导入sql文件乱码问题
- webpack2配置心路历程
- 2017-05-31 每日一记 Linux驱动命令
- 跳表:一种平衡树的概率性替代品
- 深入理解Feign之源码解析
- 深入理解阻塞队列(四)——LinkedBlockingDeque源码分析
- dBm与功率(w)
- TCP 协议简介
- JSP应用开发详解-------投票程序-案例代码运行修复
- 游戏IP价值的量化分析
- 线程通讯,多线程
- C11的标准特性