目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。
想象一下,给你一张草稿纸,一只笔,一个编辑器,你能立即实现一颗红黑树,或者AVL树
出来吗? 很难吧,这需要时间,要考虑很多细节,要参考一堆算法与数据结构之类的树,
还要参考网上的代码,相当麻烦。
用跳表吧,跳表是一种随机化的数据结构,目前开源软件 Redis 和 LevelDB 都有用到它,
它的效率和红黑树以及 AVL 树不相上下,但跳表的原理相当简单,只要你能熟练操作链表,
就能轻松实现一个 SkipList。
考虑一个有序表:
从该有序表中搜索元素 < 23, 43, 59 > ,需要比较的次数分别为 < 2, 4, 6 >,总共比较的次数
为 2 + 4 + 6 = 12 次。有没有优化的算法吗? 链表是有序的,但不能使用二分查找。类似二叉
搜索树,我们把一些节点提取出来,作为索引。得到如下结构:
这里我们把 < 14, 34, 50, 72 > 提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。
我们还可以再从一级索引提取一些元素出来,作为二级索引,变成如下结构:
这里元素不多,体现不出优势,如果元素足够多,这种索引结构就能体现出优势来了。
下面的结构是就是跳表:
其中 -1 表示 INT_MIN, 链表的最小值,1 表示 INT_MAX,链表的最大值。
跳表具有如下性质:
(1) 由很多层结构组成
(2) 每一层都是一个有序的链表
(3) 最底层(Level 1)的链表包含所有元素
(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。
例子:查找元素 117
(1) 比较 21, 比 21 大,往后面找
(2) 比较 37, 比 37大,比链表最大值小,从 37 的下面一层开始找
(3) 比较 71, 比 71 大,比链表最大值小,从 71 的下面一层开始找
(4) 比较 85, 比 85 大,从后面找
(5) 比较 117, 等于 117, 找到了节点。
具体的搜索算法如下:
C代码
/* 如果存在 x, 返回 x 所在的节点,
* 否则返回 x 的后继节点 */
find(x)
{
p = top;
while (1) {
while (p->next->key < x)
p = p->next;
if (p->down == NULL)
return p->next;
p = p->down;
}
}
先确定该元素要占据的层数 K(采用丢硬币的方式,这完全是随机的)
然后在 Level 1 ... Level K 各个层的链表都插入元素。
例子:插入 119, K = 2
如果 K 大于链表的层数,则要添加新的层。
例子:插入 119, K = 4
插入元素的时候,元素所占有的层数完全是随机的,通过一下随机算法产生:
C代码
int random_level()
{
K = 1;
while (random(0,1))
K++;
return K;
}
相当与做一次丢硬币的实验,如果遇到正面,继续丢,遇到反面,则停止,
用实验中丢硬币的次数 K 作为元素占有的层数。显然随机变量 K 满足参数为 p = 1/2 的几何分布,
K 的期望值 E[K] = 1/p = 2. 就是说,各个元素的层数,期望值是 2 层。
n 个元素的跳表,每个元素插入的时候都要做一次实验,用来决定元素占据的层数 K,
跳表的高度等于这 n 次实验中产生的最大 K,待续。。。
根据上面的分析,每个元素的期望高度为 2, 一个大小为 n 的跳表,其节点数目的
期望值是 2n。
在各个层中找到包含 x 的节点,使用标准的 delete from list 方法删除该节点。
例子:删除 71
以上转自https://kenby.iteye.com/blog/1187303
之前我们知道,二分查找依赖数组的随机访问,所以只能用数组来实现。如果数据存储在链表中,就真的没法用二分查找了吗?而实际上,我们只需要对链表稍加改造,就可以实现类似“二分”的查找算法,这种改造之后的数据结构叫作跳表(Skip List)。
对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低。
假如我们对链表每两个结点提取一个结点到上一级,然后建立一个索引指向原始结点,如下图所示。
这时候,我们要查找某一个数据的时候,就可以先在索引里面查找出一个大的范围,然后再下降到原始链表中精确查找。
比如,我们要查找 16,我们发现 16 位于 13 和 17 之间,这时候,我们就从 13 的地方下降到原始链表,然后再往后查询。原来我们查找 16,需要遍历 10 个结点,现在只需要遍历 7 个结点。
我们发现,加一层索引后,查找一个结点需要遍历的次数减少了,也就是查找效率提高了。
这一次,我们只需要遍历 6 个结点了。
数据量不大的时候这种方法可能效率提高得还不是很明显,下面看一个包含 64 个结点的例子,这次我们建立了五级索引。
查找 62 的时候原来需要遍历 62 次,现在只需要 11 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。
如果链表中总共有 nn 个结点,那么第一级索引就有 n2n2 个结点,第二级索引就有 n4n4 个结点,以此类推,那么第 kk 级索引就有 n2kn2k 个结点。如果最高级索引有 2 个结点,那总的索引级数 k=log2n−1k=log2n−1,如果我们算上原始链表的话,那也就是总共有 log2nlog2n 级。
在第 kk 级索引中,假设我们要查找的数据为 xx,当我们查找到 yy 结点时,发现 y
而总的级别数为 log2nlog2n,因此查找的时间复杂度就为 3∗log2n=logn3∗log2n=logn。跳表查找的时间复杂度和二分查找一样,但这其实是以空间来换时间的设计思路。
跳表的所有额外索引结点总数为 n2+n4+n8+...+4+2=n−2n2+n4+n8+...+4+2=n−2,所以跳表的空间复杂度为 O(n)O(n)。
但如果我们每三个结点建立一个索引,这时候额外需要的结点总数为 n2n2,虽然空间复杂度依然为 O(n)O(n),但减少了一半的索引节点存储空间。
实际上,在实际开发中,原始链表中存储的可能是很大的对象,而索引结点只需要存储关键值和几个指针,其额外占用的空间可以被忽略掉。
在链表中,如果我们知道要插入数据的位置,那么插入的时间复杂度就为 O(1)O(1)。在跳表中,查找的时间复杂度为 O(logn)O(logn),因此,动态插入数据的时间复杂度也就是 O(logn)O(logn) 了。
从链表中删除结点的时候,如果结点在索引中也有出现,那么我们除了要删除原始链表中的结点,还要删除索引中的。
当我们不停地往跳表中插入数据的时候,如果我们不更新索引,就有可能出现某两个结点之间数据非常多的情况。极端情况下,跳表还会退化为单链表。
因此,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表结点变多了,索引值就相应地增加一些。
当我们往跳表中插入数据的时候,我们可以选择同时也将这个数据插入到部分索引层中。而插入到哪些索引层中,则由一个随机函数生成一个随机数字来决定。如果这个数字为 K,那我们就将数据插入到第一级到第 K 级索引中。
Redis 中的有序集合支持的核心操作主要有以下几个:
其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度和跳表是一样的。
但是,按照区间查找数据这个操作,红黑树的效率没有跳表高。跳表可以在 O(logn)O(logn) 时间复杂度定位区间的起点,然后在原始链表中顺序向后查询就可以了,这样非常高效。
此外,相比于红黑树,跳表还具有代码更容易实现、可读性好、不容易出错、更加灵活等优点,因此 Redis 用跳表来实现有序集合。
以上转自http://www.cnblogs.com/seniusen/p/9870398.html