基于链表的查询由于不能使用二分查找算法,因此需要挨个遍历链表元素对比定位。插入和删除操作虽然只需要进行指针的变换,但首先还是要定位到插入位置,因此当链表的数据量比较大的时候,就会出现效率很低,非常的耗时。
为了减少遍历时比较的次数,提出将链表中部分的关键节点提取出来。例如1-2-3-4-5-6-7-9,将奇数点1-3-5-7-9提取出来作为关键点,当我要插入8的时候,就只需要和1,3,5,7,9进行比较而不用和2,4,6比较,这样一来就减少了比较的次数,当数据量比较大的时候,这种效率的提高就会比较明显。这是一种以空间换取时间的策略。其实这就相当于将1-3-5-7-9提取出来作为一级索引,接下来还可以继续抽取关键点进行二级索引,三级索引...等等。每建立一层索引比较次数就会降低为原先的1/2.
当新插入很多节点的时候,之前的上层节点的索引关键点就会不够用,就要考虑在每当在新插入关键点的时候就要从新关键点中选取一部分提到上一层。至于怎么提法?由于跳跃表的添加和删除关键点是不可预测的,很难用一种有效的算法来保证跳表的索引部分始终均匀,因此给出的方法是使用抛硬币的概率事件来决定,由于在大样本的情况下,正反面的概率都会趋近0.5,可以使得大体趋近均匀。
跳跃表插入关键点的关键步骤大概如下:
1)首先新关键点和各层的索引关键点进行比较,确定在原链表(最底层链表)中需要插入的位置。O(logn)
2) 把新关键点插入到原链表;
3)利用抛硬币的随机方式决定新关键点是否需要提升为上一层的索引。结果为正则继续提升,为负则停止提升。
跳跃表删除关键点的关键步骤大概如下:
1)首先自上而下,查找第一次出现关键点的索引,并逐层找到每一层对应的节点。O(logn)
2)删除每一层查找到的关键点,如果该层只剩下一个节点,删除当前整个索引层(原链表层除外)
目前经常使用的平衡数据结构有: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代码
1.
3. find(x)
4. {
5. p = top;
6. while (1) {
7. while (p->next->key < x)
8. p = p->next;
9. if (p->down == NULL)
10. return p->next;
11. p = p->down;
12. }
13. }
先确定该元素要占据的层数 K(采用丢硬币的方式,这完全是随机的)
然后在 Level 1 ... Level K 各个层的链表都插入元素。
例子:插入 119, K = 2
如果 K 大于链表的层数,则要添加新的层。
例子:插入 119, K = 4
插入元素的时候,元素所占有的层数完全是随机的,通过一下随机算法产生:
C代码
1. int random_level()
2. {
3. K = 1;
4.
5. while (random(0,1))
6. K++;
7.
8. return K;
9. }
相当与做一次丢硬币的实验,如果遇到正面,继续丢,遇到反面,则停止,
用实验中丢硬币的次数 K 作为元素占有的层数。显然随机变量 K 满足参数为 p = 1/2 的几何分布,
几何分布的期望值为E[K] = 1/p
因此,K 的期望值 E[K] = 1/p = 2. 就是说,各个元素的层数,期望值是 2 层。
n 个元素的跳表,每个元素插入的时候都要做一次实验,用来决定元素占据的层数 K,
跳表的高度等于这 n 次实验中产生的最大 K。
根据上面的分析,每个元素的期望高度为 2, 一个大小为 n 的跳表,其节点数目的
期望值是 2n。
在各个层中找到包含 x 的节点,使用标准的 delete from list 方法删除该节点。
例子:删除 71
引言:
上周现场面试阿里巴巴研发工程师终面,被问到如何让链表的元素查询接近线性时间。笔者苦思良久,缴械投降。面试官告知回去可以看一下跳跃表,遂出此文。
我们知道,普通单链表查询一个元素的时间复杂度为O(n),即使该单链表是有序的,我们也不能通过2分的方式缩减时间复杂度。
如上图,我们要查询元素为55的结点,必须从头结点,循环遍历到最后一个节点,不算-INF(负无穷)一共查询8次。那么用什么办法能够用更少的次数访问55呢?最直观的,当然是新开辟一条捷径去访问55。
如上图,我们要查询元素为55的结点,只需要在L2层查找4次即可。在这个结构中,查询结点为46的元素将耗费最多的查询次数5次。即先在L2查询46,查询4次后找到元素55,因为链表是有序的,46一定在55的左边,所以L2层没有元素46。然后我们退回到元素37,到它的下一层即L1层继续搜索46。非常幸运,我们只需要再查询1次就能找到46。这样一共耗费5次查询。
那么,如何才能更快的搜寻55呢?有了上面的经验,我们就很容易想到,再开辟一条捷径。
如上图,我们搜索55只需要2次查找即可。这个结构中,查询元素46仍然是最耗时的,需要查询5次。即首先在L3层查找2次,然后在L2层查找2次,最后在L1层查找1次,共5次。很显然,这种思想和2分非常相似,那么我们最后的结构图就应该如下图。
我们可以看到,最耗时的访问46需要6次查询。即L4访问55,L3访问21、55,L2访问37、55,L1访问46。我们直觉上认为,这样的结构会让查询有序链表的某个元素更快。那么究竟算法复杂度是多少呢?
如果有n个元素,因为是2分,所以层数就应该是log n层 (本文所有log都是以2为底),再加上自身的1层。以上图为例,如果是4个元素,那么分层为L3和L4,再加上本身的L2,一共3层;如果是8个元素,那么就是3+1层。最耗时间的查询自然是访问所有层数,耗时logn+logn,即2logn。为什么是2倍的logn呢?我们以上图中的46为例,查询到46要访问所有的分层,每个分层都要访问2个元素,中间元素和最后一个元素。所以时间复杂度为O(logn)。
至此为止,我们引入了最理想的跳跃表,但是如果想要在上图中插入或者删除一个元素呢?比如我们要插入一个元素22、23、24……,自然在L1层,我们将这些元素插入在元素21后,那么L2层,L3层呢?我们是不是要考虑插入后怎样调整连接,才能维持这个理想的跳跃表结构。我们知道,平衡二叉树的调整是一件令人头痛的事情,左旋右旋左右旋……一般人还真记不住,而调整一个理想的跳跃表将是一个比调整平衡二叉树还复杂的操作。幸运的是,我们并不需要通过复杂的操作调整连接来维护这样完美的跳跃表。有一种基于概率统计的插入算法,也能得到时间复杂度为O(logn)的查询效率,这种跳跃表才是我们真正要实现的。
容易实现的跳跃表,它允许简单的插入和删除元素,并提供O(logn)的查询时间复杂度,以下我们简称为跳跃表。
先讨论插入,我们先看理想的跳跃表结构,L2层的元素个数是L1层元素个数的1/2,L3层的元素个数是L2层的元素个数的1/2,以此类推。从这里,我们可以想到,只要在插入时尽量保证上一层的元素个数是下一层元素的1/2,我们的跳跃表就能成为理想的跳跃表。那么怎么样才能在插入时保证上一层元素个数是下一层元素个数的1/2呢?很简单,抛硬币就能解决了!假设元素X要插入跳跃表,很显然,L1层肯定要插入X。那么L2层要不要插入X呢?我们希望上层元素个数是下层元素个数的1/2,所以我们有1/2的概率希望X插入L2层,那么抛一下硬币吧,正面就插入,反面就不插入。那么L3到底要不要插入X呢?相对于L2层,我们还是希望1/2的概率插入,那么继续抛硬币吧!以此类推,元素X插入第n层的概率是(1/2)的n次。这样,我们能在跳跃表中插入一个元素了。
在此还是以上图为例:跳跃表的初试状态如下图,表中没有一个元素:
如果我们要插入元素2,首先是在底部插入元素2,如下图:
然后我们抛硬币,结果是正面,那么我们要将2插入到L2层,如下图
继续抛硬币,结果是反面,那么元素2的插入操作就停止了,插入后的表结构就是上图所示。接下来,我们插入元素33,跟元素2的插入一样,现在L1层插入33,如下图:
然后抛硬币,结果是反面,那么元素33的插入操作就结束了,插入后的表结构就是上图所示。接下来,我们插入元素55,首先在L1插入55,插入后如下图:
然后抛硬币,结果是正面,那么L2层需要插入55,如下图:
继续抛硬币,结果又是正面,那么L3层需要插入55,如下图:
继续抛硬币,结果又是正面,那么要在L4插入55,结果如下图:
继续抛硬币,结果是反面,那么55的插入结束,表结构就如上图所示。
以此类推,我们插入剩余的元素。当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表。
当然,这样的分析在感性上是很直接的,但是时间复杂度的证明实在复杂,在此我就不深究了,感兴趣的可以去看关于跳跃表的paper。
再讨论删除,删除操作没什么讲的,直接删除元素,然后调整一下删除元素后的指针即可。跟普通的链表删除操作完全一样。
再来讨论一下时间复杂度,插入和删除的时间复杂度就是查询元素插入位置的时间复杂度,这不难理解,所以是O(logn)。
在章节2中,我们采用抛硬币的方式来决定新元素插入的最高层数,这当然不能在程序中实现。代码中,我们采用随机数生成的方式来获取新元素插入的最高层数。我们先估摸一下n的规模,然后定义跳跃表的最大层数maxLevel,那么底层,也就是第0层,元素是一定要插入的,概率为1;最高层,也就是maxLevel层,元素插入的概率为1/2^maxLevel。
我们先随机生成一个范围为0~2^maxLevel-1的一个整数r。那么元素r小于2^(maxLevel-1)的概率为1/2,r小于2^(maxLevel-2)的概率为1/4,……,r小于2的概率为1/2^(maxLevel-1),r小于1的概率为1/2^maxLevel。
举例,假设maxLevel为4,那么r的范围为0~15,则r小于8的概率为1/2,r小于4的概率为1/4,r小于2的概率为1/8,r小于1的概率为1/16。1/16正好是maxLevel层插入元素的概率,1/8正好是maxLevel层插入的概率,以此类推。
通过这样的分析,我们可以先比较r和1,如果r<1,那么元素就要插入到maxLevel层以下;否则再比较r和2,如果r<2,那么元素就要插入到maxLevel-1层以下;再比较r和4,如果r<4,那么元素就要插入到maxLevel-2层以下……如果r>2^(maxLevel - 1),那么元素就只要插入在底层即可。
以上分析是随机数算法的关键。算法跟实现跟语言无关,但是Java程序员还是更容易看明白Java代码实现的跳跃表,以下贴一下别人的java代码实现。作者找不到了,就这样吧。
1 /*************************** SkipList.java *********************/
2
3 import java.util.Random;
4
5 public class SkipList> {
6 private int maxLevel;
7 private SkipListNode[] root;
8 private int[] powers;
9 private Random rd = new Random();
10 SkipList() {
11 this(4);
12 }
13 SkipList(int i) {
14 maxLevel = i;
15 root = new SkipListNode[maxLevel];
16 powers = new int[maxLevel];
17 for (int j = 0; j < maxLevel; j++)
18 root[j] = null;
19 choosePowers();
20 }
21 public boolean isEmpty() {
22 return root[0] == null;
23 }
24 public void choosePowers() {
25 powers[maxLevel-1] = (2 << (maxLevel-1)) - 1; // 2^maxLevel - 1
26 for (int i = maxLevel - 2, j = 0; i >= 0; i--, j++)
27 powers[i] = powers[i+1] - (2 << j); // 2^(j+1)
28 }
29 public int chooseLevel() {
30 int i, r = Math.abs(rd.nextInt()) % powers[maxLevel-1] + 1;
31 for (i = 1; i < maxLevel; i++)
32 if (r < powers[i])
33 return i-1; // return a level < the highest level;
34 return i-1; // return the highest level;
35 }
36 // make sure (with isEmpty()) that search() is called for a nonempty list;
37 public T search(T key) {
38 int lvl;
39 SkipListNode prev, curr; // find the highest nonnull
40 for (lvl = maxLevel-1; lvl >= 0 && root[lvl] == null; lvl--); // level;
41 prev = curr = root[lvl];
42 while (true) {
43 if (key.equals(curr.key)) // success if equal;
44 return curr.key;
45 else if (key.compareTo(curr.key) < 0) { // if smaller, go down,
46 if (lvl == 0) // if possible
47 return null;
48 else if (curr == root[lvl]) // by one level
49 curr = root[--lvl]; // starting from the
50 else curr = prev.next[--lvl]; // predecessor which
51 } // can be the root;
52 else { // if greater,
53 prev = curr; // go to the next
54 if (curr.next[lvl] != null) // non-null node
55 curr = curr.next[lvl]; // on the same level
56 else { // or to a list on a lower level;
57 for (lvl--; lvl >= 0 && curr.next[lvl] == null; lvl--);
58 if (lvl >= 0)
59 curr = curr.next[lvl];
60 else return null;
61 }
62 }
63 }
64 }
65 public void insert(T key) {
66 SkipListNode[] curr = new SkipListNode[maxLevel];
67 SkipListNode[] prev = new SkipListNode[maxLevel];
68 SkipListNode newNode;
69 int lvl, i;
70 curr[maxLevel-1] = root[maxLevel-1];
71 prev[maxLevel-1] = null;
72 for (lvl = maxLevel - 1; lvl >= 0; lvl--) {
73 while (curr[lvl] != null && curr[lvl].key.compareTo(key) < 0) {
74 prev[lvl] = curr[lvl]; // go to the next
75 curr[lvl] = curr[lvl].next[lvl]; // if smaller;
76 }
77 if (curr[lvl] != null && key.equals(curr[lvl].key)) // don't
78 return; // include duplicates;
79 if (lvl > 0) // go one level down
80 if (prev[lvl] == null) { // if not the lowest
81 curr[lvl-1] = root[lvl-1]; // level, using a link
82 prev[lvl-1] = null; // either from the root
83 }
84 else { // or from the predecessor;
85 curr[lvl-1] = prev[lvl].next[lvl-1];
86 prev[lvl-1] = prev[lvl];
87 }
88 }
89 lvl = chooseLevel(); // generate randomly level
90 newNode = new SkipListNode(key,lvl+1); // for newNode;
91 for (i = 0; i <= lvl; i++) { // initialize next fields of
92 newNode.next[i] = curr[i]; // newNode and reset to newNode
93 if (prev[i] == null) // either fields of the root
94 root[i] = newNode; // or next fields of newNode's
95 else prev[i].next[i] = newNode; // predecessors;
96 }
97 }
98 }