查找——图文翔解SkipList(跳跃表)

跳跃表

跳跃列表(也称跳表)是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(logn)平均时间)。

基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表元素,因此得名。所有操作都以对数随机化的时间进行。

如下图所示,是一个即为简单的跳跃表。传统意义的单链表是一个线性结构,向有序的链表中插入一个节点需要O(n)的时间,查找操作需要O(n)的时间。如果我们使用图中所示的跳跃表,就可以大大减少减少查找所需时间。

因为我们可以先通过每个节点的最上层的指针先进行查找,这样子就能跳过大部分的节点。然后再缩减范围,对下面一层的指针进行查找,若仍未找到,缩小范围继续查找。

上面基本上就是跳跃表的思想,每一个结点不单单只包含指向下一个结点的指针,可能包含很多个指向后续结点的指针,这样就可以跳过一些不必要的结点,从而加快查找、删除等操作。对于一个链表内每一个结点包含多少个指向后续元素的指针,这个过程是通过一个随机函数生成器得到,这样子就构成了一个跳跃表。

构造

由图不难理解跳跃表的原理,可以看出,跳跃表中的一个节点是有不同数目的后继指针的。那么问题来了,这具体是如何实现的?这些节点是如何构造的

分析

我们不可能为每一种后继指针数目的节点分配一种大小类型的节点,那我们就提取共性,看这些节点有何共通之处。

这些节点可看做由两部分构成:数据域、指针域。数据域包括key-Value,指针域是后继指针的集合。

那如何在节点中保存后继指针集合呢?用一个二级指针,分配节点的时候指向动态分配的后继指针数组。这个方案似乎可行,但问题在于我们的节点也是动态分配的,这样的话,在释放节点的时候还需要先释放节点中动态分配的数组。释放操作比较繁琐。

灵光一闪!之前本博客中介绍了一种称为“零数组”的技术,也许可以帮到我们。(详情点击

零数组是gcc的扩展特性,不过在C99中,可以用类似的声明来实现。

struct Node{
    KeyType      key;
    ValueType    value;
    struct Node* forward[0]; //C99这样玩:struct Node* forward[]
}; 

动态分配节点可以这样写:

(struct Node *)malloc(sizeof(struct Node) + length*sizeof(struct Node*)); //length是后继指针数组的长度

这样的话,我们可以像访问数组那样访问forward,且释放的时候只释放动态分配的节点即可。(forward只是起一个标记的作用)

当然,还有一种更通用的技巧,和零数组的思想类似

struct Node{
    KeyType      key;
    ValueType    value;
    struct Node* forward[1];
}; 

我们在这里用符合任何C标准的定义,定义一个1个元素的数组,用来占位。然后动态分配一大块空间,我们通过对这个1元素数组的越界访问,访问到其后分配的空间

我们可以定义一个函数专门负责分配不同大小的节点:

void NewNodeWithLevel(const int& level, struct Node& node){
    //新结点空间大小
    int total_size = sizeof(struct Node) + level*sizeof(struct Node*);
    //申请空间
    node = (struct Node)malloc(total_size);
    assert(node != NULL);
}

查找

我们以查找19为例,图解查找过程。

先从最上层的跳跃区间大的层开始查找。从头结点开始,首先和23进行比较,小于23,(此时查找指针在图中“1”位置处),查找指针到下一层继续查找。

然后和9进行判断,大于9,查找指针再往前走一步和23比较,小于23,(此时查找指针在图中“2”位置处) 此时这个值肯定在9结点和23结点之间。查找指针到下一层继续查找。

然后和13进行判断,大于13,查找指针再往前走一步和23比较,小于23,(此时查找指针在图中“3”位置处)此时这个值肯定在13结点和23结点之间。查找指针到下一层继续查找。此时,我们和19进行判断,找到了。

好了,看完这个例子,你一定对跳转表的查找操作有了清晰的理解,至于代码实现也不难了。

插入

插入和删除的实现非常像相应的链表操作,除了"高层"元素必须在多个链表中插入或删除之外。

插入包含如下几个操作:1、查找到需要插入的位置 2、申请新的结点 3、调整指针。

因为找到插入点之后,新生成节点,新节点按概率出现在每层上,故需要保存所有层的后继指针。我们用一个临时数组保存所有层的插入点处的后继指针。

在寻找插入点的时候就可以完成赋值。

for(i = list->level; i >= 0; --i){
   while(x->forward[i]->key < key){
       x = x->forward[i];
   }
   update[i] = x;
} 

删除

删除操作类似于插入操作,包含如下3步:1、查找到需要删除的结点 2、删除结点  3、调整指针

同样,需要一个临时数组保存每层的指针域,原理和插入类似,不再赘述。

【关于释放跳转表】

释放表的操作比较简单,只要像单链表一样释放表即可。

【跳跃表使用概率均衡技术而不是使用强制性均衡,因此,对于插入和删除结点比传统上的平衡树算法更为简洁高效。】

分析

跳跃列表是按层建造的。底层是一个普通的有序链表。每个更高层都充当下面列表的“快速跑道”,这里在层 i 中的元素按某个固定的概率 p (通常为0.5或0.25)出现在层 i+1 中。平均起来,每个元素都在 1/(1-p) 个列表中出现,而最高层的元素(通常是在跳跃列表前端的一个特殊的头元素)在 O(log1/p n) 个列表中出现。

要查找一个目标元素,起步于头元素和顶层列表,并沿着每个链表搜索,直到到达小于或着等于目标的最后一个元素。

在每个链表中预期的查找步数显而易见是 1/p。所以查找的总体代价是 O((log1/p n) / p),当p 是常数时是 O(log n)。通过选择不同 p 值,就可以在查找代价和存储代价之间作出权衡。

跳跃列表不像某些传统平衡树数据结构那样提供绝对的最坏情况性能保证,因为用来建造跳跃列表的扔硬币方法总有可能(尽管概率很小)生成一个糟糕的不平衡结构。但是在实际中它工作的很好,随机化平衡方案比在平衡二叉查找树中用的确定性平衡方案容易实现。跳跃列表在并行计算中也很有用,这里的插入可以在跳跃列表不同的部分并行的进行,而不用全局的数据结构重新平衡。

性能】

空间复杂度:O(n)

查找、插入和删除操作的时间复杂度都为: O(logn)

随机跳跃表表现性能也很不错,节省了大量复杂的调节平衡树的代码。其效率与红黑树、伸展树等这些平衡树可以说相差不大。

但跳跃表还在并发环境下有优势。在并发环境下,如果要更新数据,跳跃表需要更新的部分就比较少,锁的东西也就比较少,所以不同线程争锁的代价就相对少了,而红黑树有个平衡的过程,牵涉到大量的节点,争锁的代价也就相对较高了。性能也就不如前者了。

应用

了解过Redis的都知道,Redis有一个非常有用的数据结构:SortSet,基于它,我们可以很轻松的实现一个Top N的应用。这个SortSet底层就是利用跳表实现的。

跳表也被用在leveldb中。在一些词典结构中中也经常用跳表来实现字典,加快查找速度。

总结

作为一种简单的数据结构,在大多数应用中Skip lists能够代替平衡树。Skiplists算法非常容易实现、扩展和修改。Skip lists和进行过优化的平衡树有着同样高的性能,Skip lists的性能远远超过未经优化的平衡二叉树。

引用发明者William Pugh的话:

“跳跃列表是在很多应用中有可能替代平衡树而作为实现方法的一种数据结构。跳跃列表的算法有同平衡树一样的渐进的预期时间边界,并且更简单、更快速和使用更少的空间。”

【参考】

关于代码参考这里:http://blog.csdn.net/ict2014/article/details/17394259

----------------------------------

感谢您的访问,希望对您有所帮助。 欢迎大家关注、收藏以及评论。

----------------------------------

时间: 2024-10-15 20:10:58

查找——图文翔解SkipList(跳跃表)的相关文章

查找——图文翔解HashTree(哈希树)

引 在各种数据结构(线性表.树等)中,记录在结构中的相对位置是随机的.因此在机构中查找记录的时需要进行一系列和关键字的比较.这一类的查找方法建立在"比较"的基础上.查找的效率依赖于查找过程中所进行的比较次数. 之前我们介绍的各种基于比较的树查找算法,这些查找算法的效率都将随着数据记录数的增长而下降.仅仅是有的比较慢(时间复杂度为O(n)),有的比较快(时间复杂度是O(logn))而已.这些查找算法的平均查找长度是在一种比较理想的情况下获得的.在实际应用当中,对数据结构中数据的频繁增加和

查找——图文翔解RadixTree(基数树)

基数树 对于长整型数据的映射,如何解决Hash冲突和Hash表大小的设计是一个很头疼的问题. radix树就是针对这种稀疏的长整型数据查找,能快速且节省空间地完成映射.借助于Radix树,我们可以实现对于长整型数据类型的路由.利用radix树可以根据一个长整型(比如一个长ID)快速查找到其对应的对象指针.这比用hash映射来的简单,也更节省空间,使用hash映射hash函数难以设计,不恰当的hash函数可能增大冲突,或浪费空间. radix tree是一种多叉搜索树,树的叶子结点是实际的数据条目

查找——图文翔解Treap(树堆)

之前我们讲到二叉搜索树,从二叉搜索树到2-3树到红黑树到B-树. 二叉搜索树的主要问题就是其结构与数据相关,树的深度可能会很大,Treap树就是一种解决二叉搜索树可能深度过大的另一种数据结构. Treap Treap=Tree+Heap.Treap本身是一棵二叉搜索树,它的左子树和右子树也分别是一个Treap,和一般的二叉搜索树不同的是,Treap纪录一个额外的数据,就是优先级.Treap在以关键码构成二叉搜索树的同时,还满足堆的性质.这些优先级是是在结点插入时,随机赋予的,Treap根据这些优

【转】查找——图文翔解RadixTree(基数树)

本文转自:http://blog.csdn.net/yang_yulei/article/details/46371975 基数树 对于长整型数据的映射,如何解决Hash冲突和Hash表大小的设计是一个很头疼的问题.radix树就是针对这种稀疏的长整型数据查找,能快速且节省空间地完成映射.借助于Radix树,我们可以实现对于长整型数据类型的路由.利用radix树可以根据一个长整型(比如一个长ID)快速查找到其对应的对象指针.这比用hash映射来的简单,也更节省空间,使用hash映射hash函数难

SkipList (跳跃表)解析及其实现

目录 导言 查找结点的效率如何提升? 什么是跳跃表? 跳跃表必须是完美的? 预备知识 抛硬币实验 模拟建表 操作解析 伪代码 代码实现 柔性数组 跳跃表的创建与销毁 跳跃表表头结构体定义 跳跃表结点结构体定义 建立跳跃表表头操作 操作解析 伪代码 代码实现 创建单个结点操作 操作解析 代码实现 销毁操作 操作解析 代码实现 插入操作 操作解析 时间复杂度 伪代码 代码实现 删除操作 操作解析 时间复杂度 伪代码 代码实现 查找操作 操作解析 伪代码 代码实现 时间复杂度分析 简单应用 跳跃字母表

浅析SkipList跳跃表原理及代码实现

本文将总结一种数据结构:跳跃表.前半部分跳跃表性质和操作的介绍直接摘自<让算法的效率跳起来--浅谈“跳跃表”的相关操作及其应用>上海市华东师范大学第二附属中学 魏冉.之后将附上跳跃表的源代码,以及本人对其的了解.难免有错误之处,希望指正,共同进步.谢谢. 跳跃表(Skip List)是1987年才诞生的一种崭新的数据结构,它在进行查找.插入.删除等操作时的期望时间复杂度均为O(logn),有着近乎替代平衡树的本领.而且最重要的一点,就是它的编程复杂度较同类的AVL树,红黑树等要低得多,这使得其

【转】浅析SkipList跳跃表原理及代码实现

SkipList在Leveldb以及lucence中都广为使用,是比较高效的数据结构.由于它的代码以及原理实现的简单性,更为人们所接受.首先看看SkipList的定义,为什么叫跳跃表? "Skip lists  are data structures  that use probabilistic  balancing rather  than  strictly  enforced balancing. As a result, the algorithms  for insertion  a

skiplist 跳跃表

什么是跳跃表? SkipList在leveldb.redis以及lucence中都广为使用,是比较高效的数据结构.由于它的代码以及原理实现的简单性,更为人们所接受.我们首先看看SkipList的定义,为什么叫跳跃表? “     Skip lists  are data structures  that use probabilistic  balancing rather  than  strictly  enforced balancing. As a result, the algorit

详解SkipList跳跃链表【含代码】

本文始发于个人公众号:TechFlow,原创不易,求个关注 今天继续介绍分布式系统当中常用的数据结构,今天要介绍的数据结构非常了不起,和之前介绍的布隆过滤器一样,是一个功能强大原理简单的数据结构.并且它的缺点和短板更少,应用更加广泛,比如广泛使用的Redis就有用到它. SkipList简介 SkipList是一个实现快速查找.增删数据的数据结构,可以做到\(O(logN)\)复杂度的增删查.从时间复杂度上来看,似乎和平衡树差不多,但是和平衡树比较起来,它的编码复杂度更低,实现起来更加简单.学过