首页 > 专题系列 > 算法分析 > 跳跃表(Skip List)-实现(Java)
2014
05-07

跳跃表(Skip List)-实现(Java)

跳跃表是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好。

关于跳跃表的具体介绍可以参考MIT的公开课:跳跃表

跳跃表的应用

Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。

在Java的API中已经有了实现:分别是

ConcurrentSkipListMap(在功能上对应HashTable、HashMap、TreeMap) ;

ConcurrentSkipListSet(在功能上对应HashSet). 

确切来说,SkipList更像Java中的TreeMapTreeMap基于红黑树(一种自平衡二叉查找树)实现的,时间复杂度平均能达到O(log n)。

HashMap是基于散列表实现的,时间复杂度平均能达到O(1)。ConcurrentSkipListMap是基于跳表实现的,时间复杂度平均能达到O(log n)。

Skip list的性质

(1) 由很多层结构组成,level是通过一定的概率随机产生的。
(2) 每一层都是一个有序的链表,默认是升序
(3) 最底层(Level 1)的链表包含所有元素。
(4) 如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现。
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。

Ø  ConcurrentSkipListMap具有Skip list的性质 ,并且适用于大规模数据的并发访问。多个线程可以安全地并发执行插入、移除、更新和访问操作。与其他有锁机制的数据结构在巨大的压力下相比有优势。

Ø  TreeMap插入数据时平衡树采用严格的旋转(比如平衡二叉树有左旋右旋)来保证平衡,因此Skip list比较容易实现,而且相比平衡树有着较高的运行效率。

Java代码实现:

1. SkipListEntry.java , 这是跳跃表中存储的每个元素实体类,包含 上下左右 四个指针。

package skiplist;

public class SkipListEntry {
	public String key;
	public Integer value;

	public int pos; //主要为了打印 链表用

	public SkipListEntry up, down, left, right; // 上下左右 四个指针

	public static String negInf = new String("-oo"); // 负无穷
	public static String posInf = new String("+oo"); // 正无穷

	public SkipListEntry(String k, Integer v) {
		key = k;
		value = v;

		up = down = left = right = null;
	}

	public Integer getValue() {
		return value;
	}

	public String getKey() {
		return key;
	}

	public Integer setValue(Integer val) {
		Integer oldValue = value;
		value = val;
		return oldValue;
	}

	public boolean equals(Object o) {
		SkipListEntry ent;
		try {
			ent = (SkipListEntry) o; // 检测类型
		} catch (ClassCastException ex) {
			return false;
		}
		return (ent.getKey() == key) && (ent.getValue() == value);
	}

	public String toString() {
		return "(" + key + "," + value + ")";
	}
}

2. SkipList.java, 跳跃表类,包含算法的实现。 head 和 tail 分别是 顶层的头和尾。

package skiplist;

import java.util.*;

public class SkipList {
	public SkipListEntry head; // 顶层的第一个元素
	public SkipListEntry tail; // 顶层的最后一个元素

	public int n; // 跳跃表中的元素个数

	public int h; // 跳跃表的高度
	public Random r; // 投掷硬币

	public SkipList() // 默认构造函数...
	{
		SkipListEntry p1, p2;

		p1 = new SkipListEntry(SkipListEntry.negInf, null);
		p2 = new SkipListEntry(SkipListEntry.posInf, null);

		head = p1;
		tail = p2;

		p1.right = p2;
		p2.left = p1;

		n = 0;
		h = 0;
		r = new Random();
	}

	/** 返回 包含的元素个数 */
	public int size() {
		return n;
	}

	/** 跳跃表是否为空 */
	public boolean isEmpty() {
		return (n == 0);
	}

	 //在最下面一层,找到要插入的位置前面的那个key
	public SkipListEntry findEntry(String k) {
		SkipListEntry p;
		p = head;

		while (true) {
			/**
			 * 一直向右找,例: k=34. 
			 * 10 ---> 20 ---> 30 ---> 40 ^ | p 会在30处停止
			 * --------------------------------------------
			 ***/
			while (p.right.key != SkipListEntry.posInf
					&& p.right.key.compareTo(k) <= 0) {
				p = p.right;
			//	System.out.println(">>>> " + p.key);
			}
			// 如果还有下一层,就到下一层继续查找
			if (p.down != null) {
				p = p.down;
				 //System.out.println("vvvv " + p.key);
			} else
				break; // 到了最下面一层 就停止查找
		}

		return (p); // p.key <= k
	}

	/** 返回和key绑定的值 */
	public Integer get(String k) {
		SkipListEntry p;

		p = findEntry(k);

		if (k.equals(p.getKey()))
			return (p.value);
		else
			return (null);
	}

	/** 放一个key-value到跳跃表中, 替换原有的并返回 */
	public Integer put(String k, Integer v) {
		SkipListEntry p, q;
		int i;

		p = findEntry(k);

		if (k.equals(p.getKey())) {
			Integer old = p.value;
			p.value = v;
			return (old);
		}

		q = new SkipListEntry(k, v);
		q.left = p;
		q.right = p.right;
		p.right.left = q;
		p.right = q;

		i = 0; // 当前层 level = 0

		while (r.nextDouble() < 0.5) {

			//如果超出了高度,需要重新建一个顶层
			if (i >= h) {
				SkipListEntry p1, p2;

				h = h + 1;
				p1 = new SkipListEntry(SkipListEntry.negInf, null);
				p2 = new SkipListEntry(SkipListEntry.posInf, null);

				p1.right = p2;
				p1.down = head;

				p2.left = p1;
				p2.down = tail;

				head.up = p1;
				tail.up = p2;

				head = p1;
				tail = p2;
			}

			while (p.up == null) {
				p = p.left;
			}
			p = p.up;

			SkipListEntry e;

			e = new SkipListEntry(k, null); 
			e.left = p;
			e.right = p.right;
			e.down = q;

			p.right.left = e;
			p.right = e;
			q.up = e;

			q = e; // q 进行下一层迭代
			i = i + 1; // 当前层 +1

		}
		n = n + 1;

		return (null); // No old value
	}

	public Integer remove(String key) {
		return (null);
	}

	public void printHorizontal() {
		String s = "";
		int i;
		SkipListEntry p;

		p = head;

		while (p.down != null) {
			p = p.down;
		}

		i = 0;
		while (p != null) {
			p.pos = i++;
			p = p.right;
		}

		p = head;
		while (p != null) {
			s = getOneRow(p);
			System.out.println(s);

			p = p.down;
		}
	}

	//用了打印测试
	public String getOneRow(SkipListEntry p) {
		String s;
		int a, b, i;

		a = 0;

		s = "" + p.key;
		p = p.right;

		while (p != null) {
			SkipListEntry q;

			q = p;
			while (q.down != null)
				q = q.down;
			b = q.pos;

			s = s + " <-";

			for (i = a + 1; i < b; i++)
				s = s + "--------";

			s = s + "> " + p.key;

			a = b;

			p = p.right;
		}

		return (s);
	}

	//用了打印测试
	public void printVertical() {
		String s = "";
		SkipListEntry p;
		p = head;
		while (p.down != null)
			p = p.down;

		while (p != null) {
			s = getOneColumn(p);
			System.out.println(s);

			p = p.right;
		}
	}
	//用了打印测试
	public String getOneColumn(SkipListEntry p) {
		String s = "";
		while (p != null) {
			s = s + " " + p.key;
			p = p.up;
		}
		return (s);
	}
}

测试类,Test.java

package skiplist;

public class Test1 {
	public static void main(String[] args) {
		SkipList S = new SkipList();

		S.printHorizontal();
		System.out.println("------");
		// S.printVertical();
		// System.out.println("======");

		S.put("ABC", 123);
		S.printHorizontal();
		System.out.println("------");
		// S.printVertical();
		 //System.out.println("======");

		S.put("DEF", 123);
		S.printHorizontal();
		System.out.println("------");
		// S.printVertical();
		// System.out.println("======");

		S.put("KLM", 123);
		S.printHorizontal();
		System.out.println("------");
		// S.printVertical();
		// System.out.println("======");

		S.put("HIJ", 123);
		S.printHorizontal();
		System.out.println("------");
		// S.printVertical();
		// System.out.println("======");

		S.put("GHJ", 123);
		S.printHorizontal();
		System.out.println("------");
		// S.printVertical();
		// System.out.println("======");

		S.put("AAA", 123);
		S.printHorizontal();
		System.out.println("------");
		// S.printVertical();
		// System.out.println("======");

	}
}

输出结果:

-oo <-> +oo
------
-oo <-> ABC <-> +oo
-oo <-> ABC <-> +oo
------
-oo <-> ABC <---------> +oo
-oo <-> ABC <-> DEF <-> +oo
------
-oo <-> ABC <---------> KLM <-> +oo
-oo <-> ABC <-> DEF <-> KLM <-> +oo
------
-oo <-----------------> HIJ <---------> +oo
-oo <-> ABC <---------> HIJ <-> KLM <-> +oo
-oo <-> ABC <-> DEF <-> HIJ <-> KLM <-> +oo
------
-oo <-------------------------> HIJ <---------> +oo
-oo <-> ABC <-----------------> HIJ <-> KLM <-> +oo
-oo <-> ABC <-> DEF <-> GHJ <-> HIJ <-> KLM <-> +oo
------
-oo <---------------------------------> HIJ <---------> +oo
-oo <---------> ABC <-----------------> HIJ <-> KLM <-> +oo
-oo <-> AAA <-> ABC <-> DEF <-> GHJ <-> HIJ <-> KLM <-> +oo
------

每次运行的结果是不一样的,这就是为什么说跳跃表是属于随机化数据结构。

代码参考:Implementing the skip list data structure


  1. 约瑟夫也用说这么长……很成熟的一个问题了,分治的方法解起来o(n)就可以了,有兴趣可以看看具体数学的第一章,关于约瑟夫问题推导出了一系列的结论,很漂亮

  2. 第2题,TCP不支持多播,多播和广播仅应用于UDP。所以B选项是不对的。第2题,TCP不支持多播,多播和广播仅应用于UDP。所以B选项是不对的。

  3. Gucci New Fall Arrivals

    This is really nice to know. I hope it will be successful in the future. Good job on this and keep up the good work.

  4. 第二个方法挺不错。NewHead代表新的头节点,通过递归找到最后一个节点之后,就把这个节点赋给NewHead,然后一直返回返回,中途这个值是没有变化的,一边返回一边把相应的指针方向颠倒,最后结束时返回新的头节点到主函数。