SAST Weekly 是由电子工程系学生科协推出的科技系列推送,内容涵盖信息领域技术科普、研究前沿热点介绍、科技新闻跟进探索等多个方面,帮助同学们增长姿势,开拓眼界,每周更新,欢迎关注!欢迎愿意分享知识的同学投稿至 eesast@mail.tsinghua.edu.cn , 期待你的作品!
九字班的同学们刚刚才结束了数算课上线性表的学习,那么应该会知道,对于一个普通单链表而言,查询一个元素的时间复杂度为O(n)。即使这个链表是有序的,我们也不能通过二分的方式降低其时间复杂度,而必须从头开始遍历。
例如,对于下面这个有序链表,假如我们要查找元素9,我们需要从头结点开始一共遍历8个结点才能找到元素9。
对于规模小的单链表,这样的耗时似乎还可以忍受,但如果链表很大,遍历一遍会花费大量的时间(导致一堆TLE,就像封面这样,心肺骤停.JPG)。那么,有没有办法快速查询有序单链表中的元素呢?那就是今天的主题——跳跃表。
举个栗子
由于元素有序,可以通过增加一些路径来加快查找速度。还是刚刚这个链表,如果我们添加一层:只需要遍历5次就可以找到元素9了(红色的线为查找路径)。
还能再加快查找速度吗?答案是肯定的,只需要再加一层。
对于具有n个元素的链表,我们可以采取logn 层指针路径的形式,消耗2n的空间,实现在 O(logn) 的时间复杂度内,查找某个目标元素的功能。这种数据结构,我们也称之为跳跃表。理想跳跃表应该具备如下特点:包含有N个元素节点的跳跃表拥有log2N层,并且上层链表包含的节点数恰好等于下层链表节点数的1/2。是不是有二分查找的意思了?
可是,单链表毕竟不是顺序存储结构,也不能实现常规的二分查找。如果我们对跳跃表进行插入/删除结点的操作,那么跳跃表结点数就会改变,意味着跳跃表的层数也会动态改变。我们该选取哪些节点到上一层?新插入的结点又该跨越多少层?
为了解决这个问题,通常的做法是,每次向跳跃表中增加一个节点时进行一次判定,有50%的概率向上层链表增加一个跳跃节点,并再次进行50%概率的判断,直到结果为不增加为止。
举个栗子 ·
我们要插入结点 3,4,通过概率判断知道3,4跨越的层数分别为0,2 (层数从0开始算),则插入的过程如下:
插入3,跨越0层。
插入4,跨越2层。
上帝掷不掷骰子不知道,反正跳跃表的实现是依赖“抛硬币”的
在数据规模比较小时,跳跃表形成后可能不是理想的跳跃表结构,但是当数据量增大,结构越接近理想的跳跃表结构。
对比插入,删除就比较简单了,例如我们要删除4,那我们直接把4及其所跨越的层数删除就行了。
文末附有查询、插入、删除的C++实现网址,感兴趣的同学可以前往进一步学习。
性能分析 ·
时间复杂度:当表中有n个元素时,搜索、插入、删除操作的复杂性均为O(n+MaxLevel)。在最坏情况下,可能只有一个MaxLevel级元素,且余下的所有元素均在0级链上。i>0时,在i级链上花费的时间为(MaxLevel),而在0级链上花费的时间为O(n)。尽管最坏情况下的性能较差,但跳跃表仍不失为一种有价值的数据描述方法。其每种操作(搜索、插入、删除)的平均复杂性均为O(logn)。
空间复杂度:最坏情况下所有元素都可能是MaxLevel级,每个元素都需要MaxLevel+1个指针。因此,除了存储n个元素(也就是n*sizeof(element)),还需要存储链指针(所需空间为O(n*MaxLevel))。不过,一般情况下,只有n*p个元素在1级链上,n*p2个元素在2级链上,n*pi在i级链上。因此指针域的平均值(不包括头尾节点的指针)是n/(1-p)。因此虽然最坏情况下空间需求比较大,但平均的空间需求并不大。当p=0.5时,平均空间需求(加上n个节点中的指针)大约是2n个指针的空间。
小结 ·
1.一个跳跃表应该有几个层(level)组成。
2.跳跃表的第一层包含所有的元素。
3.每一层都是一个有序的链表。
4.如果元素x出现在第i层,则所有比i小的层都包含x。
5.第i层的元素通过一个down指针指向下一层拥有相同值的元素。
6.Top指针指向最高层的第一个元素。
7.跳跃表时间复杂度为O(logn),空间复杂度为 O(n)。
后记&附录 ·
跳跃表结构是拿空间换时间的一种结构,尽管空间占用不是很大。跳跃表不同于树结构,如红黑树等,它不需要花费过多的精力进行平衡算法,这也是跳跃表的性能优越的一个方面。
感谢大家的阅读!希望大家能在阅读的过程中感受到算法的魅力。祝大家学习顺利,生活愉快~
附:跳跃表查找、插入、删除C++实现:https://blog.csdn.net/yinlili2010/article/details/39503655
撰稿人:杨馨婷
审稿人:贺鲲鹏