【恋上数据结构】跳表(Skip List)原理及实现
跳表(Skip List)
- 引出跳表
- 跳表介绍
- 跳表原理及实现
- 使用跳表优化链表
- 跳表基础结构
- 跳表的搜索
- 跳表的添加、删除
- 跳表的层数
- 跳表的复杂度分析
- 跳表 - 完整源码
数据结构与算法笔记:恋上数据结构笔记目录
引出跳表
一个有序链表搜索、添加、删除的平均时间复杂度是多少?
- O(n)
能否利用二分搜索优化有序链表,将搜索、添加、删除的平均时间复杂度降低至 O(logn)?
- 链表没有像数组那样的高效随机访问(O(1) 时间复杂度),所以不能像有序数组那样直接进行二分搜索优化
有没有其他办法让有序链表搜索、添加、删除的平均时间复杂度降低至 O(logn)?
- 跳表(SkipList)
跳表介绍
跳表,又叫做跳跃表、跳跃列表,在有序链表的基础上增加了“跳跃”的功能
- 由 William Pugh 于1990年发布,设计的初衷是为了取代平衡树(比如红黑树)
Redis 中 的 SortedSet
、LevelDB 中的 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)原理及实现相关推荐
- 【恋上数据结构】布隆过滤器(Bloom Filter)原理及实现
布隆过滤器(Bloom Filter) 引出布隆过滤器(判断元素是否存在) 布隆过滤器介绍(概率型数据结构) 布隆过滤器的原理(二进制 + 哈希函数) 布隆过滤器的误判率(公式) 布隆过滤器的实现 布 ...
- 《恋上数据结构第1季》哈希表介绍以及从源码分析哈希值计算
哈希表(Hash Table) 引出哈希表 哈希表(Hash Table) 哈希冲突(Hash Collision) JDK1.8的哈希冲突解决方案 哈希函数 如何生成 key 的哈希值 Intege ...
- 《恋上数据结构第1季》动态扩容数组原理及实现
动态扩容数组 什么是数据结构? 线性表 数组(Array) 动态数组(Dynamic Array) 动态数组接口设计 清除所有元素 - clear() 添加元素 - add(E element).ad ...
- 《恋上数据结构第1季》二叉堆原理及实现、最小堆解决 TOP K 问题
二叉堆 BinaryHeap 堆(Heap) 堆的出现 堆简介 二叉堆(Binary Heap) 获取最大值 最大堆 - 添加 最大堆 - 添加优化 最大堆 - 删除 replace 最大堆 - 批量 ...
- MJ恋上数据结构(第1季 + 第2季)笔记
文章转载自:https://blog.csdn.net/weixin_43734095/article/details/104847976 恋上数据结构完整笔记(第1季 + 第2季) 前言 数据结构 ...
- 【恋上数据结构】串匹配算法(蛮力匹配、KMP【重点】、Boyer-Moore、Karp-Rabin、Sunday)
串(Sequence) 串(前缀.后缀) 串匹配算法 蛮力(Brute Force) 蛮力1 – 执行过程 + 实现 蛮力1 – 优化 蛮力2 – 执行过程 + 实现 蛮力 – 性能分析 KMP 蛮力 ...
- 【恋上数据结构】动态规划(找零钱、最大连续子序列和、最长上升子序列、最长公共子序列、最长公共子串、0-1背包)
动态规划(Dynamic Programming) 练习1:找零钱 找零钱 - 暴力递归 找零钱 - 记忆化搜索 找零钱 - 递推 思考题:输出找零钱的具体方案(具体是用了哪些面值的硬币) 找零钱 - ...
- 《恋上数据结构第1季》动态数组实现栈
栈(Stack) 栈的应用 – 浏览器的前进和后退 栈的接口设计 动态数组实现栈 练习题 逆波兰表达式求值 有效的括号 数据结构与算法笔记目录:<恋上数据结构> 笔记目录 想加深 Java ...
- 《恋上数据结构第1季》队列、双端队列、循环队列、循环双端队列
队列(Queue) 队列 Queue 队列的接口设计 队列源码 双端队列 Deque 双端队列接口设计 双端队列源码 循环队列 Circle Queue 循环队列实现 索引映射封装 循环队列 – %运 ...
最新文章
- 使用sqlmap直连数据库获取webshell
- python梯度下降法实现线性回归_【机器学习】线性回归——多变量向量化梯度下降算法实现(Python版)...
- spoj Brocken Data Base
- 1020. Tree Traversals (25) PAT甲级真题
- android 解决小米手机上选择照片路径为null的问题
- circle函数用法 turtle_Python绘图库Turtle详细分析
- 详解Redis的架构演化之路(附16张图解)
- PAT乙级(1017 A除以B)
- 如何理解nextTick函数
- 房子怎么拆除_新规,可能拆除农村这4类房子,每户家庭可能获得40万
- matlab建立遗传算法,Matlab遗传算法(一)
- android 获取刘海高度,不同刘海屏幕获取安全高度
- 颂钵带给我们是什么感受
- linux限制用户只能访问网站,Linux中限制用户访问权限的3种方法
- 53、backtrader的一些基本概念---如何用backtrader画图?
- Orcle 12c Sharding---Sharded和Duplicated表介绍
- 礼金记账本安卓_份子钱记账本-全民都爱用的随礼管理手账神器
- 寺库不再值得认可:违法案例频现,“便宜货”让罗敏和趣店亏惨
- 玉伯:从前端到体验,如何把格局做大
- 个人独资公司税收标准