参考内容
[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? trueInsertion of 55 succeeded? trueInsertion of 11 succeeded? trueInsertion of 11 again succeeded? falseSize 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? trueDeletion of 55 succeeded? trueDeletion of 11 succeeded? trueDeletion of 11 again succeeded? falseSize 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
