跳表
跳表出现的原因
一个新"事物"出现是为了解决某种问题或者更好的帮"谁"解决问题从而替代这个"谁"。
在跳表出现之前,有bst、avl、以及红黑树。(这些数据结构可都是支持按某个维度排序的结构)
bst在极端情况下(插入数据都是有序的)可能退化成一个链表。
从而出现了avl和红黑树这两种平衡树(这两种树是理想状态下的bst),这两种树维护平衡的主要手段是通过"节点自平衡",维护每个节点的的左右两棵子树的高度差不超过1。而跳表维护"平衡"的手段就显得很直观和易懂,通过给每个节点维护一个随机层数来达到平衡的目的。
总得来说就是:avl、红黑树能做到的跳表都能做到,跳表的思想和实现还更简单。
跳表是由William Pugh发明。他在 Communications of the ACM June 1990, 33(6) 668-676 发表了Skip lists: a probabilistic alternative to balanced trees,在该论文中详细解释了跳表的数据结构和插入删除操作。
跳表图示
本文所有图片来源: Redis 为什么用跳表而不用平衡树? - 掘金 (juejin.cn)
总图
跳表理解关键点 - 搞懂就很好理解了
- 每个跳表维护一个随机层数
如上图所示:3节点维护的随机层数是1,7是4,11是1,19是2,22是1,37是3
-
随机层数如何而来?
每个节点的随机层数是某种算法"随机"得到的,这里的随机并不是说给一个范围,然后在这个范围内随机。
随机算法:每个节点肯定至少有一层,那么它想得到两层的话有1/2的概率,依次类推得到3层的概率是1/4。总之,当前层要再往上一层的概率是1/2。
-
跳表每个节点的层次都对外有一个"箭头",指向后一个节点,这很难理解,如何取理解?
对上图进行顺时针旋转90度,得到如下图案。
markdown
这个图是不是好熟悉:脑子里很自然的闪过了树的结构。
只不过相比于二叉树这里的跳表树结构显得更加不规律。
这里的不规律体现在:每个节点可能有多个父节点,也可能有多个子节点。
总之:把跳表看成一棵多父节点、多子节点的树就很好理解了。
回过头来,再看在跳表中的每个节点所维护的随机层数,不就是维护多父节点和多子节点吗?随机层数为N,那么这个节点就有N个父节点,N个子节点。(**所有父节点中可能存在几个相同父节点、同理所有子节点中可能存在几个相同子节点**)
-
我看每个节点对外指向都相当的"齐",这是不是隐含着什么?
没错,确实是隐含着某种东西,每个节点的第N个孩子只能"对应"后续节点的第N个孩子。
直观表达就是:所有节点的同一层构成一个"链表"结构。(其实跳表的另一个名字叫做多层链表)
对每层的链表来说,每个节点值都是有序的。
如下图所示:总共有四个链表,其中两个我用红框框起来了。
-
在给出的跳表图形头部那个灰色的是个啥?
头结点,为了更方便的操作跳表。
在本文中,跳表中的值都是大于0的,头结点值定义为-1。
-
跳表的N层链表,N没有限制吗?
理论上没有限制,但是通常在具体实现上会固定一个最大值,我们下面实现的过程中将N最大值设置为16。
跳表的增删查
添加节点
当前跳表结构:
此时,我想插入一个值为7的节点,我们可以快速直观地得到答案:插入头节点之后,19节点之前。
可是这个过程是怎么样找到的呢?
-
从上图我们可知,当前跳表结构所维护的最高层是2层。
-
假设待插入节点7的随机层数是4。
-
此时节点随机层数大于目前最高的2,所以将最高层数变成4。
-
从head节点的第4层链表开始往后找,找到第一个值略小于 (最接近7的节点 )7的节点。(链表插入要找到前一个节点)
-
我们发现第4层的头结点直接指向了null,所以第4层值略小于7的就是头结点。
-
然后层数下降,到第三层链表来看,看我们发现和第四层是一样的,在第三层略小于7的节点依然是头结点。
(这里是从上一步得到的那个略小于7的节点开始往后,只是恰好上一层略小于7的节点是头结点,所以这里从头结点开始,后续步骤同样的道理)
-
继续层数下降,到第二层来看,我们发现头结点的下一个节点指向了节点19,而19是大于7的,所以在第二层略小于7的节点依旧是头结点。
-
继续层数下降,到第一层来看,我们依然发现,值略小于7的节点是头结点。
-
最后在这四层的每一层链表中插入我们的目标节点7。
得到如下图示:
删除节点
删除节点的操作和新增是很像的。
当前跳表结构如下图所示:
现在要删除目标数据为7的节点。
-
得到当前跳表中节点最高层数,图中所示是4。
-
从第4层开始,找到比目标值7略小的节点,很显然,在第四层略小于7的节点是头节点。
-
层数下降,到第三层,同样找比目标值略小的节点,结果仍然是头结点。(这里是从上一步得到的那个略小于7的节点开始往后,只是恰好上一层略小于7的节点是头结点,所以这里从头结点开始,后续步骤同样的道理)
-
依次步骤,得到第二层,第一层,略小于7的节点都是头结点。
-
分别从这四层链表中删除目标节点7。(链表断链操作)
结果如下所示:
查找节点
有了上面新增和删除节点,查找过程就显得很简单了。
目前跳表结构如下图所示:
目标:找到跳表中指为37的节点。
查找步骤:
- 从图中可以看出,目前最大层数是4。
- 从头结点的第4层开始,尝试性找到目标值为37的节点,如果当前层链表中没有该节点,那么找到比目标值37略小的节点。
- 第4层,很显然没有目标值节点,所以找到了比目标值37略小的节点7。
- 层数下降,从第三层的节点7开始往后找 ,找到了目标值37节点,立即返回即可。(因为是查找,不需要对整个跳表结构做改动,所以直接返回即可,不需要再往后找了)
跳表核心代码实现
理解了上面关于跳表的图示,我们就可以很轻松的实现跳表这个数据结构了
定义每个节点的结构
java
// 定义每个节点
class Node<K extends Comparable<K>,V> {
// 节点key和value
public K k;
public V v;
// p.next[i] 表示 p 节点的第 i 层 的下一个指向
// 可以理解成指向N个孩子节点的引用
Node[] next;
public Node(K k, V v,int level){
this.k = k;
this.v = v;
this.next = new Node[level];
}
public Node(int level){
this.next = new Node[level];
}
public Node(){
}
@Override
public String toString() {
return "Node{" +
"k=" + k +
", v=" + v +
'}';
}
}
跳表的全局维护变量
包含:头结点,限制最大层数,当前最大层数,跳表中的节点数。
java
// 让每个节点的索引最多16个
public static final int MAX_LEVEL = 16;
// 在当前跳表中,用到了多少层索引
private int curMaxLevel = 1;
// 跳表中节点数
private int size = 0;
// 头结点
private Node head = new Node(MAX_LEVEL);
// 随机函数,给每个节点生成层数做随机处理
private Random rd = new Random();
随机层数方法
java
// 每个节点默认有一层索引,每增加一层索引的概率是1/2
private int randomLevel(){
int level = 1;
while ((rd.nextInt() & 1) == 1 && level < MAX_LEVEL){
level ++;
}
return level;
}
以上三点是实现跳表的基础,下面是跳表的增删查方法的实现。
往跳表中插入数据
java
public void add(K key, V value) {
// 为每一个节点生成一个随机的层数
int level = randomLevel();
// 防止极端情况:第一节点的层数是1,第二个节点层数直接来个16,跨度太大
// 所以当新生成的节点层数大于当前skip list中最大的层数时,让当前层数 = skip list中最大的层数 + 1
if (level > curMaxLevel) level = ++curMaxLevel;
// 生成新节点
Node newNode = new Node(key, value,level);
// 从头结点开始
Node p = head;
// 新插入的节点只会影响当前节点level个"链表"
// i -- 控制层数下移
for (int i = level - 1; i >= 0; i--) {
// 目的是找到每层比目标值 略小 的那个节点
while (p.next[i] != null && p.next[i].k.compareTo(key) < 0){
p = p.next[i];
}
// 找到略小的了,往每层链表插入节点的过程
newNode.next[i] = p.next[i];
p.next[i] = newNode;
}
// 维护一下插入的节点数
size ++;
}
从跳表中删除指定值
java
public V remove(K key) {
Node p = head;
boolean flag = false;
// 从当前skip list的最高层开始往下遍历
for (int i = curMaxLevel - 1; i >= 0; i--) {
// 这个循环和前面插入的是一样的
while (p.next[i] != null && p.next[i].k.compareTo(key) < 0){
p = p.next[i];
}
// 1、存在第一个不比目标值小的节点
// 2、判断这个节点的值是否等于目标值
if (p.next[i] != null && p.next[i].k.compareTo(key) == 0){
// "链表" 断链操作
p.next[i] = p.next[i].next[i];
// 只有执行了断链操作,维护的size才能 --
flag = true;
}
}
// 维护节点数
if (flag) size --;
return null;
}
从跳表中查找指定值
java
private Node getNode(K key){
Node p = head;
for (int i = curMaxLevel - 1; i >= 0; i--) {
while (p.next[i] != null && p.next[i].k.compareTo(key) < 0){
p = p.next[i];
}
// 不为null,并且相等,立马返回
if (p.next[i] != null && p.next[i].k.compareTo(key) == 0){
return p.next[i];
}
}
return null;
}
以上就是整个跳表的核心实现。
我实现的跳表 - 完整版
从核心代码中可以看出,跳表中每个节点是Entry类型的。
和Jdk的HashMap和HashSet一样,我也实现了类似的这两个类,具体代码如下:
Map 接口 --- 自己定义的,非jdk带的
java
package indi.xm.data_structure.map;
/**
* @author: albert.fang
* @date: 2021/4/21 12:04
* @description: 映射接口
*/
public interface Map<K,V> {
// 增
void add(K key, V value);
// 删
V remove(K key);
// 改
void set(K key, V value);
// 查
V get(K key);
boolean contains(K key);
boolean isEmpty();
int getSize();
}
SkipListMap
java
package indi.xm.data_structure.skiplist;
import indi.xm.data_structure.map.Map;
import java.util.Iterator;
import java.util.Random;
/**
* @Author: xm.f
* @Date: 2023/3/28 18:00
*/
public class SkipListMap<K extends Comparable<K>,V> implements Map<K,V> {
// 让每个节点的索引最多16个
public static final int MAX_LEVEL = 16;
// 在当前跳表中,用到了多少层索引
private int curMaxLevel = 1;
// 跳表中节点数
private int size = 0;
// 头结点
private Node head = new Node(MAX_LEVEL);
// 随机函数,给每个节点生成层数做随机处理
private Random rd = new Random();
@Override
public void add(K key, V value) {
// 为每一个节点生成一个随机的层数
int level = randomLevel();
// 防止极端情况:第一节点的层数是1,第二个节点层数直接来个16,跨度太大
// 所以当新生成的节点层数大于当前skip list中最大的层数时,让当前层数 = skip list中最大的层数 + 1
if (level > curMaxLevel) level = ++curMaxLevel;
// 生成新节点
Node newNode = new Node(key, value,level);
// 从头结点开始
Node p = head;
// 新插入的节点只会影响当前节点level个"链表"
// todo 拆分出来讲这个循环
// i -- 控制层数下移
for (int i = level - 1; i >= 0; i--) {
// 目的是找到每层比目标值 略小 的那个节点
while (p.next[i] != null && p.next[i].k.compareTo(key) < 0){
p = p.next[i];
}
// 往每层链表插入节点的过程
newNode.next[i] = p.next[i];
p.next[i] = newNode;
}
// 维护一下插入的节点数
size ++;
}
@Override
public V remove(K key) {
Node p = head;
boolean flag = false;
// 从当前skip list的最高层开始往下遍历
for (int i = curMaxLevel - 1; i >= 0; i--) {
// 这个循环和前面插入的是一样的
while (p.next[i] != null && p.next[i].k.compareTo(key) < 0){
p = p.next[i];
}
// 1、存在第一个不比目标值小的节点
// 2、判断这个节点的值是否等于目标值
if (p.next[i] != null && p.next[i].k.compareTo(key) == 0){
// "链表" 断链操作
p.next[i] = p.next[i].next[i];
// 只有执行了断链操作,维护的size才能 --
flag = true;
}
}
// 维护节点数
if (flag) size --;
return null;
}
@Override
public void set(K key, V value) {
Node node;
if (( node = getNode(key)) != null) {
node.v = value;
}else {
// 否则新建节点
add(key,value);
}
}
@Override
public V get(K key) {
Node node = getNode(key);
return node == null ? null : (V)node.v;
}
@Override
public boolean contains(K key) {
return getNode(key) != null;
}
@Override
public boolean isEmpty() {
return getSize() == 0;
}
@Override
public int getSize() {
return size;
}
public void clear(){
head = new Node(MAX_LEVEL);
size = 0;
curMaxLevel = 1;
}
private Node getNode(K key){
Node p = head;
for (int i = curMaxLevel - 1; i >= 0; i--) {
while (p.next[i] != null && p.next[i].k.compareTo(key) < 0){
p = p.next[i];
}
// 不为null,并且相等,立马返回
if (p.next[i] != null && p.next[i].k.compareTo(key) == 0){
return p.next[i];
}
}
return null;
}
// 每个节点默认有一层索引,每增加一层索引的概率是1/2
private int randomLevel(){
int level = 1;
while ((rd.nextInt() & 1) == 1 && level < MAX_LEVEL){
level ++;
}
return level;
}
// 定义每个节点
class Node<K extends Comparable<K>,V> {
// 节点key和value
public K k;
public V v;
// p.next[i] 表示 p 节点的第 i 层 的下一个指向
// 可以理解成指向N个孩子节点的引用
Node[] next;
public Node(K k, V v,int level){
this.k = k;
this.v = v;
this.next = new Node[level];
}
public Node(int level){
this.next = new Node[level];
}
public Node(){
}
@Override
public String toString() {
return "Node{" +
"k=" + k +
", v=" + v +
'}';
}
}
public Iterator iterator(){
return new Iterator() {
Node p = head.next[0];
int s = getSize();
@Override
public boolean hasNext() {
return s != 0;
}
@Override
public Object next() {
s --;
Node tmp = p;
p = p.next[0];
return tmp;
}
};
}
public Iterator iteratorForSet(){
return new Iterator() {
Node p = head.next[0];
int s = getSize();
@Override
public boolean hasNext() {
return s != 0;
}
@Override
public Object next() {
s --;
Node tmp = p;
p = p.next[0];
return tmp.k;
}
};
}
}
SkipListSet
java
package indi.xm.data_structure.skiplist;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
/**
* @Author: xm.f
* @Date: 2023/3/29 10:54
*/
public class SkipListSet<K extends Comparable<K>> implements Set {
private static final Object IMMUTABLE = null;
private SkipListMap<K,Object> skipListMap = new SkipListMap();
@Override
public int size() {
return skipListMap.getSize();
}
@Override
public boolean isEmpty() {
return skipListMap.isEmpty();
}
@Override
public boolean contains(Object o) {
return skipListMap.contains((K)o);
}
@Override
public Iterator iterator() {
Iterator iterator = skipListMap.iteratorForSet();
return new Iterator() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Object next() {
return iterator.next();
}
};
}
@Override
public Object[] toArray() {
return new Object[0];
}
@Override
public boolean add(Object o) {
try {
skipListMap.add((K)o,IMMUTABLE);
}catch (Exception e){
return false;
}
return true;
}
@Override
public boolean remove(Object o) {
try {
skipListMap.remove((K)o);
}catch (Exception e){
return false;
}
return true;
};
@Override
public boolean addAll(Collection c) {
try{
for (Object o : c) {
add(o);
}
}catch (Exception e){
return false;
}
return true;
}
@Override
public void clear() {
skipListMap.clear();
}
@Override
public boolean removeAll(Collection c) {
for (Object o : c) {
if (!remove(o)) {
return false;
}
}
return true;
}
@Override
public boolean retainAll(Collection c) {
clear();
return addAll(c);
}
@Override
public boolean containsAll(Collection c) {
for (Object o : c) {
if (!contains(o)) {
return false;
}
}
return true;
}
@Override
public Object[] toArray(Object[] a) {
return new Object[0];
}
}
比较器
将上述三个类拷贝到自己的ide,就可以直接使用了。
上面说了一大堆,但是我怎么知道,我实现的跳表没有问题呢?没错,咱是严谨的人,得验证。
怎么比较,当然是写比较器咯,用jdk自带的skipList和我自己实现的比较。
生成随机数组
生成随机数组,目的是将生成的随机数据分别添加到两个跳表中。(我的,JDK的)
java
// 生成没有重复的随机数组
public static int[] getRandomArray(){
int[] tmp = new int[200_0000];
Random rd = new Random();
// 具体大小和这个循环有关
for (int i = 0; i < 150_0000; i++) {
int r = rd.nextInt(200_0000);
tmp[r] = 1;
}
int rdl = 0;
for (int i = 0; i < tmp.length; i++) {
if (tmp[i] != 0) rdl++;
}
int[] ret = new int[rdl];
for (int i = 0; i < tmp.length; i++) {
if (tmp[i] == 0 ) continue;
ret[--rdl] = i;
}
return ret;
}
具体比较过程
从4个维度比较:
- 添加速度
- 删除速度
- 查询速度
- 两个跳表中的元素是否一致
java
public static void compareSdkAndMySkipListTest(){
ConcurrentSkipListSet<Integer> set = new ConcurrentSkipListSet<>();
SkipListSet<Integer> skipList = new SkipListSet<>();
int[] randomArray = getRandomArray();
// 1、test add speed
long start = System.currentTimeMillis();
for (int i : randomArray) {
skipList.add(i);
}
System.out.println("my skipList add 总共耗时:" + (System.currentTimeMillis() - start) + "ms 现在大小是:" + skipList.size());
start = System.currentTimeMillis();
for (int i : randomArray) {
set.add(i);
}
System.out.println("sdk skipList add 总共耗时:" + (System.currentTimeMillis() - start) + "ms 现在大小是:" + set.size());
// 2、test sdk and my skip list value whether same
int[] tmpSet = new int[randomArray.length];
int[] tmpSkipList = new int[randomArray.length];
int i = 0;
Iterator<Integer> setIterator = set.iterator();
while (setIterator.hasNext()) {
tmpSet[i++] = setIterator.next();
}
i = 0;
Iterator<Integer> iterator = skipList.iterator();
while (iterator.hasNext()) {
tmpSkipList[i++] = iterator.next();
}
for (int j = 0; j < randomArray.length; j++) {
if (tmpSet[j] != tmpSkipList[j]) {
throw new RuntimeException("SkipList error");
}
}
System.out.println("sdk and my skip list value is same");
// 3、test get speed
start = System.currentTimeMillis();
for (int randomValue : randomArray) {
if (!skipList.contains(randomValue)) {
System.out.println("my skipList get 不应该打印");
}
}
System.out.println("my skipList get 总共耗时:" + (System.currentTimeMillis() - start) + "ms 现在大小是:" + skipList.size());
uan'lia断链
start = System.currentTimeMillis();
for (int randomValue : randomArray) {
if (!set.contains(randomValue)) {
System.out.println("sdk skipList get 不应该打印");
}
}
System.out.println("sdk skipList get 总共耗时:" + (System.currentTimeMillis() - start) + "ms 现在大小是:" + set.size());
// 4、test remove speed
start = System.currentTimeMillis();
for (int randomValue : randomArray) {
skipList.remove(randomValue);
}
System.out.println("my skipList remove 总共耗时:" + (System.currentTimeMillis() - start) + "ms 现在大小是:" + skipList.size());
start = System.currentTimeMillis();
for (int randomValue : randomArray) {
set.remove(randomValue);
}
System.out.println("sdk skipList remove 总共耗时:" + (System.currentTimeMillis() - start) + "ms 现在大小是:" + set.size());
System.out.println("mySkipListSet compare to jdk is same,congratulation to you");
}
我本地的比较结果
从比较数据上可以看出除了添加操作意外,其它操作的用时都是差不多的。
我分析:JDK 跳表添加操作比较耗时应该是由于这个跳表是juc里的安全类,对添加操作加了锁导致的。