字典哈希表的实现原理_跳跃表(Skip list)原理与java实现

Skip list是一个用于有序元素序列快速搜索的数据结构,由美国计算机科学家William Pugh发明于1989年。它的效率和红黑树以及 AVL 树不相上下,但实现起来比较容易。作者William Pugh是这样介绍Skip list的:

Skip lists are a probabilistic data structure that seem likely to supplant balanced trees as the implementation method of choice for many applications. Skip list algorithms have the same asymptotic expected time bounds as balanced trees and are simpler, faster and use less space.

Skip list是一个“概率型”的数据结构,可以在很多应用场景中替代平衡树。Skip list算法与平衡树相比,有相似的渐进期望时间边界,但是它更简单,更快,使用更少的空间。

Skip list是一个分层结构多级链表,最下层是原始的链表,每个层级都是下一个层级的“高速跑道”。

972fb8a6907165f2105f48ddbc402cec.png

跳跃的表的性质包括:

某个i层的元素,出现在i+1层的概率p是固定的,例如常取p=1/2或p=1/4;

平均来讲,每个元素出现在1/(1-p)个链表中;

最高的元素,例如head通常采用Int.MIN_VALUE作为的最小值,会出现在每一层链表中;

原理分析:

我们知道,普通单链表查询一个元素的时间复杂度为O(n),即使该单链表是有序的,我们也不能通过2分的方式缩减时间复杂度。

9ae27df729a917029e0a169163a7b022.png

如上图,我们要查询元素为55的结点,必须从头结点,循环遍历到最后一个节点,不算-INF(负无穷)一共查询8次。那么用什么办法能够用更少的次数访问55呢?最直观的,当然是新开辟一条捷径去访问55。

6c61c9dd22080bf94d828a933ff52cda.png

如上图,我们要查询元素为55的结点,只需要在L2层查找4次即可。在这个结构中,查询结点为46的元素将耗费最多的查询次数5次。即先在L2查询46,查询4次后找到元素55,因为链表是有序的,46一定在55的左边,所以L2层没有元素46。然后我们退回到元素37,到它的下一层即L1层继续搜索46。非常幸运,我们只需要再查询1次就能找到46。这样一共耗费5次查询。

那么,如何才能更快的搜寻55呢?有了上面的经验,我们就很容易想到,再开辟一条捷径。

b99d06926f883fbce8acfcd035cedc0d.png

如上图,我们搜索55只需要2次查找即可。这个结构中,查询元素46仍然是最耗时的,需要查询5次。即首先在L3层查找2次,然后在L2层查找2次,最后在L1层查找1次,共5次。很显然,这种思想和2分非常相似,那么我们最后的结构图就应该如下图。

cdcb5e77d1f9024993d0343a788f6a0d.png

我们可以看到,最耗时的访问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次。这样,我们能在跳跃表中插入一个元素了。

在此还是以上图为例:跳跃表的初试状态如下图,表中没有一个元素:

05f6ab2468501841fa75727bd1531ae3.png

如果我们要插入元素2,首先是在底部插入元素2,如下图:

980d3453d467fba1c7b8c156c586ce2b.png

然后我们抛硬币,结果是正面,那么我们要将2插入到L2层,如下图

28c215b173161228b13eb2147ecfe540.png

继续抛硬币,结果是反面,那么元素2的插入操作就停止了,插入后的表结构就是上图所示。接下来,我们插入元素33,跟元素2的插入一样,现在L1层插入33,如下图:

8925832e7d92c29e7121716b2b044e9a.png

然后抛硬币,结果是反面,那么元素33的插入操作就结束了,插入后的表结构就是上图所示。接下来,我们插入元素55,首先在L1插入55,插入后如下图:

d7ea260ae7ea7264073cfc8894bc312f.png

然后抛硬币,结果是正面,那么L2层需要插入55,如下图:

b04b4765db3e24ebc7a4c88f52e96a29.png

继续抛硬币,结果又是正面,那么L3层需要插入55,如下图:

71d03434375203ba88cd0b98b08acce6.png

继续抛硬币,结果又是正面,那么要在L4插入55,结果如下图:

fa7a1a38261140a9ec733884fd391038.png

继续抛硬币,结果是反面,那么55的插入结束,表结构就如上图所示。

以此类推,我们插入剩余的元素。当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表。

当然,这样的分析在感性上是很直接的,但是时间复杂度的证明实在复杂,在此我就不深究了,感兴趣的可以去看关于跳跃表的paper。

再讨论删除,删除操作没什么讲的,直接删除元素,然后调整一下删除元素后的指针即可。跟普通的链表删除操作完全一样。

再来讨论一下时间复杂度,插入和删除的时间复杂度就是查询元素插入位置的时间复杂度,这不难理解,所以是O(logn)。

Java实现

节点类

/**

* 跳跃表的节点,包括key-value和上下左右4个指针

* created by 曹艳丰,2016-08-14

* 参考:http://www.acmerblog.com/skip-list-impl-java-5773.html

* */

public class SkipListNode {

public int key;

public T value;

public SkipListNode up, down, left, right; // 上下左右 四个指针

public static final int HEAD_KEY = Integer.MIN_VALUE; // 负无穷

public static final int TAIL_KEY = Integer.MAX_VALUE; // 正无穷

public SkipListNode(int k,T v) {

// TODO Auto-generated constructor stub

key = k;

value = v;

}

public int getKey() {

return key;

}

public void setKey(int key) {

this.key = key;

}

public T getValue() {

return value;

}

public void setValue(T value) {

this.value = value;

}

public boolean equals(Object o) {

if (this==o) {

return true;

}

if (o==null) {

return false;

}

if (!(o instanceof SkipListNode>)) {

return false;

}

SkipListNode ent;

try {

ent = (SkipListNode) o; // 检测类型

} catch (ClassCastException ex) {

return false;

}

return (ent.getKey() == key) && (ent.getValue() == value);

}

@Override

public String toString() {

// TODO Auto-generated method stub

return "key-value:"+key+"-"+value;

}

}

跳跃表实现

import java.util.Random;

/**

* 不固定层级的跳跃表

* created by 曹艳丰,2016-08-14

* 参考:http://www.acmerblog.com/skip-list-impl-java-5773.html

* */

public class SkipList {

private SkipListNode head,tail;

private int nodes;//节点总数

private int listLevel;//层数

private Random random;// 用于投掷硬币

private static final double PROBABILITY=0.5;//向上提升一个的概率

public SkipList() {

// TODO Auto-generated constructor stub

random=new Random();

clear();

}

/**

*清空跳跃表

* */

public void clear(){

head=new SkipListNode(SkipListNode.HEAD_KEY, null);

tail=new SkipListNode(SkipListNode.TAIL_KEY, null);

horizontalLink(head, tail);

listLevel=0;

nodes=0;

}

public boolean isEmpty(){

return nodes==0;

}

public int size() {

return nodes;

}

/**

* 在最下面一层,找到要插入的位置前面的那个key

* */

private SkipListNode findNode(int key){

SkipListNode p=head;

while(true){

while (p.right.key!=SkipListNode.TAIL_KEY&&p.right.key<=key) {

p=p.right;

}

if (p.down!=null) {

p=p.down;

}else {

break;

}

}

return p;

}

/**

* 查找是否存储key,存在则返回该节点,否则返回null

* */

public SkipListNode search(int key){

SkipListNode p=findNode(key);

if (key==p.getKey()) {

return p;

}else {

return null;

}

}

/**

* 向跳跃表中添加key-value

*

* */

public void put(int k,T v){

SkipListNode p=findNode(k);

//如果key值相同,替换原来的vaule即可结束

if (k==p.getKey()) {

p.value=v;

return;

}

SkipListNode q=new SkipListNode(k, v);

backLink(p, q);

int currentLevel=0;//当前所在的层级是0

//抛硬币

while (random.nextDouble()

//如果超出了高度,需要重新建一个顶层

if (currentLevel>=listLevel) {

listLevel++;

SkipListNode p1=new SkipListNode(SkipListNode.HEAD_KEY, null);

SkipListNode p2=new SkipListNode(SkipListNode.TAIL_KEY, null);

horizontalLink(p1, p2);

vertiacallLink(p1, head);

vertiacallLink(p2, tail);

head=p1;

tail=p2;

}

//将p移动到上一层

while (p.up==null) {

p=p.left;

}

p=p.up;

SkipListNode e=new SkipListNode(k, null);//只保存key就ok

backLink(p, e);//将e插入到p的后面

vertiacallLink(e, q);//将e和q上下连接

q=e;

currentLevel++;

}

nodes++;//层数递增

}

//node1后面插入node2

private void backLink(SkipListNode node1,SkipListNode node2){

node2.left=node1;

node2.right=node1.right;

node1.right.left=node2;

node1.right=node2;

}

/**

* 水平双向连接

* */

private void horizontalLink(SkipListNode node1,SkipListNode node2){

node1.right=node2;

node2.left=node1;

}

/**

* 垂直双向连接

* */

private void vertiacallLink(SkipListNode node1,SkipListNode node2){

node1.down=node2;

node2.up=node1;

}

/**

* 打印出原始数据

* */

@Override

public String toString() {

// TODO Auto-generated method stub

if (isEmpty()) {

return "跳跃表为空!";

}

StringBuilder builder=new StringBuilder();

SkipListNode p=head;

while (p.down!=null) {

p=p.down;

}

while (p.left!=null) {

p=p.left;

}

if (p.right!=null) {

p=p.right;

}

while (p.right!=null) {

builder.append(p);

builder.append("");

p=p.right;

}

return builder.toString();

}

}

测试

public class Main { public static void main(String[] args) { // TODO Auto-generated method stub SkipList list=new SkipList(); System.out.println(list); list.put(2, "yang"); list.put(1, "guo"); list.put(3, "chen"); list.put(1, "cao");//测试同一个key值 list.put(4, "张"); list.put(6, "三"); list.put(5, "风"); System.out.println(list); System.out.println(list.size()); }}

Java中的跳跃表

Java API中提供了支持并发操作的跳跃表ConcurrentSkipListSet和ConcurrentSkipListMap。下面摘录”Java多线程(四)之ConcurrentSkipListMap深入分析“中的一些结论。

有序的情况下:

  • 在非多线程的情况下,应当尽量使用TreeMap(红黑树实现)。
  • 对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。但是对于高并发程序,应当使用ConcurrentSkipListMap。

无序情况下:

  • 并发程度低,数据量大时,ConcurrentHashMap 存取远大于ConcurrentSkipListMap。
  • 数据量一定,并发程度高时,ConcurrentSkipListMap比ConcurrentHashMap效率更高。

————————————————

本文件借鉴:

https://blog.csdn.net/BrilliantEagle/article/details/52206261

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值