Skip Lists: A Probabilistic Alternative to Balanced Trees
写博客,顺便看了一篇论文,也是挺好的。其实这篇博客我一直想写,但是怕写不好,没什么勇气。
跳跃表是一种可以用来替代平衡树的数据结构,因为它使用的是基于概率选择而不是严格平衡的方法,导致了在跳跃表中,插入和删除变得更加简单,速度明显变快。
跳跃表大概长这个样子,由图可以看出,是一种空间换时间的算法。与平衡二叉树的比较而言,在平衡二叉树的插入和删除时往往很可能需要全局的调整,而跳跃表只需要做局部的调整,查询速度都一样为O(lgn)。,在高并发下,为了线程安全,你可能需要对整个二叉树进行加锁,而跳跃表只需要加锁其中的一部分。基于上述的原因,Java并发包使用SkipList来实现Map,Set即
ConcurrentHashMap
和 ConcurrentSkipListSet
。
和HashMap和HashSet不一样,通过跳跃表生成的数据存储是有序的。
也可以看出,本质上是同时维护多个*分层链表*。
对于每个节点,可能有这样的一“operation”
- next(p): Return the position following p on the same level.同一层的下一个节点
- prev(p): Return the position preceding p on the same level. 同一层的前一个节点
- below(p): Return the position below p in the same tower.同一个柱子(tower 塔)的下一个节点
- above(p): Return the position above p in the same tower.同一个柱子(tower 塔)的上一个节点
说归说,但是实际上并没有什么用,这里将不使用这种访问方法,因为数组的下标就能实现这样的operation。
首先定义节点SkipNode,一个节点值,一个指针数组。
随机建表
下图是从论文上截取出来的初始化建表的过程,通过一层一层的建立跳跃表的结构。
首先是最低层,然后随机选择(概率因子p也可以自己设定)每个节点有1/p个概率被选择上,构建第二层,然后随机选择第二层的1/p个节点构建第三层,以此类推直到构建到你想要的高度(可以自己设定的)。
查找算法
查找一个元素,比如查找上图的元素12,顺序应该是这样的:从上往下查找,6小,向前,NIL;6向下一层,25大;向下一层,9小,向前,25大;向下一层,12查找到。返回
论文上的句子我简单翻译一下,可能不是很明白:在某一层查找一个元素,如果查找到了则直接返回,如果已经走到了最后,或者遇到的值已经超过想找的元素的大小,则向下一层继续寻找,直到找到元素或者走到了最底层不能向下但是仍然没有找到则返回false。
论文上的算法:
Search(list, searchKey)
x := list→header
-- loop invariant: x→key < searchKey
for i := list→level downto 1 do
while x→forward[i]→key < searchKey do
x := x→forward[i]
-- x→key < searchKey ≤ x→forward[1]→key
x := x→forward[1]
if x→key = searchKey then return x→value
else return failure
可以看出,算法真的是令人惊喜,也许是我认识的算法太少了,通过两层循环就找到了想要的元素,可以自己推敲一下。首先从顶层开始,内层循环,一直向前,直到数值比想要的大,然后向下一层。真是精美啊,我特喜欢。
实现起来就是这样的,这里只实现一个Set类型的,判断是否存在即可。
插入算法
插入算法,首先要考虑,插入的位置,这和查找算法实际上做的是同一个过程。然后找到你要插入位置的前置,生成一个节点,插入即可。
涉及到的关键问题有
- 查找并记录要插入位置的前一个元素
- 随机生成tower的lvl的大小,如果大于最大的高度level应该做调整。
- 查找的前一个元素和要生成元素的对接,形成链表。
此处贴出论文上的代码,对于自己的代码,将在最后全部呈现出来。
Insert(list, searchKey, newValue)
local update[1..MaxLevel]
x := list→header
for i := list→level downto 1 do
while x→forward[i]→key < searchKey do
x := x→forward[i]
-- x→key < searchKey ≤ x→forward[i]→key
update[i] := x
x := x→forward[1]
if x→key = searchKey then x→value := newValue
else
lvl := randomLevel()
if lvl > list→level then
for i := list→level + 1 to lvl do
update[i] := list→header
list→level := lvl
x := makeNode(lvl, searchKey, value)
for i := 1 to level do
x→forward[i] := update[i]→forward[i]
update[i]→forward[i] := x
很重要的重点,randomLevel()怎么做的。
说出来你可能不信:
public static int randomLevel() {
int lvl = 1;
while (lvl < MAX_LEVEL && Math.random() < P)
lvl++;
return lvl;
}
就这么一点点,其中P定义为你想选择的概率 ,可以是1/2,一般选择
public static final double P = 1 / Math.E;
一般你可能会觉得这样产生出来的结构会不会很不均匀啊之类的,实践一下就知道了,是不会的。
删除算法
删除算法也是一样的,首先找到要删除的节点,记录删除节点的前一个位置,然后向后拼接成完整的链表即可。直接看算法:
Delete(list, searchKey)
local update[1..MaxLevel]
x := list→header
for i := list→level downto 1 do
while x→forward[i]→key < searchKey do
x := x→forward[i]
update[i] := x
x := x→forward[1]
if x→key = searchKey then
for i := 1 to list→level do
if update[i]→forward[i] ≠ x then break
update[i]→forward[i] := x→forward[i]
free(x)
while list→level > 1 and
list→header→forward[list→level] = NIL do
list→level := list→level – 1
Code 代码
package SkipNode;
/**
* 跳表节点数据存储结构
*/
class SkipNode<E extends Comparable<? super E>> {
public final E value; //节点存储的数据
public final SkipNode<E>[] forward; //节点的指针数组
/**
* 根据节点的层级构造一个节点
*
* @param level 节点层级
* @param value 节点存储值
*/
@SuppressWarnings("unchecked")
public SkipNode(int level, E value) {
forward = new SkipNode[level + 1];//level层的元素后面带着level+1的指针数组
this.value = value;
}
}
public class SkipSet<E extends Comparable<? super E>> {
/**
* 概率因子,1/E
*/
// public static final double P = 0.5;
public static final double P = 1 / Math.E;
/**
* 最大层级
*/
public static final int MAX_LEVEL = 6;
/**
* 开始节点,不存值,贯穿所有层
*/
public final SkipNode<E> header = new SkipNode<E>(MAX_LEVEL, null);
/**
* 当前跳表的最高层级
*/
public int level = 0;
/**
* 插入一个元素
*
* @param value 待插入值
*/
@SuppressWarnings("unchecked")
public void insert(E value) {
SkipNode<E> x = header;
SkipNode<E>[] update = new SkipNode[MAX_LEVEL + 1];
//查找元素的位置,这里其实做了一次contain操作,注释见contain
for (int i = level; i >= 0; i--) {
while (x.forward[i] != null
&& x.forward[i].value.compareTo(value) < 0) {
x = x.forward[i];
}
//update[i]是比value小的数里面最大的,是value的前置节点
update[i] = x;
}
x = x.forward[0];
//此处不允许插入相同元素,为一个set
if (x == null || !x.value.equals(value)) {//跳表中不包含所要插的元素
//随机产生插入的层级
int lvl = randomLevel();
//产生的随机层级比当前跳表的最高层级大,需要添加相应的层级,并更新最高层级
if (lvl > level) {
for (int i = level + 1; i <= lvl; i++) {
update[i] = header;
}
level = lvl;
}
//生成新节点
x = new SkipNode<E>(lvl, value);
//调整节点的指针,和指向它的指针
for (int i = 0; i <= lvl; i++) {
x.forward[i] = update[i].forward[i];
update[i].forward[i] = x;
}
}
}
/**
* 删除一个元素
*
* @param value 待删除值
*/
@SuppressWarnings("unchecked")
public void delete(E value) {
SkipNode<E> x = header;
SkipNode<E>[] update = new SkipNode[MAX_LEVEL + 1];
//查找元素的位置,这里其实做了一次contain操作,注释见contain
for (int i = level; i >= 0; i--) {
while (x.forward[i] != null
&& x.forward[i].value.compareTo(value) < 0) {
x = x.forward[i];
}
update[i] = x;
}
x = x.forward[0];
//删除元素,调整指针
if (x.value.equals(value)) {
for (int i = 0; i <= level; i++) {
if (update[i].forward[i] != x)
break;
update[i].forward[i] = x.forward[i];
}
//如果元素为本层最后一个元素,则删除同时降低当前层级
while (level > 0 && header.forward[level] == null) {
level--;
}
}
}
/**
* 查找是否包含此元素
*
* @param searchValue 带查找值
* @return true:包含;false:不包含
*/
public boolean contains(E searchValue) {
SkipNode<E> x = header;
//从开始节点的最高层级开始查找
for (int i = level; i >= 0; i--) {
//当到达本层级的NULL节点或者遇到比查找值大的节点时,转到下一层级查找
while (x.forward[i] != null
&& x.forward[i].value.compareTo(searchValue) < 0) {
x = x.forward[i];
}
}
x = x.forward[0];
//此时x有三种可能,1.x=null,2.x.value=searchValue,3.x.value>searchValue
return x != null && x.value.equals(searchValue);
}
/**
* 这里是跳表的精髓所在,通过随机概率来判断节点的层级
*
* @return 节点的层级
*/
public static int randomLevel() {
int lvl = 1;
while (lvl < MAX_LEVEL && Math.random() < P)
lvl++;
return lvl;
}
/**
* 输出跳表的所有元素
* 遍历最底层的元素即可
*/
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{");
SkipNode<E> x = header.forward[0];
while (x != null) {
sb.append(x.value);
x = x.forward[0];
if (x != null)
sb.append(",");
}
sb.append("}");
return sb.toString();
}
}
与JDK ConcurrentSkipListSet 的运行比较
package SkipNode;
import java.util.concurrent.ConcurrentSkipListSet;
/**
* Created by bamboo on 2016/9/7.
*/
public class Main {
public static void main(String[] args) {
SkipSet<Integer> skipSet = new SkipSet<Integer>();
long startTime = System.currentTimeMillis();//获取当前时间
for (int i = 0; i < 900000; i++) {
skipSet.insert(i);
}
long endTime = System.currentTimeMillis();
System.out.println("SkipList程序运行时间:" + (endTime - startTime) + "ms");
ConcurrentSkipListSet<Integer> set = new ConcurrentSkipListSet<>();
long start = System.currentTimeMillis();//获取当前时间
for (int i = 0; i < 900000; i++) {
set.add(i);
}
long end = System.currentTimeMillis();
System.out.println("concurrentSkipList程序运行时间:" + (end - start) + "ms");
}
}
比较:
SkipNode.Main
SkipList程序运行时间:31448ms
concurrentSkipList程序运行时间:652ms
Process finished with exit code 0
不堪入目啊 我还是找个地方藏起来吧。