跳表(Skip List)

  • 引出跳表
  • 跳表介绍
  • 跳表原理及实现
    • 使用跳表优化链表
    • 跳表基础结构
    • 跳表的搜索
    • 跳表的添加、删除
  • 跳表的层数
  • 跳表的复杂度分析
  • 跳表 - 完整源码

数据结构与算法笔记:恋上数据结构笔记目录

引出跳表

一个有序链表搜索、添加、删除的平均时间复杂度是多少?

  • O(n)

能否利用二分搜索优化有序链表,将搜索、添加、删除的平均时间复杂度降低至 O(logn)?

  • 链表没有像数组那样的高效随机访问(O(1) 时间复杂度),所以不能像有序数组那样直接进行二分搜索优化

有没有其他办法让有序链表搜索、添加、删除的平均时间复杂度降低至 O(logn)

  • 跳表(SkipList)

跳表介绍

跳表,又叫做跳跃表跳跃列表,在有序链表的基础上增加了“跳跃”的功能

  • 由 William Pugh 于1990年发布,设计的初衷是为了取代平衡树(比如红黑树)

Redis 中 的 SortedSetLevelDB 中的 MemTable 都用到了跳表

  • Redis、LevelDB 都是著名的 Key-Value数据库

对比平衡树:

  • 跳表的实现和维护会更加简单
  • 跳表的搜索、删除、添加的平均时间复杂度是 O(logn)(与红黑树相同)

跳表原理及实现

使用跳表优化链表

普通链表:

有效层数为 2 的跳表:

有效层数为 4 的跳表:

跳表基础结构

/*** 跳表* @author yusael*/
@SuppressWarnings("unchecked")
public class SkipList <K, V> {private static final double P = 0.25; // 仿照Redis的做法, 维护一个概率值private static final int MAX_LEVEL = 32; // 最高层数private int size;private Comparator<K> comparator;/*** 有效层数*/private int level;/*** 不存放任何 K-V 的虚拟头节点*/private Node<K, V> first; public SkipList(Comparator<K> comparator) {this.comparator = comparator;// 头节点的高度必须是跳表的最高层数(非有效层数), 为了后面插入新节点做准备first = new Node<>(null, null, MAX_LEVEL);}private int compare(K k1, K k2) {return comparator != null? comparator.compare(k1, k2): ((Comparable<K>)k1).compareTo(k2);}private static class Node<K, V> {K key;V value;Node<K, V>[] nexts; // 该节点的不同层指向的下一节点// 拿上面的四层跳表的图来举例// first.next[3] == 21节点// first.next[2] == 9节点// first.next[1] == 6节点// first.next[0] == 3节点public Node(K key, V value, int level) {this.key = key;this.value = value;nexts = new Node[level];}}}

跳表的搜索

① 从顶层链表的首元素开始,从左往右搜索,直至找到一个大于或等于目标的元素,或者到达当前层链表的尾部
② 如果该元素等于目标元素,则表明该元素已被找到
③ 如果该元素大于目标元素或已到达链表的尾部,则退回到当前层的前一个元素,然后转入下一层进行搜索

public V get(K key) {keyCheck(key);Node<K, V> node = first;for (int i = level - 1; i >= 0 ; i--) {int cmp = -1;while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {node = node.nexts[i];}// node.nexts[i].key >= keyif (cmp == 0) return node.nexts[i].value;}return null;
}

跳表的添加、删除


添加的细节:随机决定新添加元素的层数(官方建议)

/*** 跳表中加入新结点时的层数随机* @return*/
private int randomLevel() {int level = 1; while (Math.random() < P && level < MAX_LEVEL) { // 每次有25%的概率+1层level++;}return level;
}public V put(K key, V value) {keyCheck(key);Node<K, V> node = first;// 前驱节点, 插入节点时要用到, 获取了前驱就相当于获取了后继Node<K, V>[] prevs = new Node[level];for (int i = level - 1; i >= 0 ; i--) {int cmp = -1;while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {node = node.nexts[i];}// node.nexts[i].key >= keyif (cmp == 0) { // 节点原本就存在V oldV = node.nexts[i].value;node.nexts[i].value = value;return oldV;}prevs[i] = node; // 保存前驱节点}// 新节点的层数(随机)int newLevel = randomLevel();// 添加新节点Node<K, V> newNode = new Node<>(key, value, newLevel);// 设置新节点的前驱和后继(获取了前驱就相当于获取了后继)for (int i = 0; i < newLevel; i++) {if (i >= level) { // 新节点的层数比有效层数高// 头结点成为新节点的前驱first.nexts[i] = newNode; // 让头节点指向新节点(头节点创建时是最高层)// 后继结点默认指向null} else { // 新节点的层数比有效层数低newNode.nexts[i] = prevs[i].nexts[i]; // 让新节点的后继节点指向(之前的)前驱的后继prevs[i].nexts[i] = newNode; // 让前驱节点指向新节点}}level = Math.max(level, newLevel); // 计算跳表的最终层数(更新有效层数)size++; // 节点数量增加return null; // 之前不存在该节点, 返回null
}

删除的细节:删除一个元素后,整个跳表的层数可能会降低

public V remove(K key) {keyCheck(key);Node<K, V> node = first;Node<K, V>[] prevs = new Node[level];boolean exist = false; // 判断是否有该节点for (int i = level - 1; i >= 0; i--) {int cmp = -1;while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {node = node.nexts[i];}// key <= node.nexts[i].key if (cmp == 0) exist = true; // 存在prevs[i] = node; // 保存前驱节点}if (!exist) return null; // 跳表中没有该元素, 无需删除// 需要被删除的节点// 此时该元素必然存在, 且node必然为最下面一层, 该元素的前驱节点 Node<K, V> removedNode = node.nexts[0];// 设置后继for (int i = 0 ; i < removedNode.nexts.length; i++) {prevs[i].nexts[i] = removedNode.nexts[i];}// 更新跳表的层数int newLevel = level;while (--newLevel > 0 && first.nexts[newLevel] == null) {level = newLevel;}size--; // 数量减少return removedNode.value;
}

跳表的层数

跳表的复杂度分析

每一层的元素数量:从下往上

  • 第 1 层链表固定有 n 个元素
  • 第 2 层链表平均有 n * p 个元素
  • 第 3 层链表平均有 n * p^2 个元素
  • 第 k 层链表平均有 n * p^k 个元素
  • ……

最高层的层数是 log1/p n,平均有个 1/p 元素

在搜索时,每一层链表的预期查找步数最多是 1/p

  • 所以总的查找步数是 -(logp n/p)
  • 时间复杂度是 O(logn)

跳表 - 完整源码

package com.mj;import java.util.Comparator;/*** 跳表* @author yusael*/
@SuppressWarnings("unchecked")
public class SkipList <K, V> {private static final double P = 0.25; // 仿照Redis的做法, 维护一个概率值private static final int MAX_LEVEL = 32; // 最高层数private int size;private Comparator<K> comparator;/*** 有效层数*/private int level;/*** 不存放任何 K-V 的虚拟头结点*/private Node<K, V> first; public SkipList(Comparator<K> comparator) {this.comparator = comparator;first = new Node<>(null, null, MAX_LEVEL);}public SkipList() {this(null);}public int size() {return size;}public boolean isEmpty() {return size == 0;}public V put(K key, V value) {keyCheck(key);Node<K, V> node = first;// 前驱节点, 插入节点时要用到, 获取了前驱就相当于获取了后继Node<K, V>[] prevs = new Node[level];for (int i = level - 1; i >= 0 ; i--) {int cmp = -1;while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {node = node.nexts[i];}// node.nexts[i].key >= keyif (cmp == 0) { // 节点原本就存在V oldV = node.nexts[i].value;node.nexts[i].value = value;return oldV;}prevs[i] = node; // 保存前驱节点}// 新节点的层数(随机)int newLevel = randomLevel();// 添加新节点Node<K, V> newNode = new Node<>(key, value, newLevel);// 设置新节点的前驱和后继(获取了前驱就相当于获取了后继)for (int i = 0; i < newLevel; i++) {if (i >= level) { // 新节点的层数比有效层数高// 头结点成为新节点的前驱first.nexts[i] = newNode; // 让头节点指向新节点(头节点创建时是最高层)// 后继结点默认指向null} else { // 新节点的层数比有效层数低newNode.nexts[i] = prevs[i].nexts[i]; // 让新节点的后继节点指向(之前的)前驱的后继prevs[i].nexts[i] = newNode; // 让前驱节点指向新节点}}level = Math.max(level, newLevel); // 计算跳表的最终层数(更新有效层数)size++; // 节点数量增加return null; // 之前不存在该节点, 返回null}public V get(K key) {keyCheck(key);// first.next[3] == 21节点// first.next[2] == 9节点// first.next[1] == 6节点// first.next[0] == 3节点Node<K, V> node = first;for (int i = level - 1; i >= 0 ; i--) {int cmp = -1;while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {node = node.nexts[i];}// node.nexts[i].key >= keyif (cmp == 0) return node.nexts[i].value;}return null;}public V remove(K key) {keyCheck(key);Node<K, V> node = first;Node<K, V>[] prevs = new Node[level];boolean exist = false; // 判断是否有该节点for (int i = level - 1; i >= 0; i--) {int cmp = -1;while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {node = node.nexts[i];}// key <= node.nexts[i].key if (cmp == 0) exist = true; // 存在prevs[i] = node; // 保存前驱节点}if (!exist) return null; // 跳表中没有该元素, 无需删除// 需要被删除的节点// 此时该元素必然存在, 且node必然为最下面一层, 该元素的前驱节点 Node<K, V> removedNode = node.nexts[0];// 设置后继for (int i = 0 ; i < removedNode.nexts.length; i++) {prevs[i].nexts[i] = removedNode.nexts[i];}// 更新跳表的层数int newLevel = level;while (--newLevel > 0 && first.nexts[newLevel] == null) {level = newLevel;}size--; // 数量减少return removedNode.value;}/*** 跳表中加入新结点时的层数随机* @return*/private int randomLevel() {int level = 1; while (Math.random() < P && level < MAX_LEVEL) { // 每次有25%的概率+1层level++;}return level;}private void keyCheck(K key) {if (key == null) {throw new IllegalArgumentException("key must not be null.");}}private int compare(K k1, K k2) {return comparator != null? comparator.compare(k1, k2): ((Comparable<K>)k1).compareTo(k2);}private static class Node<K, V> {K key;V value;Node<K, V>[] nexts;public Node(K key, V value, int level) {this.key = key;this.value = value;nexts = new Node[level];}@Overridepublic String toString() {return key + ":" + value + "_" + nexts.length;}}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append("一共" + level + "层").append("\n");for (int i = level - 1; i >= 0; i--) {Node<K, V> node = first;while (node.nexts[i] != null) {sb.append(node.nexts[i]);sb.append("\t");node = node.nexts[i];}sb.append("\n");}return sb.toString();}
}

测试代码:

package com.mj;import java.util.TreeMap;
import com.mj.tool.Asserts;
import com.mj.tool.Times;public class Main {public static void main(String[] args) {//      SkipList<Integer, Integer> list = new SkipList<>();
//      test(list, 10, 10);time();}private static void time() {TreeMap<Integer, Integer> map = new TreeMap<>();SkipList<Integer, Integer> list = new SkipList<>();int count = 100_0000;int delta = 10;Times.test("SkipList", () -> {test(list, count, delta);});System.out.println("-----------------------");Times.test("TreeMap", () -> {test(map, count, delta);});}public static void test(SkipList<Integer, Integer> list, int count, int delta) {for (int i = 0; i < count; i++) {list.put(i, i + delta);}
//      System.out.println(list);for (int i = 0; i < count; i++) {Asserts.test(list.get(i) == i + delta);}Asserts.test(list.size() == count);for (int i = 0; i < count; i++) {Asserts.test(list.remove(i) == i + delta);}Asserts.test(list.size() == 0);}private static void test(TreeMap<Integer, Integer> map, int count, int delta) {for (int i = 0; i < count; i++) {map.put(i, i + delta);}for (int i = 0; i < count; i++) {Asserts.test(map.get(i) == i + delta);}Asserts.test(map.size() == count);for (int i = 0; i < count; i++) {Asserts.test(map.remove(i) == i + delta);}Asserts.test(map.size() == 0);}}
一共4层
19:29_4
4:14_3  19:29_4
3:13_2  4:14_3  7:17_2  9:19_2  19:29_4
0:10_1  1:11_1  2:12_1  3:13_2  4:14_3  5:15_1  6:16_1  7:17_2  8:18_1  9:19_2  10:20_1 11:21_1 12:22_1 13:23_1 14:24_1 15:25_1 16:26_1 17:27_1 18:28_1 19:29_4
【SkipList】
耗时:0.451s(451.0ms)
-------------------------------------
【TreeMap】
耗时:0.383s(383.0ms)
-------------------------------------

【恋上数据结构】跳表(Skip List)原理及实现相关推荐

  1. 【恋上数据结构】布隆过滤器(Bloom Filter)原理及实现

    布隆过滤器(Bloom Filter) 引出布隆过滤器(判断元素是否存在) 布隆过滤器介绍(概率型数据结构) 布隆过滤器的原理(二进制 + 哈希函数) 布隆过滤器的误判率(公式) 布隆过滤器的实现 布 ...

  2. 《恋上数据结构第1季》哈希表介绍以及从源码分析哈希值计算

    哈希表(Hash Table) 引出哈希表 哈希表(Hash Table) 哈希冲突(Hash Collision) JDK1.8的哈希冲突解决方案 哈希函数 如何生成 key 的哈希值 Intege ...

  3. 《恋上数据结构第1季》动态扩容数组原理及实现

    动态扩容数组 什么是数据结构? 线性表 数组(Array) 动态数组(Dynamic Array) 动态数组接口设计 清除所有元素 - clear() 添加元素 - add(E element).ad ...

  4. 《恋上数据结构第1季》二叉堆原理及实现、最小堆解决 TOP K 问题

    二叉堆 BinaryHeap 堆(Heap) 堆的出现 堆简介 二叉堆(Binary Heap) 获取最大值 最大堆 - 添加 最大堆 - 添加优化 最大堆 - 删除 replace 最大堆 - 批量 ...

  5. MJ恋上数据结构(第1季 + 第2季)笔记

    文章转载自:https://blog.csdn.net/weixin_43734095/article/details/104847976 恋上数据结构完整笔记(第1季 + 第2季) 前言 数据结构 ...

  6. 【恋上数据结构】串匹配算法(蛮力匹配、KMP【重点】、Boyer-Moore、Karp-Rabin、Sunday)

    串(Sequence) 串(前缀.后缀) 串匹配算法 蛮力(Brute Force) 蛮力1 – 执行过程 + 实现 蛮力1 – 优化 蛮力2 – 执行过程 + 实现 蛮力 – 性能分析 KMP 蛮力 ...

  7. 【恋上数据结构】动态规划(找零钱、最大连续子序列和、最长上升子序列、最长公共子序列、最长公共子串、0-1背包)

    动态规划(Dynamic Programming) 练习1:找零钱 找零钱 - 暴力递归 找零钱 - 记忆化搜索 找零钱 - 递推 思考题:输出找零钱的具体方案(具体是用了哪些面值的硬币) 找零钱 - ...

  8. 《恋上数据结构第1季》动态数组实现栈

    栈(Stack) 栈的应用 – 浏览器的前进和后退 栈的接口设计 动态数组实现栈 练习题 逆波兰表达式求值 有效的括号 数据结构与算法笔记目录:<恋上数据结构> 笔记目录 想加深 Java ...

  9. 《恋上数据结构第1季》队列、双端队列、循环队列、循环双端队列

    队列(Queue) 队列 Queue 队列的接口设计 队列源码 双端队列 Deque 双端队列接口设计 双端队列源码 循环队列 Circle Queue 循环队列实现 索引映射封装 循环队列 – %运 ...

最新文章

  1. 使用sqlmap直连数据库获取webshell
  2. python梯度下降法实现线性回归_【机器学习】线性回归——多变量向量化梯度下降算法实现(Python版)...
  3. spoj Brocken Data Base
  4. 1020. Tree Traversals (25) PAT甲级真题
  5. android 解决小米手机上选择照片路径为null的问题
  6. circle函数用法 turtle_Python绘图库Turtle详细分析
  7. 详解Redis的架构演化之路(附16张图解)
  8. PAT乙级(1017 A除以B)
  9. 如何理解nextTick函数
  10. 房子怎么拆除_新规,可能拆除农村这4类房子,每户家庭可能获得40万
  11. matlab建立遗传算法,Matlab遗传算法(一)
  12. android 获取刘海高度,不同刘海屏幕获取安全高度
  13. 颂钵带给我们是什么感受
  14. linux限制用户只能访问网站,Linux中限制用户访问权限的3种方法
  15. 53、backtrader的一些基本概念---如何用backtrader画图?
  16. Orcle 12c Sharding---Sharded和Duplicated表介绍
  17. 礼金记账本安卓_份子钱记账本-全民都爱用的随礼管理手账神器
  18. 寺库不再值得认可:违法案例频现,“便宜货”让罗敏和趣店亏惨
  19. 玉伯:从前端到体验,如何把格局做大
  20. 个人独资公司税收标准

热门文章

  1. 多目标跟踪全解析,全网最全
  2. 区块链安全 - DAO攻击事件解析
  3. Qt4_快速设计对话框
  4. 安装win10和ubuntu双系统启动不了解决
  5. 数据库逻辑删除的sql语句_SQL查询优化的数据库设计和逻辑断言
  6. C++ Template 使用简介
  7. Elasticsearch合并高亮字段
  8. json序列化定义类型jsckson
  9. linux下串口多线程通信 ,多串口收发数据错乱问题解决办法
  10. 2019-04-17 PowerShell基本语法