跳表(Skip list)
可以实现对数据快速的删除,插入,查找。
如何理解“跳表”?
对于有序的单链表,其查找也需要遍历所有元素,时间复杂度为O(n)。
如果每2个结点建立一个指针作为索引。那么第一层索引的结点数为:n/2。
2层:n/4。 h层:n/(2h)。假设h层索引,最高结点为2。那么n/(2h)=2,h=log2n-1
如果包含原始链表这一层,整个跳表的高度就是 log2n。我们在跳表中查询某个数据的时候,如果每一层都要遍历 m 个结点,那在跳表中查询一个数据的时间复杂度就是 O(m*logn)。m为前后项和指向下面一层的时间:3,那么跳表的查询时间复杂度为:O(logn)。
跳表是否浪费空间呢?
索引的结点数为:n/2+n/4+n/8+...+4+2 = n-2 ,所以空间复杂度为O(n)。
对于一个大的对象来说,这些存的指针的空间根本不算什么。
怎么降低空间消耗呢?
可以没3或者5个结点在索引层存1个结点。
跳表高效的动态插入和删除
由于删除和插入的时间复杂度为O(1),那么只需要计算查找的复杂度,
所以删除和插入的时间也是复杂度:O(logn)
跳表索引动态更新
当我们不停的在2个结点之间插入数据,而不更新索引层的话,最差的情况下查找的复杂度退化为:O(n2)。
要解决这个问题可以在插入数据的时候随机的在索引层更新插入索引。
为什么 Redis 要用跳表来实现有序集合,而不是红黑树?
Redis 中的有序集合支持的核心操作主要有下面这几个:
1、插入一个数据;
2、删除一个数据;
3、查找一个数据;
4、按照区间查找数据(比如查找值在[100, 356]之间的数据);
5、迭代输出有序序列。
其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
Java实现跳表
import java.util.Random;
public class SkipList {
Node head ;// 顶层的头
Node last ; // 顶层的尾
Integer size;// 真正元素的的大小
Integer high;// 层高
Random r = new Random();// 随机数 private static class Node {
Integer key;// 键
Object value;// 值
Node left;
Node right;
Node up;
Node down; public Node(Integer key, Object value) {// 带参数构造函数
this.key = key;
this.value = value;
} @Override
public String toString() {
return "Node{" +
"key=" + key +
", value=" + value +
'}';
}
} public SkipList() {
this.head = new Node(Integer.MIN_VALUE,null);
this.last = new Node(Integer.MAX_VALUE,null); this.head.right = last;
this.last.left = head;
this.size = 0;
this.high = 0;
} private Node find(Integer key) {// 根据key查找<= key的Node。
Node temp = head;
while (true) {
// 从第一个开始,往右边找,直到大于key的索引就停下
while (temp.right.key <= key) {
temp = temp.right;
} // 往下一层索引查找,直到down等于null
if (temp.down == null) {
return temp;
} else {
temp = temp.down;
}
}
} // 根据key查找value
public Object get(Integer key) {
Node temp = find(key);
if (key == temp.key) {// 如果key存在,返回value
return temp.value;
} else {
return null;
}
} // 插入元素,如果key存在就更新
public void put (Integer key, Object value){
Node temp = find(key); // 如果key存在,更新value
if (key == temp.key){
temp.value = value;
} // key不存在,插入元素
Node now = new Node(key,value);
now.right = temp.right;
temp.right.left = now;
now.left = temp;
temp.right = now;
size += 1; // 随机增加now的索引
Integer level = 1;// 当前索引层,从下往上数,存放数据层为0
while (r.nextDouble() > 0.5){
// 如果当前索引为最大层,索引增加一层
if (level > high){
addLevel();
} Node up = new Node(key, null);
up.down = now; // 给up的left索引赋值
temp = now.left;
while (temp.up == null){
temp = temp.left;
}
up.left = temp.up; // 给up的right索引赋值
temp = now.right;
while (temp.up == null){
temp = temp.right;
}
up.right = temp.up;
}
} public void remove(Integer key){
Node temp = find(key);
if (temp.key == key){// 如果key存在,则删除元素
size --;
while (temp != null){// 循环删除当前元素和索引
temp.right.left = temp.left;
temp.left.right = temp.right;
temp = temp.up;
}
}
} private void addLevel() {// 添加空索引层,用于添加索引(哨兵模式)。
Node newHead = new Node(Integer.MIN_VALUE,null);
head.up = newHead;
newHead.down = head;
head = newHead; Node newLast = new Node(Integer.MAX_VALUE,null);
last.up = newLast ;
newLast.down = last; newLast.left = head;
head.right = newLast;
high ++;
} @Override
public String toString() {
return "SkipList{" +
"head=" + head +
", last=" + last +
", size=" + size +
", high=" + high +
", r=" + r +
'}';
}
}