目录
- 概述
- 代码实现
- 常见面试题
- 参考文章
概述
本文主要讲解跳表的原理、代码实现以及常见的面试题。
跳表本质上是一种查找结构,相比于平衡树,不仅实现简单,而且插入、删除、查找的时间复杂度均为O(logN)。跳表其实就是链表,只是对有序的链表增加上附加的前进链接(增加是以随机化的方式进行的),所以在列表中的查找可以快速的跳过部分列表从而快速检索。
由于跳表在Redis的使用,导致面试中经常会被提及,所以深入了解跳表的实现非常必要。
代码实现
第一种实现方法
这种实现方法构建的跳表整体如下图所示:
这种实现方式中的SkipNode定义如下所示。
private class SkipNode<K, V> { private K key; private V value; private SkipNode<K, V>[] forward; public SkipNode( K key, V value, int levels ) { this.key = key; this.value = value; this.forward = (SkipNode<K, V>[]) new SkipNode[levels+1]; for (int i = 0; i <= levels; i++) this.forward[i] = null; } public K key() { return this.key; } public V value() { return this.value; } ······ }
SkipNode定义中需要留意就是forward数组,这个数组的大小是随机的,大小代表这个节点的高度,数组中每个元素代表了这一层的下一个SkipNode。
下面来看insert的代码实现。
/** * 将新值插入到链表中 * @param k * @param newValue */ public void insert(K k, V newValue) { int newLevel = randomLevel(); // 如果随机的层大于现在的最大层, 进行层调整 if (newLevel > level) adjustHead(newLevel); this.level = newLevel; SkipNode<K, V>[] update = (SkipNode<K, V>[]) new SkipNode[level+1]; SkipNode<K, V> x = this.head; // 找寻每一层的插入位置 for (int i=level; i>=0; i--) { while((x.forward[i] != null) && ((k.compareTo(x.forward[i].key())) > 0)) x = x.forward[i]; update[i] = x; } // 创建新节点 x = new SkipNode<K, V>(k, newValue, newLevel); // 类似于链表插入 for (int i=0; i <= newLevel; i++) { x.forward[i] = update[i].forward[i]; update[i].forward[i] = x; } this.size++; }
从上面的代码中可以看出insert主要有几个步骤:
首先随机产生层数,创建新节点
每层遍历,得到新节点在每层插入的前一个节点
逐层插入新节点(类似于链表插入)
下面来看find的代码实现。
public V find(K searchKey) { SkipNode<K, V> x = this.head; // 类似于一个下楼梯的过程 for (int i=level; i>=0; i--) while ((x.forward[i] != null) && (searchKey.compareTo(x.forward[i].key()) > 0)) x = x.forward[i]; x = x.forward[0]; if ((x != null) && (searchKey == x.key())) return x.value(); else return null; }
find的过程比较简单,类似于生活中下楼梯,具体过程见上图中的红线所示。
第二种实现方法
这种方式最终创建的跳表如下所示。
SkipListEntry的定义如下。
public class SkipListEntry<K extends Comparable, V> { public K key; public V value; public int pos; public SkipListEntry up, down, left, right; // 构造函数 public SkipListEntry(K k, V v) { key = k; value = v; up = down = left = right = null; } }
skipList的初始化操作。 定义了头和尾节点,并且把它们相连接。
public SkipList(){ SkipListEntry p1, p2; p1 = new SkipListEntry<K, V>(null, null); p2 = new SkipListEntry<K, V>(null, null); head = p1; tail = p2; p1.right = p2; p2.left = p1; n = 0; h = 0; r = new Random(); }
查找操作如下所示。查找操作依然类似于下楼梯。
public SkipListEntry<K, V> findEntry(K k) { SkipListEntry<K, V> p = head; while ( true ) { //首先向右走 while ( p.right.key != null && p.right.key.compareTo(k) <= 0 ) { p = p.right; } // 向下走 if ( p.down != null ) { p = p.down; } else break; } return(p); }
添加节点的实现如下。
public Object put (K k, V v) { SkipListEntry p, q; int i; // 待插入的前一个位置 p = findEntry(k); if ( k.equals( p.getKey())) { Object old = p.getValue(); 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; // Current level = 0 // 随机插入 while ( r.nextDouble() < 0.5 ) { // 当前插入的是第i层 if ( i >= h ) { SkipListEntry p1, p2; h = h + 1; p1 = new SkipListEntry(null,null); p2 = new SkipListEntry(null,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 = 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; i = i + 1; } n = n + 1; return null; }
和第一种实现方式不同的是,第二种实现方法的插入操作在每一层都需要重新创建节点进行插入,空间浪费。所以推荐第一种实现方法。
常见面试题
redis为什么使用跳表,为什么不用红黑树
相比于红黑树、平衡二叉树,跳表不仅查找、插入、删除时间复杂度都是O(logN),并且实现简单很多。
跳表数据结构的实现
可以参考第一种实现方法。
参考文章
http://www.mathcs.emory.edu/~cheung/Courses/323/Syllabus/Map/skip-list-impl.html
https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html
喜欢文章的可以关注公众号“面经详解”,里面很多常见面试题讲解。