zset 有序集合:(zadd,zrange,zcard,zrank)
- zset 内部由 hash 字典加跳跃表来实现。
- zadd test 99 “math”
- zrange test 60 100 (区间查找60到100分的 value)
- zcard
- zrank test ”math“ (排名)
使用场景:
- 可以用来存储学生的成绩,value 是学生的 ID,score 是他的考试成绩,可以按成绩进行排序,这样就可以得到学生的名次了。
- 还能用来存储粉丝列表,value 是粉丝的用户 ID,score 是关注时间,可以对粉丝按关注时间进行排序。
跳表: 跳表是一种链表加多层索引的结构,支持快速的插入、删除、查找操作,时间复杂度都是O(logn)。空间复杂度为O(n)。
跳跃表的时间复杂度为O(logn):假设跳表每两个元素提取出一个元素作为上一级的索引,也就是开始是1/2,然后1/4,1/8 … ,每一级索引减少上一级一半的元素,那么最高级索引有 2 个元素,假设总元素个数为 n,那么 n/(2h) = 2 ,得到高度 h = logn - 1,再加上原始链表这一层,整个跳表的高度就是 logn 了。在这个跳表中查询元素时,每一层都要遍历 2 个节点,这样查找一个数据的时间复杂度就是 O(2*logn),所以跳表的时间复杂度为 O(logn)。
跳跃表的空间复杂度为O(n):假设每两个元素提取一个索引,那么最后额外需要的空间是一个等比数列,为:n/2 + n/4 + n/8 … + 8 + 4 + 2 = n - 2 ,所以跳表的空间复杂度为 O(n)。(计算方法是对式子 + 2 - 2)
Redis之所以使用跳表,是因为跳表能以 O(logn) 的时间复杂度定位到区间的起点,然后在原始链表中顺序往后遍历来找到区间中的元素。
为什么Redis选择使用跳表而不是红黑树来实现有序集合 zset?跳表优点:Redis的有序集合支持的操作有:插入元素,删除元素,查找元素,有序输出所有元素,查找区间内所有元素。前4项红黑树都可以完成,且时间复杂度与跳表一致。但最后一项,红黑树的效率就没有跳表高了。在跳表中,要查找区间的元素,只需定位到两个区间端点在最低层级的位置,然后顺序遍历元素即可,非常高效。而红黑树只能定位到端点后,再从首位置开始每次都要查找后继节点,相对来说比较耗时。此外,跳表更加灵活,可以通过改变索引结构来平衡执行效率和内存消耗之间的关系,而且跳表实现起来简单易读,红黑树实现起来相对困难,所以Redis选择使用跳表来实现有序集合。
为什么有序集合 zset 需要同时使用跳跃表和字典来实现?:因为无论单独使用字典还是跳跃表,性能与同时使用它们相比会降低。比如只使用字典实现有序集合,那么虽然以O(1)复杂度查找成员分值的特性会被保留,但因为字典是以无序的方式来保存集合元素,所以执行范围型操作,比如 zrank、zrange命令时,程序都需要对字典保存的所有元素进行排序,完成这种排序至少需要O(NlogN)的时间复杂度,以及额外O(N)的内存空间,因为要创建一个数组来保存排序后的元素。 如果只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度将从O(1)上升为O(logN)。 因此,为了让有序集合的查找和范围型操作都尽可能快的执行,redis选择同时使用字典和跳跃表来实现有序集合,这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用它们来保存集合元素不会产生重复成员或分值,因此也不会浪费额外的内存。
redis中跳跃表的应用场景:redis只在两个地方用到了跳跃表,一个是实现有序集合键(zset),另一个是在集群节点中用作内部数据结构。
跳表是可以实现二分查找的有序链表(链表加上多级索引的结构,就是跳表)