跳跃列表原理和实现
1.跳跃列表简介:
跳跃列表是一种随机化的数据结构,基于并联的链表,其效率可比拟于二叉查找树。基本上,跳跃列表是对有序的链表增加上附加的前进连接,增加是以随机化的方式进行的,所以在列表中的查找可以快速地跳过部分列表,因此而得名。所有操作都以对数随机化时间进行。
以上简介摘自-维基百科-跳跃列表
2.跳跃列表的产生思想
有序链表大家都熟悉,假如有一个如下的有序链表:
查找元素23,得从头结点开始依次遍历节点直到找到此节点。这样如果链表很长,而正要查找的元素位于链表比较靠后的位置,则就相当于全部遍历。
怎么能使查找变快呢?可否跳过中间的一些节点呢?
基于这样的想法,我们可以大胆假想链表如下结构:
假如在构造此链表的时候,我们构造了另一个链表,它指向原链表中的元素,只不过它中间跳过一些节点,但任然是按照原链表顺序将其中元素串起来,这样在查找过程中可以跳过一些节点。既然构造一个可以,那多个呢?但是插入,删除元素呢会有什么问题呢?基于这些疑问,接下来我们看看跳跃列表的详细构造和描述。
3.跳跃表的构造和描述
跳跃表描述:
- 一个跳跃列表由几层组成
- 底层包含所有元素
- 每一层都是一个有序链表
- 在层 i 中的元素按某个固定的概率 p (通常为0.5或0.25)出现在层 i+1 中(也就是说高层中的元素必然在低层)
- 第i层的某个元素可以向下访问与它有相同值的下层节点
如下所示:
level 4:1
level 3:1-----4---6
level 2:1---3-4---6-----9
level 1:1-2-3-4-5-6-7-8-9-10
也skiplist的cookbook:
下来说一下跳跃表的插入,删除,查询的操作
插入操作步骤:
-
1.从顶层链表开始遍历,寻找插入点。
具体:
插入元素x和当前指向的节点的值y比较:
x < y:从当前查找位置,下降一层
x > y:继续向前遍历
重复上面步骤直到结束。遍历时记录每一层的最后遍历位置,这里用update表示,也就是要插入的位置。 -
2.申请新节点,将新元素放入,随机产生一个层数。如果层数大于当前跳跃表的层数,扩大1步骤中的层记录,将新层的值修改为头节点。
-
3.将update记录的指向对应层的指针指向新的节点,新节点的各个层的指针指向后一个节点。(和普通链表的插入时调整指针是类似的,只不过这里是多层调整)
-
4.修改跳跃列表的层数
图解:
图为元素21找到插入位置后,然后将前面的红箭头都指向21,21指向23
删除操作:
- 1.从顶层链表开始遍历,寻找删除位置点。
具体:
插入元素x和当前指向的节点的值y比较:
x < y:从当前查找位置,下降一层
x > y:继续向前遍历
重复上面步骤直到结束。遍历时记录每一层的最后遍历位置,这里用update表示,也就是要删除的元素。 - 2.取出最底层的最后遍历位置的元素,与要删除的元素对比,如果相等,则此元素是要删除的元素,与插入类似,将update记录的对应层的指针指修改为删除节点上的对应层上的指针值(和普通链表的删除时调整指针是类似的,只不过这里是多层调整),释放
- 4.修改跳跃列表的层数。
4.跳跃表的实现
节点类:
public class SkipNode<K,V>{
K k;
V v;
SkipNode<K,V>[] forward;
@SuppressWarnings("unchecked")
public SkipNode(K k,V v,int level) {
this.k = k;
this.v = v;
forward = (SkipNode<K,V>[])new SkipNode[level + 1];
for(int i = 0;i < level;i++) {
forward[i] = null;
}
}
@Override
public String toString() {
return "SkipNode [k=" + k + ", v=" + v + ", forward=" + Arrays.toString(forward) + "]";
}
}
跳跃列表类:
public class SkipList<K extends Comparable<? super K>,V> {
SkipNode<K,V> head;
int level;
int size;
public SkipList(){
head = new SkipNode<K,V>(null,null,0);
//刚开始只有一层,也就是第0层
level = 0;
size = 0;
}
/**
* @Description:随机生成层数
* @return:int
*/
private int randomLevel() {
int lev;
for(lev = 1;Util.random(2) == 0;lev++);
return lev;
}
public void insert(K k,V v) {
int newLevel = randomLevel();
//调整头节点
if(newLevel > level){
SkipNode<K,V> tmp = head;
head = new SkipNode<K,V>(null,null,newLevel);
for(int i = 0;i < tmp.forward.length;i++){
head.forward[i] = tmp.forward[i];
}
level = newLevel;
}
@SuppressWarnings("unchecked")
SkipNode<K,V>[] update = new SkipNode[level + 1];
SkipNode<K,V> x = head;
for(int i = level;i >= 0;i--) {
while(x.forward[i] != null && x.forward[i].k.compareTo(k) < 0) {
x = x.forward[i];
}
update[i] = x;
}
x = x.forward[0];
if(x != null && x.k != null && x.k.compareTo(k) == 0) {
x.v = v;
} else {
x = new SkipNode<K,V>(k,v,newLevel);
for(int i = 0;i < newLevel;i++) {
x.forward[i] = update[i].forward[i];
update[i].forward[i] = x;
}
size++;
}
}
public V find(K k) {
SkipNode<K,V> x = head;
for(int i = level;i >= 0;i--) {
while(x.forward[i] != null && x.forward[i].k.compareTo(k) < 0) {
x = x.forward[i];
}
}
x = x.forward[0];
if(k.compareTo(x.k) == 0) {
return x.v;
}
return null;
}
public void delete(K k) {
@SuppressWarnings("unchecked")
SkipNode<K,V>[] update = new SkipNode[level + 1];
SkipNode<K,V> x = head;
for(int i = level;i >= 0;i--) {
while(x.forward[i] != null && x.forward[i].k.compareTo(k) < 0) {
x = x.forward[i];
}
update[i] = x;
}
x = x.forward[0];
if(k.compareTo(x.k) == 0) {
for(int i = 0;i < level;i++) {
if(update[i].forward[i] != x) {
break;
}
update[i].forward[i] = x.forward[i];
}
x = null;
while(level > 0 && head.forward[level] == null) {
level = level - 1;
}
size--;
}
}
/**
* @Description:按层输出(只输出key)
* @return:void
*/
public void printKeyByLevel() {
SkipNode<K,V> x = head;
for(int i = level - 1;i >= 0;i--) {
System.out.print("level-" + i + ":");
x = head.forward[0];
String headCurLevelForward = head.forward[i] != null?head.forward[i].k.toString() : "NULL";
System.out.print(String.format("%5s",headCurLevelForward) + " ");
while(x != null) {
if(x.forward.length <= i) {
System.out.print(String.format("%5s"," ") + " ");
} else {
if(x.forward[i] == null) {
System.out.print(String.format("%5s"," ") + " ");
} else {
System.out.print(String.format("%5s", x.forward[i].k) + " ");
}
}
x = x.forward[0];
}
System.out.println();
}
}
/**
* @Description:按节点输出(只输出key)
* @return:void
*/
public void printKeyByNode() {
SkipNode<K,V> x = head;
while(x != null) {
System.out.print(String.format("%4s forward-size-%s:",x.k == null?"head":x.k,x.forward.length) + " ");
for(int i = 0;i < x.forward.length;i++) {
if(x.forward[i] == null) {
System.out.print(String.format("%3s"," ") + " ");
} else {
System.out.print(String.format("%3s",x.forward[i].k,x.forward[i].v) + " ");
}
}
System.out.println();
x = x.forward[0];
}
}
/**
* @Description: 随机数生成
*/
static class Util {
static Random random = new Random();
public static int random(int n) {
return Math.abs(random.nextInt()) % n;
}
}
public static void main(String[] args) {
SkipList<Integer,Integer> skipList = new SkipList<Integer, Integer>();
skipList.insert(1, 1);
skipList.insert(5, 5);
skipList.insert(17, 17);
skipList.insert(19, 19);
skipList.insert(23, 23);
skipList.insert(26, 26);
skipList.insert(21, 21);
System.out.println("printKeyByLevel:");
skipList.printKeyByLevel();
System.out.println("printKeyByNode:");
skipList.printKeyByNode();
}
}
运行结果样例:
新建跳跃列表,插入元素(1,1),(5,5),(17,17),(19,19),(21,21),(23,23),(26,26)
由于是随机化的,所以每次结果都会不一样,所以这里只作为参照。
printKeyByLevel:
level-4: 17
level-3: 17
level-2: 17 23
level-1: 1 17 23
level-0: 1 5 17 19 21 23 26
printKeyByNode:
head forward-size-6: 1 1 17 17 17
1 forward-size-3: 5 17
5 forward-size-2: 17
17 forward-size-6: 19 23 23
19 forward-size-2: 21
21 forward-size-2: 23
23 forward-size-4: 26
26 forward-size-2:
以上已我看着skiplist的cookbook上的算法实现的,这里做了一点调整,就是在插入元素的时候,先随机生成层数,扩大层调整头节点后,再依次查找,寻找插入位置。也就是将扩大层后对头结点调整的动作提前。源码连接
参看资料: