最初知道跳表(Skip List)是在看redis原理的时候,redis中的有序集合使用了跳表做为数据结构。接着就查了一些资料,来学习一下跳表。后面会使用java代码来实现跳表。java
跳表由William Pugh发明。他在论文《Skip lists: a probabilistic alternative to balanced trees》中详细介绍了跳表的数据结构和插入删除等操做。论文是这么介绍跳表的:node
Skip lists are a data structure that can be used in place of balanced trees.
Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees.redis
也就是说,跳表能够用来替代红黑树,使用几率均衡技术,插入、删除操做更简单、更快。算法
做者在文章中由链表的查找一步步引入到跳表的介绍。首先来看论文里的一张图:数组
上图a,已排好序的链表,查找一个结点最多须要比较N
个结点。数据结构
上图b,每隔2个结点增长一个指针,指向该结点间距为2的后续结点,那么查找一个结点最多须要比较ceil(N/2)+1
个结点。dom
上图c,每隔4个结点增长一个指针,指向该结点间距为4的后续结点,那么查找一个结点最多须要比较ceil(N/4)+1
个结点。函数
若是每第2^i
个结点都有一个指向间距为2^i的后续结点的指针,这样不断增长指针,比较次数会降为log(N)
。这样的话,搜索会很快,但插入和删除会很困难。学习
一个拥有k个指针的结点称为一个k层结点(level k node)。按照上面的逻辑,50%的结点为1层,25%的结点为2层,12.5%的结点为3层...若是每一个结点的层数随机选取,但仍服从这样的分布呢(上图e,对比上图d)?ui
使一个k层结点的第i个指针指向第i层的下一个结点,而不是它后面的第2^(i-1)个结点,那么结点的插入和删除只须要原地修改操做;一个结点的层数,是在它被插入的时候随机选取的,而且永不改变。由于这样的数据结构是基于链表的,而且额外的指针会跳过中间结点,因此做者称之为跳表(Skip Lists)。
原始论文使用伪代码的形式来描述算法。本文以java语言来描述算法。
首先定义一下须要用的数据结构。表中的元素使用结点来表示,结点的层数在它被插入时随机计算决定(与表中已有结点数目无关)。一个i层的结点有i个前向指针(java中使用结点对象数组forward来表示),索引为从1到i。用MaxLevel来记录跳表的最大层数。跳表的层数为当前全部结点中的最大层数(若是list为空,则层数为1)。列表头header拥有从1到MaxLevel的前向指针:
public class SkipList<T> { // 最高层数 private final int MAX_LEVEL; // 当前层数 private int listLevel; // 表头 private SkipListNode<T> listHead; // 表尾 private SkipListNode<T> NIL; // 生成randomLevel用到的几率值 private final double P; // 论文里给出的最佳几率值 private static final double OPTIMAL_P = 0.25; public SkipList() { // 0.25, 15 this(OPTIMAL_P, (int)Math.ceil(Math.log(Integer.MAX_VALUE) / Math.log(1 / OPTIMAL_P)) - 1); } public SkipList(double probability, int maxLevel) { P = probability; MAX_LEVEL = maxLevel; listLevel = 1; listHead = new SkipListNode<T>(Integer.MIN_VALUE, null, maxLevel); NIL = new SkipListNode<T>(Integer.MAX_VALUE, null, maxLevel); for (int i = listHead.forward.length - 1; i >= 0; i--) { listHead.forward[i] = NIL; } } // 内部类 class SkipListNode<T> { int key; T value; SkipListNode[] forward; public SkipListNode(int key, T value, int level) { this.key = key; this.value = value; this.forward = new SkipListNode[level]; } } }
按key搜索,找到返回该key对应的value,未找到则返回null。
经过遍历forward数组来需找特定的searchKey。假设skip list的key按照从小到大的顺序排列,那么从跳表的当前最高层listLevel开始寻找searchKey。在某一层找到一个非小于searchKey的结点后,跳到下一层继续找,直到最底层为止。那么根据最后搜索中止位置的下一个结点,就能够判断searchKey在不在跳表中。
下图为在跳表中找8的过程:
插入的删除的方法类似,都是经过查找与链接(search and splice),以下图:
维护一个update数组,在搜索结束以后,update[i]保存的是待插入/删除结点在第i层的左侧结点。
若key不存在,则插入该key与对应的value;若key存在,则更新value。
若是待插入的结点的层数高于跳表的当前层数listLevel,则更新listLevel。
选择待插入结点的层数randomLevel:
randomLevel只依赖于跳表的最高层数和几率值p。算法在后面的代码中。
另外一种实现方法为,若是生成的randomLevel大于当前跳表的层数listLevel,那么将randomLevel设置为listLevel+1,这样方便之后的查找,在工程上是能够接受的,但同时也破坏了算法的随机性。
删除特定的key与对应的value。
若是待删除的结点为跳表中层数最高的结点,那么删除以后,要更新listLevel。
参考了网上的一些代码,用java写了一版,包含main函数,可运行。
public class SkipList<T> { // 最高层数 private final int MAX_LEVEL; // 当前层数 private int listLevel; // 表头 private SkipListNode<T> listHead; // 表尾 private SkipListNode<T> NIL; // 生成randomLevel用到的几率值 private final double P; // 论文里给出的最佳几率值 private static final double OPTIMAL_P = 0.25; public SkipList() { // 0.25, 15 this(OPTIMAL_P, (int)Math.ceil(Math.log(Integer.MAX_VALUE) / Math.log(1 / OPTIMAL_P)) - 1); } public SkipList(double probability, int maxLevel) { P = probability; MAX_LEVEL = maxLevel; listLevel = 1; listHead = new SkipListNode<T>(Integer.MIN_VALUE, null, maxLevel); NIL = new SkipListNode<T>(Integer.MAX_VALUE, null, maxLevel); for (int i = listHead.forward.length - 1; i >= 0; i--) { listHead.forward[i] = NIL; } } // 内部类 class SkipListNode<T> { int key; T value; SkipListNode[] forward; public SkipListNode(int key, T value, int level) { this.key = key; this.value = value; this.forward = new SkipListNode[level]; } } public T search(int searchKey) { SkipListNode<T> curNode = listHead; for (int i = listLevel; i > 0; i--) { while (curNode.forward[i].key < searchKey) { curNode = curNode.forward[i]; } } if (curNode.key == searchKey) { return curNode.value; } else { return null; } } public void insert(int searchKey, T newValue) { SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL]; SkipListNode<T> curNode = listHead; for (int i = listLevel - 1; i >= 0; i--) { while (curNode.forward[i].key < searchKey) { curNode = curNode.forward[i]; } // curNode.key < searchKey <= curNode.forward[i].key update[i] = curNode; } curNode = curNode.forward[0]; if (curNode.key == searchKey) { curNode.value = newValue; } else { int lvl = randomLevel(); if (listLevel < lvl) { for (int i = listLevel; i < lvl; i++) { update[i] = listHead; } listLevel = lvl; } SkipListNode<T> newNode = new SkipListNode<T>(searchKey, newValue, lvl); for (int i = 0; i < lvl; i++) { newNode.forward[i] = update[i].forward[i]; update[i].forward[i] = newNode; } } } public void delete(int searchKey) { SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL]; SkipListNode<T> curNode = listHead; for (int i = listLevel - 1; i >= 0; i--) { while (curNode.forward[i].key < searchKey) { curNode = curNode.forward[i]; } // curNode.key < searchKey <= curNode.forward[i].key update[i] = curNode; } curNode = curNode.forward[0]; if (curNode.key == searchKey) { for (int i = 0; i < listLevel; i++) { if (update[i].forward[i] != curNode) { break; } update[i].forward[i] = curNode.forward[i]; } while (listLevel > 0 && listHead.forward[listLevel - 1] == NIL) { listLevel--; } } } private int randomLevel() { int lvl = 1; while (lvl < MAX_LEVEL && Math.random() < P) { lvl++; } return lvl; } public void print() { for (int i = listLevel - 1; i >= 0; i--) { SkipListNode<T> curNode = listHead.forward[i]; while (curNode != NIL) { System.out.print(curNode.key + "->"); curNode = curNode.forward[i]; } System.out.println("NIL"); } } public static void main(String[] args) { SkipList<Integer> sl = new SkipList<Integer>(); sl.insert(20, 20); sl.insert(5, 5); sl.insert(10, 10); sl.insert(1, 1); sl.insert(100, 100); sl.insert(80, 80); sl.insert(60, 60); sl.insert(30, 30); sl.print(); System.out.println("---"); sl.delete(20); sl.delete(100); sl.print(); } }