文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。
跳表的由来
说明:图片来自极客时间
由来
二分查找的数据结构是数组,利用数组随机访问的特定查找的时间复杂度是O(logn)。如果数据结构是链表,可以达到这样的速度吗?答案是可以的。只是要改造。改造之后的结构就是跳表,是一种动态数据结构,可以支持快速的插入、删除、查找、按范围查找。功能类似于红黑树。Redis中的有序集合使用的就是跳表。
跳表的结构
对于单链表来说,存储的数据是有序的,想要查找某个数,时间复杂度是O(n)。如果对单链表建一个一级“索引”,就是说每两个节点提取一个节点。提取出的节点有一个down指针指向原始链表中的同一个节点。
现在要查找数据16,那么查找路径是:1,4,7,9,13,13(原始链表),16。查找7个节点。单链表查找16需要查找10个节点。
如果对一级索引再建“索引”,形成二级索引。
现在要查找数据16,那么查找路径是:1,7,13,13,13,16。查找6个节点。
当n小的时候,减少的节点数量不明显。如果是n=64。建5级索引。
现在查找62需要11个节点,路径是1,33,33,49,49,57,57,61,61,61,62。原来需要62个节点。提升效果很明显。当n越大,提升效果越明显。
跳表=链表+多级索引
跳表的时空复杂度
时间复杂度
当节点个数为n的时候,跳表会建几层索引呢?第1级索引节点个数n2\dfrac{n}{2}2n,第2级索引节点个数n4\dfrac{n}{4}4n,第k级索引节点个数n2k\dfrac{n}{2^k}2kn。最上面一层索引节点个数是2。也就是说2=n2l2=\dfrac{n}{2^l}2=2ln,l+1=log2nl+1=log_2nl+1=log2n,l=log2n−1l=log_2n-1l=log2n−1。再加上原始链表层,跳表有log2nlog_2nlog2n层,记为logn。
每一层最多查询的节点个数是3。因为在建每一层索引的时候,是每2个数据建一个节点。例如查找数据x,在第k层发现y<x,x>zy<x,x>zy<x,x>z,所以通过y的down指针向,从第k层走到第k-1层。而y和z节点之间最多有3个节点(包含y和z)。
那么跳表的时间复杂度就是O(logn)。和二分查找是同样的查找效率。
空间复杂度
链表的查找速度和二分一样,这是需要付出空间代价的。也就是以空间换时间。那么额外需要多少空间呢?n2+n4+n8+...+2=n\dfrac{n}{2}+\dfrac{n}{4}+\dfrac{n}{8}+...+2=n2n+4n+8n+...+2=n,等比数列求和。所以空间复杂度是O(n)。
跳表插入和删除
插入
对于插入来讲,为了维持链表的有序性,在插入一个数据的时候需要先查找到插入的位置。
例如在链表中插入6,需要查找插入位置,查找节点1,1,4,4,5,时间复杂度和查找一个数字类似,O(logn)。链表的插入操作是O(1),所以整体插入操作时间复杂度O(logn)。
删除
删除操作不仅要删除链表层,同时需要删除索引层的节点。时间复杂度O(logn)。
动态索引表更新
索引更新
当不断插入数据的时候,如果不更新索引层,极端情况下跳表退化为单链表。当插入数据的时候,同时更新某些索引层。至于在哪些层建索引,可以通过随机函数来选择。
思考题
1 redist为什么使用跳表而不是红黑树?
redist的核心操作是:
插入一个数据;
删除一个数据;
查找一个数据;
按范围查找一个区间内的数据;
迭代输出有序序列
红黑树效率不高的操作是:按范围查找一个区间内的数据。
其他原因:跳表更容易实现,代码比较简单。
2 如果每3个或者5个节点抽取一个做索引,那么跳表的时间复杂度和空间复杂度是多少呢?
如果每三个或者五个节点提取一个节点作为上级索引,那么对应的查询数据时间复杂度,也还是 O(logn)。其实这里的底数已经不是2,而是3或者5。
空间复杂度,也依然是一个等比数列的和:n3+n9+n8+...+3=32(n−1)\dfrac{n}{3}+\dfrac{n}{9}+\dfrac{n}{8}+...+3=\dfrac{3}{2}(n-1)3n+9n+8n+...+3=23(n−1),记为O(n)。