跳跃链表又称“跳表”,是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好。在 redis的底层实现中就采用了跳跃表。
引入
在单链表中查询一个元素的时间复杂度为O(n),即使该单链表是有序的,我们也不能通过二分的方式缩减时间复杂度。 如下图:
但是如果我们在有序的单链表上加入多层索引,如下图,此时它的查找效率就类似于二分查找
访问46需要6次查询。即L4访问55,L3访问21、55,L2访问37、55,L1访问46。此时时间复杂度为O(logn)。
性质
跳表具有如下性质:
(1) 由很多层结构组成
(2) 每一层都是一个有序的链表
(3) 最底层(Level 1)的链表包含所有元素
(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。
实现原理
-
上面引入的是最理想的跳跃表,但是如果想要在上图中插入或者删除一个元素呢?比如我们要插入一个元素22、23、24……,自然在L1层,我们将这些元素插入在元素21后,那么L2层,L3层呢?我们是不是要考虑插入后怎样调整连接,才能维持这个理想的跳跃表结构。我们知道,平衡二叉树的调整是一件令人头痛的事情,左旋右旋左右旋……一般人还真记不住,而调整一个理想的跳跃表将是一个比调整平衡二叉树还复杂的操作。幸运的是,我们并不需要通过复杂的操作调整连接来维护这样完美的跳跃表。有一种基于概率统计的插入算法,也能得到时间复杂度为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次。这样,我们能在跳跃表中插入一个元素了。 当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表。
代码实现
- 定义
import java.util.Random;
public class SkipList {
// height
public int h;
// 表头
private SkipListEntry head;
// 表尾
private SkipListEntry tail;
// 生成randomLevel用到的概率值
private Random r;
public SkipList() {
//链表头节点
head = new SkipListEntry(Integer.MIN_VALUE, Integer.MIN_VALUE);
//链表尾节点
tail = new SkipListEntry(Integer.MAX_VALUE, Integer.MAX_VALUE);
head.right =tail;
tail.left = head;
h = 0;
r = new Random();
}
/**
* 跳表节点
*/
static class SkipListEntry {
Integer key;
Integer value;
SkipListEntry right;
SkipListEntry left;
SkipListEntry down;
SkipListEntry up;
public SkipListEntry(Integer key, Integer value) {
this.key = key;
this.value = value;
}
}
}
- 查找
/**
* 查找
* @param key
* @return
*/
public Integer get(int key) {
SkipListEntry p;
p = findEntry(key);
if(p.key ==key) {
return p.value;
} else {
return null;
}
}
/**
* 根据key查找结点
* @param key
* @return 返回key相等的节点或插入的前驱
*/
public SkipListEntry findEntry(Integer key)
{
SkipListEntry p;
p = head;
/**
* 基本逻辑是从head(跳表的最高层链表的头结点)开始自右开始查找,
* 当找到该层链表的最接近且小于指定key的节点时,往下开始查找,
* 最终找到最底层的那个节点
*/
while ( true )
{
while ( p.right.key != Integer.MAX_VALUE && p.right.key< key )
{
p = p.right;
}
if ( p.down != null )
{
p = p.down;
}
else
break;
}
return(p); // p.key <= key
}
- 插入
/**
* 插入
* @param key
* @param value
* @return 如果跳跃表中存在含有key值的节点,则替换新值返回旧值
*/
public Integer put(int key, int value) {
SkipListEntry p, q;
int i = 0;
// 查找适合插入的位子
p = findEntry(key);
// 如果跳跃表中存在含有key值的节点,则进行value的修改操作即可完成
if(p.key ==key) {
Integer oldValue = p.value;
p.value = value;
return oldValue;
}
// 如果跳跃表中不存在含有key值的节点,则进行新增操作
q = new SkipListEntry(key, value);
q.left = p;
q.right = p.right;
p.right.left = q;
p.right = q;
//本层操作完毕,看更高层操作
//抛硬币随机决定是否上层插入
while ( r.nextDouble() < 0.5 )
{
if ( i >= h )
{
//如果i大于最大层次,则创建新的层
addEmptyLevel();
}
//找到第一个上层索引
while ( p.up == null )
{
p = p.left;
}
p = p.up;
SkipListEntry e;
// 创建上层索引节点,这里需要注意的是除底层节点之外的节点对象是不需要value值的
e = new SkipListEntry(key, null);
e.left = p;
e.right = p.right;
e.down = q;
//把e插入到上层
p.right.left = e;
p.right = e;
q.up = e;
//把e当作新插入的节点,递归执行
q = e;
// level增加
i = i + 1;
}
return null;
}
/**
* 创建新的空索引层
*/
private void addEmptyLevel() {
SkipListEntry p1, p2;
p1 = new SkipListEntry(Integer.MIN_VALUE, null);
p2 = new SkipListEntry(Integer.MAX_VALUE, null);
p1.right = p2;
p1.down = head;
p2.left = p1;
p2.down = tail;
head.up = p1;
tail.up = p2;
head = p1;
tail = p2;
h = h + 1;
}
- 删除
/**
* 删除
* @param key
* @return 返回删除的值
*/
public Integer remove(int key) {
SkipListEntry p, q;
//查找到包含key值的节点
p = findEntry(key);
if(!p.key.equals(key)) {
return null;
}
Integer oldValue = p.value;
//将节点p从链表中移除,接着如果有更高level的节点,则重复此操作。
while(p != null) {
q = p.up;
//断链
p.left.right = p.right;
p.right.left = p.left;
//递归
p = q;
}
return oldValue;
}
参考链接:
https://blog.csdn.net/qpzkobe/article/details/80056807
https://blog.csdn.net/bohu83/article/details/83654524