redis使用跳表不用B+数的缘由是:redis是内存数据库,而B+树纯粹是为了mysql这种IO数据库准备的。B+树的每一个节点的数量都是一个mysql分区页的大小(阿里面试)html
还有个几个姊妹篇:介绍mysql的B+索引原理 参考:一步步分析为何B+树适合做为索引的结构 以及索引原理 (阿里面试)mysql
参考:kafka如何实现高并发存储-如何找到一条须要消费的数据(阿里)面试
参考:二分查找法:各类排序算法的时间复杂度和空间复杂度(阿里)redis
关于mysql 存储引擎 介绍包括默认的索引方式参考:MySql的多存储引擎架构, 默认的引擎InnoDB与 MYISAM的区别(滴滴 阿里)算法
3*跳表高度
,因此忽略低阶项和系数后的时间复杂度就是 ○(㏒n),空间复杂度是O(n) 数据结构 | 实现原理 | key查询方式 | 查找效率 | 存储大小 | 插入、删除效率 |
---|---|---|---|---|---|
Hash | 哈希表 | 支持单key | 接近O(1) | 小,除了数据没有额外的存储 | O(1) |
B+树 | 平衡二叉树扩展而来 | 单key,范围,分页 | O(Log(n) | 除了数据,还多了左右指针,以及叶子节点指针 | O(Log(n),须要调整树的结构,算法比较复杂 |
跳表 | 有序链表扩展而来 | 单key,分页 | O(Log(n) | 除了数据,还多了指针,可是每一个节点的指针小于<2,因此比B+树占用空间小 | O(Log(n),只用处理链表,算法比较简单 |
对LSM结构感兴趣的能够看下cassandra vs mongo (1)存储引擎sql
若是对如下问题感到困惑或只知其一;不知其二,请继续看下去,相信本文必定会对你有帮助数据库
首先为何要把mysql索引和redis跳表放在一块儿讨论呢,由于他们解决的都是同一种问题,用于解决数据集合的查找问题,即根据指定的key,快速查到它所在的位置(或者对应的value)编程
当你站在这个角度去思考问题时,还会不知道B+树索引和hash索引的区别吗数组
如今咱们将问题领域边界划分清楚了,就是为了解决数据集合的查找问题。这一块须要考虑哪些问题呢安全
咱们看下几种经常使用的查找结构
hash
hash是key,value形式,经过一个散列函数,可以根据key快速找到value
关于hash算法 ,这也是阿里的必考题 深度的原理 我写了几篇博客:尤为是最后一篇resize ,以及resize以前与以后的hashmap的状况,
参考:Hashtable数据存储结构-遍历规则,Hash类型的复杂度为啥都是O(1)-源码分析
参考:HashMap, HashTable,HashSet,TreeMap 的时间复杂度
参考:HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别
参考:ConcurrentHashMap原理分析(1.7与1.8)-put和 get 两次Hash到达指定的HashEntry
resize 参考:HashMap多线程并发问题分析-正常和异常的rehash1(阿里)
B+ 树:
注意 这是关于B+树的总结,若是你掌握到这个程度 是远远不够的,
请参考详细的B+树原理:一步步分析为何B+树适合做为索引的结构 以及索引原理 (阿里面试)
B+树 的数据都在叶子节点,非叶子节点存放 索引
B+树是在平衡二叉树基础上演变过来,为何咱们在算法课上没学到B+树和跳表这种结构呢。由于他们都是从工程实践中获得,在理论的基础上进行了妥协。
B+树首先是有序结构,为了避免至于树的高度过高,影响查找效率,在叶子节点上存储的不是单个数据,而是一页数据,提升了查找效率,而为了更好的支持范围查询,B+树在叶子节点冗余了非叶子节点数据,为了支持翻页,叶子节点之间经过指针链接。
跳表
上几篇主要是学习二分查找算法,可是二分查找底层依赖的是数组随机访问的特性,因此只能用数组来实现。若是数据存储在链表中,就没办法使用二分查找了吗?
此时跳表出现了,跳表(Skip list)
实际上就是在链表的基础上改造生成的。
跳表是一种各方面性能都比较优秀的 动态数据结构,能够支持快速的插入、删除、查找操做,写起来也不复杂,甚至能够替代 红黑树??。
Redis 一共有5种数据结构,包括:
一、字符串(String)
redis对于KV的操做效率很高,能够直接用做计数器。例如,统计在线人数等等,另外string类型是二进制存储安全的,因此也可使用它来存储图片,甚至是视频等。
二、哈希(hash)
存放键值对,通常能够用来存某个对象的基本属性信息,例如,用户信息,商品信息等,另外,因为hash的大小在小于配置的大小的时候使用的是ziplist结构,比较节约内存,因此针对大量的数据存储能够考虑使用hash来分段存储来达到压缩数据量,节约内存的目的,例如,对于大批量的商品对应的图片地址名称。好比:商品编码固定是10位,能够选取前7位作为hash的key,后三位做为field,图片地址做为value。这样每一个hash表都不超过999个,只要把redis.conf中的hash-max-ziplist-entries改成1024,便可。
三、列表(List)
列表类型,能够用于实现消息队列,也可使用它提供的range命令,作分页查询功能。
四、集合(Set)
集合,整数的有序列表能够直接使用set。能够用做某些去重功能,例如用户名不能重复等,另外,还能够对集合进行交集,并集操做,来查找某些元素的共同点
五、有序集合(zset)
有序集合,可使用范围查找,排行榜功能或者topN功能。
其中第五个zset 有序集合 就是用跳表来实现的。那 Redis 为何会选择用跳表来实现有序集合呢?
对于单链表来讲,咱们查找某个数据,只能从头至尾遍历链表,此时时间复杂度是 ○(n)。
那么怎么提升单链表的查找效率呢?看下图,对链表创建一级 索引
,每两个节点提取一个结点到上一级,被抽出来的这级叫作 索引
或 索引层
。
开发中常常会用到一种处理方式,hashmap 中存储的值类型是一个 list,这里就能够把索引当作 hashmap 中的键,将每 2 个结点当作每一个键对应的值 list。
因此要找到13,就不须要将16前的结点全遍历一遍,只须要遍历索引,找到13,而后发现下一个结点是17,那么16必定是在 [13,17] 之间的,此时在13位置降低到原始链表层,找到16,加上一层索引后,查找一个结点须要遍历的结点个数减小了,也就是说查找效率提升了
那么咱们再加一级索引呢?
跟前面创建一级索引的方式类似,咱们在第一级索引的基础上,每两个结点就抽出一个结点到第二级索引。此时再查找16,只须要遍历 6 个结点了,须要遍历的结点数量又减小了。
当结点数量多的时候,这种添加索引的方式,会使查询效率提升的很是明显、
在一个单链表中,查询某个数据的时间复杂度是 ○(n),那在一个具备多级索引的跳表中,查询某个数据的时间复杂度是多少呢?
按照上面的示例,每两个节点就抽出一个一级索引,每两个一级索引又抽出一个二级索引,因此第一级索引的结点个数大约就是 n/2
,第二级索引的结点个数就是 n/4
,第 k 级索引的结点个数就是 n/2^k
。
假设一共创建了 h 级索引,最高级的索引有两个节点(若是最高级索引只有一个结点,那么这一级索引发不到判断区间的做用,那么是没什么意义的),因此有:
根据上图得知,每级遍历 3 个结点便可,而跳表的高度为 h ,因此每次查找一个结点时,须要遍历的结点数为 3*跳表高度
,因此忽略低阶项和系数后的时间复杂度就是 ○(㏒n)
其实此时就至关于基于单链表实现了二分查找。可是这种查询效率的提高,因为创建了不少级索引,会不会很浪费内存呢?
来分析一下跳表的空间复杂度。 为O(n)
因此若是将包含 n 个结点的单链表构形成跳表,咱们须要额外再用接近 n 个结点的存储空间,那怎么才能下降索引占用的内存空间呢?
前面是每两个结点抽一个结点到上级索引,若是咱们每三个,或每五个结点,抽一个结点到上级索引,是否是就不用那么多索引结点了呢?
计算空间复杂度的过程与前面的一致,尽管最后空间复杂度依然是 ○(n),但咱们知道,使用大○表示法忽略的低阶项或系数,实际上一样会产生影响,只不过咱们为了关注高阶项而将它们忽略。
实际上,在实际开发中,咱们不须要太在乎索引占据的额外空间,在学习数据结构与算法时,咱们习惯的将待处理数据当作整数,可是实际开发中,原始链表中存储的极可能是很大的对象,而索引结点只须要存储关键值(用来比较的值)和几个指针(找到下级索引的指针),并不须要存储原始链表中完整的对象,因此当对象比索引结点大不少时,那索引占用的额外空间就能够忽略了。
跳表这个动态数据结构,不只支持查找操做,还支持动态的插入、删除操做,并且插入、删除操做的时间复杂度也是 ○(㏒n)。
对于单纯的单链表,须要遍历每一个结点来找到插入的位置。可是对于跳表来讲,由于其查找某个结点的时间复杂度是 ○(㏒n),因此这里查找某个数据应该插入的位置,时间复杂度也是 ○(㏒n)。
那么删除操做呢?
当咱们不停的往跳表中插入数据时,若是咱们不更新索引,就可能出现某 2 个索引结点之间数据很是多的状况。极端状况下,跳表会退化成单链表。
跳表是经过随机函数来维护前面提到的 平衡性
。
咱们往跳表中插入数据的时候,能够选择同时将这个数据插入到第几级索引中,好比随机函数生成了值 K,那咱们就将这个结点添加到第一级到第 K 级这 K 级索引中。
跳表的实现有点复杂,而且跳表的实现并非这篇的重点。主要是学习思路。
Redis 中的有序集合是经过跳表来实现的,严格点讲,还用到了散列表(关于散列表),若是查看 Redis 开发手册,会发现 Redis 中的有序集合支持的核心操做主要有下面这几个:
其中,插入、查找、删除以及迭代输出有序序列这几个操做,红黑树也能完成,时间复杂度和跳表是同样的,可是,按照区间来查找数据这个操做,红黑树的效率没有跳表高。
对于按照区间查找数据这个操做,跳表能够作到 ○(㏒n) 的时间复杂度定位区间的起点,而后在原始链表中顺序日后遍历就能够了。这样作很是高效。
固然,还有其余缘由,好比,跳表代码更容易实现,可读性好不易出错。跳表更加灵活,能够经过改变索引构建策略,有效平衡执行效率和内存消耗。
不过跳表也不能彻底替代红黑树。由于红黑树出现的更早一些。不少编程语言中的 Map 类型都是用红黑树来实现的。写业务的时候直接用就行,可是跳表没有现成的实现,开发中想用跳表,得本身实现。