聊聊Mysql索引和redis跳表( 二 )


4、集合(Set)
集合,整数的有序列表可以直接使用set 。可以用作某些去重功能,例如用户名不能重复等,另外,还可以对集合进行交集,并集操作,来查找某些元素的共同点
5、有序集合(zset)
有序集合,可以使用范围查找,排行榜功能或者topN功能 。
其中第五个zset 有序集合 就是用跳表来实现的 。那 Redis 为什么会选择用跳表来实现有序集合呢?
一、如何理解跳表?对于单链表来说,我们查找某个数据,只能从头到尾遍历链表,此时时间复杂度是 ○(n) 。

聊聊Mysql索引和redis跳表

文章插图
 
那么怎么提高单链表的查找效率呢?看下图,对链表建立一级 索引,每两个节点提取一个结点到上一级,被抽出来的这级叫做 索引 或 索引层 。
聊聊Mysql索引和redis跳表

文章插图
 
开发中经常会用到一种处理方式,hashmap 中存储的值类型是一个 list,这里就可以把索引当做 hashmap 中的键,将每 2 个结点看成每个键对应的值 list 。
所以要找到13,就不需要将16前的结点全遍历一遍,只需要遍历索引,找到13,然后发现下一个结点是17,那么16一定是在 [13,17] 之间的,此时在13位置下降到原始链表层,找到16,加上一层索引后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了
那么我们再加一级索引呢?
跟前面建立一级索引的方式相似,我们在第一级索引的基础上,每两个结点就抽出一个结点到第二级索引 。此时再查找16,只需要遍历 6 个结点了,需要遍历的结点数量又减少了 。
聊聊Mysql索引和redis跳表

文章插图
 
当结点数量多的时候,这种添加索引的方式,会使查询效率提高的非常明显
聊聊Mysql索引和redis跳表

文章插图
 
二、用跳表查询到底有多快在一个单链表中,查询某个数据的时间复杂度是 ○(n),那在一个具有多级索引的跳表中,查询某个数据的时间复杂度是多少呢?
按照上面的示例,每两个节点就抽出一个一级索引,每两个一级索引又抽出一个二级索引,所以第一级索引的结点个数大约就是 n/2,第二级索引的结点个数就是 n/4,第 k 级索引的结点个数就是 n/2^k 。
假设一共建立了 h 级索引,最高级的索引有两个节点(如果最高级索引只有一个结点,那么这一级索引起不到判断区间的作用,那么是没什么意义的),所以有:
聊聊Mysql索引和redis跳表

文章插图
时间复杂度的分析

聊聊Mysql索引和redis跳表

文章插图
 
根据上图得知,每级遍历 3 个结点即可,而跳表的高度为 h ,所以每次查找一个结点时,需要遍历的结点数为 3*跳表高度 ,所以忽略低阶项和系数后的时间复杂度就是 ○(㏒n)
其实此时就相当于基于单链表实现了二分查找 。但是这种查询效率的提升,由于建立了很多级索引,会不会很浪费内存呢?
三、跳表是不是很浪费内存?来分析一下跳表的空间复杂度 。为O(n)
聊聊Mysql索引和redis跳表

文章插图
 

聊聊Mysql索引和redis跳表

文章插图
空间复杂度
所以如果将包含 n 个结点的单链表构造成跳表,我们需要额外再用接近 n 个结点的存储空间,那怎么才能降低索引占用的内存空间呢?
前面是每两个结点抽一个结点到上级索引,如果我们每三个,或每五个结点,抽一个结点到上级索引,是不是就不用那么多索引结点了呢?
聊聊Mysql索引和redis跳表

文章插图
 
计算空间复杂度的过程与前面的一致,尽管最后空间复杂度依然是 ○(n),但我们知道,使用大○表示法忽略的低阶项或系数,实际上同样会产生影响,只不过我们为了关注高阶项而将它们忽略 。
聊聊Mysql索引和redis跳表

文章插图
空间复杂度
实际上,在实际开发中,我们不需要太在意索引占据的额外空间,在学习数据结构与算法时,我们习惯的将待处理数据看成整数,但是实际开发中,原始链表中存储的很可能是很大的对象,而索引结点只需要存储关键值(用来比较的值)和几个指针(找到下级索引的指针),并不需要存储原始链表中完整的对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了 。


推荐阅读