最近在网上看到redis为什么用跳表而不用平衡树,觉得好奇就看了一下。
跳表的概念网上都有,这里就不赘述了。总的来说跳表相比于平衡树实现简单,并且范围查找的效率至少跟平衡树一样。同时skiplist又无法归入哈希表、平衡树这两种用于查找的常见又高效的数据结构。所以动手实现了一个(主要是看到了实现简单)。
/**
* 此跳表用于有序存储节点并且查找指定权值节点的时间复杂度为O(logn)
*
* @author FenG
* @date 2019-10-25 19:36:28
*/
public class SkipList<T> {
// 跳表接近二分查找的查询速度,最高层为32,理论上是数据量小于等于2^32-1时效率最高
private static final int MAX_LEVEL = 32;
private final SkipNode HEAD;
private final Random random = new Random();
private int level = 0;
private int size = 0;
public SkipList() {
this(4);
}
public SkipList(int level) {
this.HEAD = new SkipNode<T>(level);
this.level = level;
}
/**
* 根据score获取节点的值
*
* @param score
* @return
*/
public T get(double score) {
SkipNode<T> node = doGet(score);
return node == null ? null : node.value;
}
/**
* 查找score在start和end间的值
*
* @param start
* @param end
* @return score在start和end间的子视图
*/
public SubList<T> range(int start, int end) {
if (end < start) {
throw new IllegalArgumentException("End should bigger than start");
}
SkipNode<T> p = HEAD;
int lev = level - 1;
boolean found = false;
while (lev >= 0 && p != null) {
if (p.level[lev].forward == null) {
lev--;
continue;
}
if (p.level[lev].forward.score < start) {
p = p.level[lev].forward;
} else {
p = p.level[lev].forward;
found = true;
break;
}
}
if (!found) {
return SubList.EMPTY_LIST;
}
int len = 0;
LinkedList<T> list = new LinkedList<T>();
while (p != null) {
if (p.score > end) {
break;
}
list.add(p.value);
p = p.level[0].forward;
}
return new SubList<T>((T[]) list.toArray());
}
/**
* 按score有序插入值为value的节点
*
* @param score
* @param value
*/
public void put(double score, T value) {
// 若不为空则存在同样权值的节点
SkipNode<T> node = null;
node = doGet(score);
if (node != null) {
// 节点不为空,说明此权值存在,改变节点的value即可
node.value = value;
} else {
// 暴力查找插入点并赋值,这部分还可以优化,也许可以不用调用doGet(double)
// 有一个简单的想法:用一个记录路径的集合,这个路径存访问过的Level实例,
// 找到插入点后将路径中的Level实例取出来再调整forward的值
// 这样可以省去一次查找时间,因为doGet(double)进行了一次查找,而插入点又进行了一次查找
int lev = randomLevel();
SkipNode<T> newNode = new SkipNode<T>(score, value, lev);
if (lev > level) {
// 提高头节点的层数
Level[] src = HEAD.level;
Level[] dest = new Level[lev];
System.arraycopy(src, 0, dest, 0, level);
while (level < lev) {
dest[level] = new Level();
dest[level].forward = newNode;
level++;
}
HEAD.level = dest;
}
SkipNode<T> p = HEAD;
lev = level - 1;
int h = newNode.level.length - 1;
while (lev >= 0) {
if (p.level[lev].forward != null && p.level[lev].forward.score < score) {
p = p.level[lev].forward;
} else {
if (lev > h) {
lev--;
continue;
}
if (p.level[lev].forward == newNode) {
// 因为新节点的层数大于之前最大层数时将新节点的引用赋给了头节点新增层的forward属性
// 在下层时会使当前层的forward属性引用newNode(while (level < lev)循环处),
// 然后插入时导致newNode.level[lev].forward引用了自己,从而出现环
// 所以需要判断p.level[lev].forward的引用是否是newNode本身
lev--;
continue;
}
newNode.level[lev].forward = p.level[lev].forward;
p.level[lev].forward = newNode;
lev--;
}
}
size++;
}
}
/**
* 根据score的值来删除节点
*
* @param score
*/
public void delete(double score) {
SkipNode p = HEAD;
int lev = level - 1;
boolean deleted = false;
while (lev >= 0 && p != null) {
if (p.level[lev].forward == null) {
lev--;
continue;
}
if (p.level[lev].forward.score == score) {
deleted = true;
// 删除下一个节点,如果该层有下一个节点,则下一个节点同样有该层
p.level[lev].forward = p.level[lev].forward.level[lev].forward;
lev--;
} else if (p.level[lev].forward.score < score) {
p = p.level[lev].forward;
} else {
lev--;
}
}
if (deleted) {
size--;
}
}
public int size() {
return size;
}
@Override
public String toString() {
if (size <= 0) {
return "SkipList{}";
}
StringBuilder sb = new StringBuilder("SkipList{\n");
SkipNode p = HEAD;
int lev = level - 1;
while (lev >= 0) {
while (p != null) {
sb.append(p.value);
sb.append(" ");
p = p.level[lev].forward;
}
sb.append("\n");
lev--;
p = HEAD;
}
sb.append("}");
return sb.toString();
}
private int randomLevel() {
int lev = 1;
while ((random.nextInt() & 1) == 0 && lev < MAX_LEVEL) {
lev++;
}
// 最小为1层
return lev;
}
private SkipNode<T> doGet(double score) {
SkipNode<T> p = HEAD;
int lev = level - 1;
while (lev >= 0 && p != null) {
if (p.score == score) {
return p;
}
if (p.level[lev].forward == null) {
// 下一个节点为空,往下找
lev--;
continue;
}
if (p.level[lev].forward.score > score) {
// 此层的下一个节点权值大于目标,往下找
lev--;
} else {
p = p.level[lev].forward;
}
}
return null;
}
private static class SubList<E> extends AbstractList<E> {
private static final SubList EMPTY_LIST = new SubList();
E[] element;
int size;
public SubList() {
}
public SubList(E[] element) {
this.element = element;
this.size = this.element.length;
}
@Override
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalStateException("Array index is out of range: " + index);
}
return element[index];
}
@Override
public int size() {
return size;
}
@Override
public String toString() {
if (size <= 0) {
return "SubList{}";
}
StringBuilder sb = new StringBuilder("SubList{");
for (E e : element) {
sb.append(e);
sb.append(", ");
}
sb.delete(sb.length() - 2, sb.length());
sb.append("}");
return sb.toString();
}
}
private class SkipNode<E> {
Level[] level;
double score;
E value;
SkipNode<E> backward;
SkipNode(int lev) {
initLevel(lev);
this.score = Double.MIN_VALUE;
}
SkipNode(double score, E value, int lev) {
initLevel(lev);
this.value = value;
this.score = score;
}
private void initLevel(int lev) {
level = new Level[lev];
for (int i = 0; i < lev; i++) {
level[i] = new Level();
}
}
}
private class Level<E> {
SkipNode<E> forward;
// 跨度
// int span;
}
}
网上看到了很多实现跳表的例子,但都是用down来表示下层,这样需要很多实例节点来表示同一个数据,造成了更大的空间浪费,所以自己用数组实现了层,redis源码中还有span属性,但是没想到怎么实现跨度,所以暂时没用到这个属性。然后就是插入还能优化,但懒得写了。。。
/**
* 添加了虚拟机参数:
* -XX:AutoBoxCacheMax=1000000 -Xmx1g -Xms1g
* 为了节省创建Integer的时间
*/
@Test
public void skipListCompareToLinkedList() {
Random random = new Random();
SkipList<Integer> skipList = new SkipList<Integer>();
LinkedList<Integer> linkedList = new LinkedList<Integer>();
// 加载IntegerCache类,使其开始缓存
Integer.valueOf(1);
System.out.println("-------------测试添加一百万个元素的构造性能-------------");
long start = System.currentTimeMillis();
for(int i = 0; i < 1000000; i++) {
skipList.put(i, i);
}
long end = System.currentTimeMillis();
System.out.println("创建skipList的时间: " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
linkedList.add(i);
}
end = System.currentTimeMillis();
System.out.println("创建linkedList的时间: " + (end - start));
System.out.println("-------------测试一万次查找性能-------------");
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
skipList.get(random.nextInt(1000000));
}
end = System.currentTimeMillis();
System.out.println("skipList: " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
linkedList.get(random.nextInt(1000000));
}
end = System.currentTimeMillis();
System.out.println("linkedList: " + (end - start));
}
虚拟机的三个参数分别是
第一个参数是Integer自动装箱缓存的最大值,为了在测试时不反复创建Integer,所以在开始前先将Integer创建并缓存好。
第二个和第三个参数是指定java堆的大小,之前没指定发现空间申请失败了几次,然后执行fullGC导致LinkedList的创建甚至比SkipList更慢,所以调节了堆的大小。
-------------测试添加一百万个元素的构造性能-------------
创建skipList的时间: 545
创建linkedList的时间: 25
-------------测试一万次查找性能-------------
skipList: 19
linkedList: 7175
可以看到SkipList的插入比LinkedList耗时,但在查找上快了不止一点点,插入所消耗的时间完全可以接受。