跳表

跳表

跳表出现的原因

一个新"事物"出现是为了解决某种问题或者更好的帮"谁"解决问题从而替代这个"谁"。

在跳表出现之前,有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)

总图

跳表理解关键点 - 搞懂就很好理解了

  1. 每个跳表维护一个随机层数
如上图所示:3节点维护的随机层数是1,7是4,11是1,19是2,22是1,37是3
  1. 随机层数如何而来?

    每个节点的随机层数是某种算法"随机"得到的,这里的随机并不是说给一个范围,然后在这个范围内随机。

    随机算法:每个节点肯定至少有一层,那么它想得到两层的话有1/2的概率,依次类推得到3层的概率是1/4。总之,当前层要再往上一层的概率是1/2。

  2. 跳表每个节点的层次都对外有一个"箭头",指向后一个节点,这很难理解,如何取理解?

    对上图进行顺时针旋转90度,得到如下图案。

markdown 复制代码
这个图是不是好熟悉:脑子里很自然的闪过了树的结构。

只不过相比于二叉树这里的跳表树结构显得更加不规律。

这里的不规律体现在:每个节点可能有多个父节点,也可能有多个子节点。

总之:把跳表看成一棵多父节点、多子节点的树就很好理解了。

回过头来,再看在跳表中的每个节点所维护的随机层数,不就是维护多父节点和多子节点吗?随机层数为N,那么这个节点就有N个父节点,N个子节点。(**所有父节点中可能存在几个相同父节点、同理所有子节点中可能存在几个相同子节点**)
  1. 我看每个节点对外指向都相当的"齐",这是不是隐含着什么?

    没错,确实是隐含着某种东西,每个节点的第N个孩子只能"对应"后续节点的第N个孩子。

    直观表达就是:所有节点的同一层构成一个"链表"结构。(其实跳表的另一个名字叫做多层链表)

    对每层的链表来说,每个节点值都是有序的。

    如下图所示:总共有四个链表,其中两个我用红框框起来了。

  1. 在给出的跳表图形头部那个灰色的是个啥?

    头结点,为了更方便的操作跳表。

    在本文中,跳表中的值都是大于0的,头结点值定义为-1。

  2. 跳表的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里的安全类,对添加操作加了锁导致的。

相关推荐
Beauty.56837 分钟前
P1328 [NOIP2014 提高组] 生活大爆炸版石头剪刀布
数据结构·c++·算法
爱棋笑谦39 分钟前
二叉树计算
java·开发语言·数据结构·算法·华为od·面试
jimmy.hua1 小时前
C++刷怪笼(5)内存管理
开发语言·数据结构·c++
Freak嵌入式1 小时前
全网最适合入门的面向对象编程教程:50 Python函数方法与接口-接口和抽象基类
java·开发语言·数据结构·python·接口·抽象基类
MogulNemenis2 小时前
力扣春招100题——队列
数据结构·算法·leetcode
学java的小菜鸟啊2 小时前
第五章 网络编程 TCP/UDP/Socket
java·开发语言·网络·数据结构·网络协议·tcp/ip·udp
菜鸟求带飞_3 小时前
算法打卡:第十一章 图论part01
java·数据结构·算法
是小Y啦3 小时前
leetcode 106.从中序与后续遍历序列构造二叉树
数据结构·算法·leetcode
万河归海4284 小时前
C语言——二分法搜索数组中特定元素并返回下标
c语言·开发语言·数据结构·经验分享·笔记·算法·visualstudio
秋夫人5 小时前
B+树(B+TREE)索引
数据结构·算法