前言
集合在计算机中的设计目标
- 查询尽量快。几乎没有业务对查询速度是没有要求是。
- 增删改尽量快。 如果查询是第一业务需求条件。那么增删改就可以成为第二业务条件,有些业务场景中增删改的并不多,速度要求也不高。
- 占得空间尽量少。虽然满足业务是第一要务,但空间占用引起的成本问题也是要考虑的问题。计算机中的很多算法就是在时间 和 空间之间做平衡。
如何快速查询
直接从头到尾遍历的就不说了,虽然最简单,但也是最慢的一种算法,时间复杂度:o(n)
数组 + 二分查找算法。
优点:使用二分搜索算法检索,时间复杂度o(logn)
注意这里的logn只是计算机行业里的默认写法(目前计算机是二进制的世界),表示的是"以2为底n的对数”,数学公式
大家经常在计算机算法时间复杂度中可以见到,但也许并没有想过这是个什么样的数(它大概多大)。可以使用对数计算器算一下,这是一个特别小的数(我们常说“指数级增长”,那么他的反面对数,就是“指数级下降”)
缺点:
- 数组问题1:占用过多连续存储空间。 在计算机的物理存储中只要两种数据结构:数组,链表。数组虽然可以实现随机访问,但数组要求计算机分配连续的大块存储空间是比较浪费存储空间的一件事。(这里是浪费,而不是占用,因为计算机中的整块连续存储是稀缺资源)
- 数组问题2:数组扩容问题。除非我们的业务允许一开始就固定好数组大小,并且之后不会增加。否则新增数据时重新分配数组空间并且复制数据的过程又是一个即浪费空间又消耗性能的过程。
动态构建二分搜索树。
本文不是讲二分搜索树的,具体原理这里不再详述。
优点:使用链表解决了前面说了数组的两种问题。同时还保留了二分法的快速检索优势。
缺点:
- 问题1:占用空间。构建二叉树占用了更多空间。这个缺点几乎可以忽略,因为新增的节点只是“索引”,占用空间并不大,当我们实际存储的数据比较大时,新增的这点索引就几乎可以忽略了。
- 问题2:树结构不稳定。当数据不够随机的时候,可能构建出一颗不平衡二叉树,检索时间复杂度可以退化到最坏的o(n)。
使用数组 + hash算法
如果说二分搜索树还算符合人的常规思路,那么哈希可以说是一种天才算法,简单又高效。
不但查询时间复杂度只有o(1),而且可以通过控制哈希函数,把前面所说的“数组问题2”(数组扩容)解决掉,把分配的空间边界固定下来。但前面所说的“数组问题1”(占用过多连续存储空间)还是没法解决,要解决这个问题,似乎只能靠链表。但链表无法随机访问,而hash算法利用的就是数组的随机访问特性。
主角登场:跳表
一说到跳表,我们经常和红黑数做对比,原因就是 两者都在试图优化 “二分搜索树”遇到的“问题2”(树结构不稳定)。 两种结构从两个不同的思路来解决这个问题:
- 红黑树的思路:树既然容易长歪,变成不平衡二叉树,那我就在每一步新增节点的时候对树进行调整(具体算法这里不再详述)。
- 跳表的思路:前面也说了,树之所以容易长歪,根源是数据不够随机,导致树长歪,那我就给这棵树加上随机性(这思路也是够天才的)。结果就产生了让树从树叶开始倒着长这样的神奇思路。而我们的目的是形成一颗平衡二叉树,平衡二叉树最完美的外观就是这个样子:
从现在开始,我们规定一下:叶子节点为树的第一层,往上依次为第二层、第三层、、、、
假如我们最终要存储的是n个数全在叶子节点上,那么第二层就应该有n/2个节点,第三层有n/4个节点。。。以此类推。
下面开始一层一层往上构建:
- 第一层:没什么说的,就是全部的存储节点。
- 第二层:让哪些节点放到这一层成为索引呢,这里算法设计者做了一个很巧妙的设计:让每个叶子节点抛硬币。谁抛中正面谁称为索引,这样就给二叉树带来了随机性。根据抛硬币的概率,每个叶子节点都有1/2的概率可以称为索引,放到第二层。期望也就是n/2个节点会被放到第二层。
- 第三层:让第二层的节点抛硬币,同样的有 (n/2) * (1/2) = n/4 左右的节点被放到了第三层。
。。。
最终就构建形成如下图所示的跳表
实现代码(java)
SkipList(使用了泛型,可以直接使用。如果发现代码可以改进或者有bug,请留言)