参考内容
[1] 📑 论文:https://epaperpress.com/sortsearch/download/skiplist.pdf
[2] 📙 漫画:https://www.jianshu.com/p/dc252b5efca6
[3] 🚀 博客:https://logi.im/algorithm/skiplist.html
[4] 💤 知乎:https://zhuanlan.zhihu.com/p/107835889
跳表概述
SkipList(跳跃表)来源于William Pugh于1990年发表的Skip Lists: A Probabilistic Alternative toBalanced Trees。
- 跳跃表是由多层有序链表组合而成的,最底一层的链表保存了所有的数据,每向上的一层链表依次保存了下一层链表的部分数据。
- 相邻的两层链表中元素相同的节点之间存在引用关系,一般是上层节点中存在一个指向下层节点的引用。
- 跳跃表的目的在于提高了查询效率,同时也牺牲了一定的存储空间。
设计初衷:在某些场景下替代红黑树。
对比二叉查找树:维持结构平衡的成本比较低,完全依靠随机,并发性更强。而二叉查找树在多次插入删除后,需要rebalance来重新调整结构平衡。
算法简介
插入过程
方向:自底向上。
根据指定的随机Level向上层添加节点。
每一个节点的层数(level)是随机出来的,而且新插入一个节点不会影响其他节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。
查找过程
方向:自顶向下,从左向右。
先在最上一层链表中从左向右查找,然后依次向下查找,直到最下一层的某个节点结束。
复杂度分析
假设总结点数为n,每2个节点间构建一个索引,少于2个节点的层无意义
层次 | 节点数 |
---|---|
top | 2 |
… | … |
k | n / 2k |
… | … |
2 | n / 22 |
1 | n / 2 |
0 | n |
加上最后一层,则总层数为,1 ~ top层需要占用额外空间,节点数总和为
所以跳表的空间复杂度为O(n)
。
时间复杂度
插入和删除依赖查找,调整指针的时间复杂度为O(1)
。
如果像上面那样建立最密索引,每层只需一次比较,因为层数为,所以最好时间复杂度为
O(logn)
。
不建立索引退化为链表,最差时间复杂度为O(n)
。
Java实现
/**
* @author KHighness
* @since 2021-07-22
* @apiNote 跳跃表
*/
public class SkipList<E extends Comparable<? super E>> {
private final static class Node<S> {
private final static class Level<T> {
/** 层后继 */
Node<T> backward;
/** 层跨度 */
int span;
public Level(Node<T> backward, int span) {
this.backward = backward;
this.span = span;
}
}
/** 数据 */
S item;
/** 前驱 */
Node<S> forward;
/** 层级数组 */
Level<S>[] levels;
public Node(S item, int level) {
this.item = item;
this.levels = new Level[level];
}
}
private static final int DEFAULT_LEVEL = 6;
/** 表头 */
private final Node<E> head;
/** 表尾 */
private Node<E> tail;
/** 最大层级 */
private final int maxLevel;
/** 总节点数 */
private long size;
public SkipList() {
this(DEFAULT_LEVEL);
}
public SkipList(int maxLevel) {
this.head = new Node<>(null, maxLevel);
this.tail = new Node<>(null, maxLevel);
this.maxLevel = maxLevel;
for (int i = 0; i < maxLevel; i++) {
head.levels[i] = new Node.Level<>(null, 0);
}
}
/**
* 查找并返回第level层小于elem的最后一个节点
* <p>同一level,从左向右
*
* @param start 起始搜寻点
* @param level 当前层级
* @param elem 比较数据
* @return 查找节点
*/
private Node<E> getLevelPrev(Node<E> start, int level, E elem) {
while (start.levels[level].backward != null &&
start.levels[level].backward.item.compareTo(elem) < 0) {
start = start.levels[level].backward;
}
return start;
}
/**
* 查找并返回包含指定数据的节点
* <p>自顶向下,从左向右
*
* @param elem 待查数据
* @return 找到则返回数据所在节点,未找到返回null
*/
private Node<E> getNode(E elem) {
Node<E> prev = head;
for (int i = maxLevel - 1; i >= 0; i--) {
// 获取当前层级的前驱
prev = getLevelPrev(prev, i, elem);
// 确保存在后继
if (prev.levels[i] == null) continue;
Node<E> node = prev.levels[i].backward;
if (node != null && node.item.equals(elem)) {
return node;
}
}
return null;
}
/**
* 生成并返回一个随机层级
*
* @return [1, maxLevel]之间的随机数
*/
private int getRandomLevel() {
return (int) Math.ceil(Math.random() * (maxLevel - 1)) + 1;
}
/**
* 查找数据是否存在
*
* @param elem 待查数据
* @return true 代表存在,false 代表不存在
*/
public boolean contains(E elem) {
return getNode(elem) != null;
}
/**
* 插入数据
* <ol>
* <li>根据新数据新建一个节点
* <li>生成节点的随机高度level
* <li>对于[1, level]每一层,设置层次数组中节点的后继和跨度
* <li>在最底层即第1层,插入节点,维护前驱和后继的关系
* </ol>
*
* @param elem 待插数据
* @return true 代表插入成功,false 代表数据已存在
*/
public boolean insert(E elem) {
if (contains(elem)) return false;
int level = getRandomLevel();
Node<E> newNode = new Node<>(elem, level);
Node<E> prev = head;
for (int i = level - 1; i >= 0; i--) {
// 获得当前层级的前驱
prev = getLevelPrev(prev, i, elem);
Node.Level<E> prevLevel = prev.levels[i];
Node<E> next = prevLevel.backward;
// 第一层,将新节点插入到前驱和后继的中间,维护关系
if (i == 0) {
if (next != null) {
next.forward = newNode;
} else {
tail = newNode;
}
newNode.forward = prev;
}
// 增加后继的跨度
while (next != null) {
next.levels[i].span++;
next = next.levels[i].backward;
}
// 设置新节点的后继和跨度
newNode.levels[i] = new Node.Level<>(prevLevel.backward,
prevLevel.span + 1);
// 更新前驱的后继
prevLevel.backward = newNode;
}
size++;
return true;
}
/**
* 删除数据
* <ol>
* <li>查找节点,获取到节点的层次数组的长度level
* <li>对于[1, level]每一层,更新层级数组中节点前驱的后继
* <li>在最底层即第1层,删除结点,维护前驱和后继的关系
* </ol>
*
* @param elem 待删数据
* @return true 代表删除成功,false 代表数据不存在
*/
public boolean delete(E elem) {
Node<E> node = getNode(elem);
if (node == null) return false;
Node<E> prev = head;
int level = node.levels.length;
for (int i = level - 1; i >= 0; i--) {
// 获得当前层级的前驱
prev = getLevelPrev(prev, i, elem);
Node<E> next = node.levels[i].backward;
// 更新前驱的后继
prev.levels[i].backward = next;
// 更新后继的前驱或尾节点
if (i == 0) {
if (next != null) {
next.forward = prev;
} else {
tail = prev;
}
}
// 减少后继的跨度
while (next != null) {
next.levels[i].span--;
next = next.levels[i].backward;
}
}
size--;
return true;
}
public void print() {
int maxItemLength = size == 0 ? 0 : tail.item.toString().length();
String format = "-> |%" + maxItemLength + "s| ";
StringBuilder gap = new StringBuilder("------");
for (int i = 0; i < maxItemLength; i++) gap.append('-');
System.out.printf("Size of list: %d\n", size);
for (int i = maxLevel - 1; i >= 0; i--) {
Node<E> node = head;
int nextSpan = 1;
if (node.levels[i].backward != null) {
nextSpan = node.levels[i].backward.levels[i].span;
}
System.out.printf("|L%d| ", i);
while (node.levels[i].backward != null) {
node = node.levels[i].backward;
while (nextSpan++ < node.levels[0].span) {
System.out.print(gap);
}
System.out.printf(format, node.item);
}
for (int j = nextSpan; j <= tail.levels[0].span; j++) {
System.out.print(gap);
}
System.out.println("-> |NULL|");
}
}
}
测试结果
测试代码:
/**
* @author KHighness
* @since 2021-07-22
* @apiNote 跳跃表测试
*/
public class Main {
public static void main(String[] args) {
SkipList<Integer> list = new SkipList<>(7);
for (int i = 0; i < 10; i++) {
// [10, 100)
int item = (int) (Math.random() * 90) + 10;
list.insert(item);
}
list.print();
System.out.printf("\nInsertion of 88 succeeded? %s\n", list.insert(88));
System.out.printf("Insertion of 55 succeeded? %s\n", list.insert(55));
System.out.printf("Insertion of 11 succeeded? %s\n", list.insert(11));
System.out.printf("Insertion of 11 again succeeded? %s\n\n", list.insert(11));
list.print();
System.out.printf("\nDeletion of 88 succeeded? %s\n", list.delete(88));
System.out.printf("Deletion of 55 succeeded? %s\n", list.delete(55));
System.out.printf("Deletion of 11 succeeded? %s\n", list.delete(11));
System.out.printf("Deletion of 11 again succeeded? %s\n\n", list.delete(11));
list.print();
}
}
运行结果:
Size of list: 10
|L6| ---------> |23| -------------------------> |58| ---------------------------------> |NULL|
|L5| ---------> |23| -----------------> |46| -> |58| ---------------------------------> |NULL|
|L4| ---------> |23| ---------> |43| -> |46| -> |58| ---------------------------------> |NULL|
|L3| ---------> |23| -> |35| -> |43| -> |46| -> |58| -> |60| -------------------------> |NULL|
|L2| -> |12| -> |23| -> |35| -> |43| -> |46| -> |58| -> |60| -> |61| -----------------> |NULL|
|L1| -> |12| -> |23| -> |35| -> |43| -> |46| -> |58| -> |60| -> |61| -> |62| -> |65| -> |NULL|
|L0| -> |12| -> |23| -> |35| -> |43| -> |46| -> |58| -> |60| -> |61| -> |62| -> |65| -> |NULL|
Insertion of 88 succeeded? true
Insertion of 55 succeeded? true
Insertion of 11 succeeded? true
Insertion of 11 again succeeded? false
Size of list: 13
|L6| -> |11| ---------> |23| -------------------------> |55| -> |58| -----------------------------------------> |NULL|
|L5| -> |11| ---------> |23| -----------------> |46| -> |55| -> |58| -----------------------------------------> |NULL|
|L4| -> |11| ---------> |23| ---------> |43| -> |46| -> |55| -> |58| -----------------------------------------> |NULL|
|L3| -> |11| ---------> |23| -> |35| -> |43| -> |46| -> |55| -> |58| -> |60| ---------------------------------> |NULL|
|L2| -> |11| -> |12| -> |23| -> |35| -> |43| -> |46| -> |55| -> |58| -> |60| -> |61| -------------------------> |NULL|
|L1| -> |11| -> |12| -> |23| -> |35| -> |43| -> |46| -> |55| -> |58| -> |60| -> |61| -> |62| -> |65| -> |88| -> |NULL|
|L0| -> |11| -> |12| -> |23| -> |35| -> |43| -> |46| -> |55| -> |58| -> |60| -> |61| -> |62| -> |65| -> |88| -> |NULL|
Deletion of 88 succeeded? true
Deletion of 55 succeeded? true
Deletion of 11 succeeded? true
Deletion of 11 again succeeded? false
Size of list: 10
|L6| ---------> |23| -------------------------> |58| ---------------------------------> |NULL|
|L5| ---------> |23| -----------------> |46| -> |58| ---------------------------------> |NULL|
|L4| ---------> |23| ---------> |43| -> |46| -> |58| ---------------------------------> |NULL|
|L3| ---------> |23| -> |35| -> |43| -> |46| -> |58| -> |60| -------------------------> |NULL|
|L2| -> |12| -> |23| -> |35| -> |43| -> |46| -> |58| -> |60| -> |61| -----------------> |NULL|
|L1| -> |12| -> |23| -> |35| -> |43| -> |46| -> |58| -> |60| -> |61| -> |62| -> |65| -> |NULL|
|L0| -> |12| -> |23| -> |35| -> |43| -> |46| -> |58| -> |60| -> |61| -> |62| -> |65| -> |NULL|
Process finished with exit code 0