SkipList 基本原理和Java实现

在数据结构中,树一直被很多系统钟爱,如mysql 的innodb使用的是B+ 数,在java中Map的hash碰撞后,如果链表超过8 会切换为红黑树。树结构的好处个人认为是在写入的时候对数据进行一个预处理,而且这个出力和子节点的数量相关,在写入的时候按照规则插入,能够在查询的时候有效的查询对应的子树,从而达到查询时间为LogN 。如红黑树,红黑树的结构是异构化的23 树,能够确保左右子树的高度差维持在1,有效保证数据平衡,但是红黑树的实现比较复杂,插入过程涉及到树的重平衡。后来出现了一种链表的结构,称之为跳表即SkipList,他是由William Pugh 在Skip Lists: A Probabilistic Alternative to Balanced Trees提出的一种平衡性很好的数据结构,这种数据结构是使用空间换时间,实现较为简单,查询复杂度和树结构接近。在redis 和leveldb中都有使用。本文会介绍下SkipList的基本原理和一个java实现。

SkipList 原理

在某种意义上,链表可以算得上是一种特殊的树,只有一个分支的树。一般情况下分为单向链表和双向链表。单向链表里面的节点会有个指针指向下一个节点,双向链表包含两个指针,一个指向指向他的节点(prev),一个指向下一个节点。SkipList 是单向链表,但是不止一个链表,而是使用n个链表分层,每一层中的node 指向的下一个节点各不相同。后续的说明主要是针对上文提到的论文中的一个小摘抄:

论文中提到(第一节),二叉树在随机大小写入的情况下效率很高,但是如果是顺序大小写入则表现的比较一般,这个是因为二叉树尤其是平衡二叉树需要对数结构进行一个平衡引起的。

SkipList是一种平衡树的概率性的替代品,这里概率性个人觉得主要是上面提到的随机大小数据写入过程平衡树效率比顺序写高,就是通过另外的方式实现了这种顺序写变为随机。论文中提到,250个元素的skiplist需要查询查过3次的概率不到百万分之一。

论文中还提到,skiplist在实现上比较容易,而且使用的是概率性的计算来保持数据的平衡,在空间上平均每个节点可能只需要1+1/3 个指针。

SkipList 还有一个好处就对锁范围的控制上,在平衡二叉树或者红黑树,在并发操作的情况下,特定的时候如需要修改Root节点的属性,可能需要对整棵树都加锁。但是SkipList里面可以针对局部节点进行加锁,在并发情况下实现比较好。

上图选自论文中的figure1。

n标识当前链表的长度,且链表是顺序存储的。

  1. 在初始阶段,当前的单向链表查询可能需要遍历n
  2. 如果将可以记录每间隔个节点创建连接,则可以将查找的时间缩小为n/2+1。按照最上层比较,可以找到当前查询值的前一个节点,所以是n/2+1
  3. 还可以在2 的基础上,每间隔一个继续创建一个连接,则时间缩小为n/4+2。首先最上层找,此时是n/4,然后在第二层,因为当前中间有b层,而且b层此时只有1个节点,当前值要么大于这个值要么小于,所以需要2次就可以找到当前值。
  4. 现在将具有k 个指向其他指针的节点称之为level K 节点,则说明第2^i 节点后面有2^i 个节点,则节点分布在一个比较均匀的数据中。level0 0 节点有100%个节点,有50% 的节点在level1,有25% 在level2,以此类推。
  5. 但是如果我们随机选择level1 层的节点,可能会出现上图e中情况,也就是高层分配不均匀,如6 虽然在第四层,但是他却处于第二个位置。

可以看到,SkipList的核心其实是这个层数的确定,如果是按照十分恰当的分配节点中的每一个节点的level,他的时间复杂度就是logN。

这个上升当前节点的level的时机是影响链表查询的关键。

SkipList的操作

主要设计到增删查:

  1. Search操作返回目标关键字所关联的value,如果没有该关键字则返回failure
  2. Insert操作对目标关键字更新value,如果目标关键字不存在,则将关键字插入
  3. Delete操作删除目标关键字。

查询操作

查询操作是删除和插入的基础,因为每一层都是单向链表,所以必须要找到插入的父节点才行,如果当前节点的level 较高,可能还需要将查询过程中的每一层的父节点都记录下来。

伪代码如下:(论文figure2)

伪代码是搜索一个searchKey 是否在skiplist中。具体步骤如下:

  1. 将x 初始话为skiplist的头节点
  2. 进入第一次循环,循环是从当前最大的level 到 level1
  3. 然后进入第二层循环,在当前层的forword 对象中找到key值不小于当前searchkey的值
  4. 跳出3 的循环后,此时已经找到本层不小于searchkey的值,继续往下一层找。如果找到了等于当前searchkey的值,则返回值存储的value。
  5. 如果从maxlevel到1 都没有这个值,返回一个failture。

查询就是从最上层往下查询的过程。

插入和删除

在上文中,我们查询不到这个值就返回一个failture,但是其实也可以返回当前找到的level1层的值,因为这个值是当前skiplist中不小于searchKey的值。如果说是插入,那么也应该是插入到这个值的后面。但是因为涉及到level的变化,所以查询过程中需要将当前查到的每一层不小于当前的值都查出来,然后随机一个当前值的level,然后将存储的每一层level都和当前level进行一个绑定。删除也类似。

插入和删除图形化为:

后文根据论文中的伪代码介绍下操作

插入

插入的伪代码如下:论文figure4

  1. 定义一个变量update,用来存储上文提到的每一层中不小于当前值的Node。
  2. 查找当前的key和将每一层比较的结果赋值到update中。
  3. 如果查到当前值了,那么put就变成了update,直接将skiplist中的对应key的value修改为当前的newvalue。
  4. 如果没有找到,也就是上文提到的,首先随机产生一个当前的level。如果当前的level超过skiplist当前最大的level,那么需要将当前的level设置为新生成的level,然后将head放到每一层的头节点。
  5. 将插入的数据变成Node,Node 包含了当前key,value 和level信息,当然内部还有个next信息
  6. 从第1层往上,将当前的x插入到每一层。

这里有个问题就是对level的管理,因为不可能随便随机一个数出来,最好的是希望设置一个maxlevel,然后作为seed,在maxLevel下面随机产生层数。

随机算法

在论文中提到过,skiplist本身是个概率性的算法,个人觉得概率性其实就是指的层数的选择。他想要达到的效果是在上升中,某一层出现的概率是大致一定的。也就是说如果第i层的元素能够按照某个固定的概率p(上文使用的是1/2)出现在在i+1 层,这里面涉及到概率论里面期望运算,就不再赘述了(我已经还给老师了)。一句话说就是如过选择的是p=1/2,那么我们就希望如果n等于16,那么第0层是16,第二次是8,第三层是4。说白了就是返回的值出现的概率是一样的,比如 返回1的概率就是1/2,返回2的概率就是1/4。

arduino 复制代码
private static final double PROBABILITY = 0.5;  
private int randomLevel() {
        int level = 1;
        Random random = new Random();
        while (random.nextDouble() < PROBABILITY && level < MAX_LEVEL) {
            level++;
        }
        return level;
    }

使用这种方式可以生成,主要是因为random.nextDouble() 出现的数是比较均匀的分布,就是出现的概率是相对一定的,然后只要小于0.5 就多一层,也就是上面的1/2的概率,继续增加level,则就是1/2 *1/2 的概率了。

删除

删除也是在查询基础上做的,伪代码如下:

首先仍然是记录下查询过程的每一层的上一个节点,如果找到这个值对应的Node,就将update中存储的值都进行一个链表删除操作,然后释放内存。最后,还需要检查当前是否删除了最高的level中的最后一个node,如果是还需要将当前的level减1.

自己写个SkipList

使用java 自己写了个skiplist,其中数据使用DataNode,索引使用IndexNode,避免值的无效覆盖:

ini 复制代码
import java.util.LinkedList;
import java.util.Random;
​
public class SkipList<K extends Comparable<K>, V> {
    // 当前最大的层数
    private int maxLevel = 12;
    // 头结点,down 使用down指向下一层
    private SkipListIndexNode<K, V> head;
    // 当前的level
    private int level = 1;
    private static Random random;
    //跳到下一层的概率
    private double probability = 0.5;
​
    public SkipList(int maxLevel, double probability) {
        random = new Random();
        this.maxLevel = maxLevel;
        this.probability = probability;
        SkipListDataNode data = new SkipListDataNode(Integer.MIN_VALUE, null);
        head = new SkipListIndexNode(data, 1);
    }
​
​  // 根据key ,返回存储的Value,如果没有,返回null
    public V get(K key) {
        SkipListDataNode<K, V> old = searchNode(key);
        if (old != null) {
            return old.value;
        } else {
            return null;
        }
    }
​ // 获取最大值,应该有更优解,比如存储最底层的末尾节点
    public K getMax() {
        SkipListIndexNode<K, V> cur = head;
        while (cur.down != null) {
            while (cur.right != null) {
                cur = cur.right;
            }
            if (cur.down == null && cur.right == null) {
                break;
            }
            cur = cur.down;
        }
        return cur.getKey();
    }
​  // 获取最小值,应该有更优解,比如存储最底层的head节点
    public K getMin() {
        SkipListIndexNode<K, V> cur = head;
        while (cur.down != null) {
            cur = cur.down;
        }
        return cur.right == null ? null : cur.right.getKey();
    }
​
    public void put(K key, V value) {
        SkipListDataNode old = searchNode(key);
        if (old != null) {
            old.value = value;
            return;
        }
        int newLevel = randomLevel();
        if(newLevel>maxLevel){
            newLevel=maxLevel;
        }
        if (newLevel > level ) {
            for (int i = level; i < newLevel; i++) {
                genNewHead();
            }
        }
        SkipListDataNode<K, V> data = new SkipListDataNode<K, V>(key, value);
        SkipListIndexNode<K, V> indexNode = new SkipListIndexNode<K, V>(data, newLevel);
        LinkedList<SkipListIndexNode> update = new LinkedList<SkipListIndexNode>();
        SkipListIndexNode<K, V> cur = head;
        while (cur != null && cur.level > newLevel) {
            cur = cur.down;
        }
        while (cur != null) {
            while (cur.right != null && cur.right.getKey().compareTo(key) < 0) {
                cur = cur.right;
            }
            update.add(cur);
            cur = cur.down;
        }
        SkipListIndexNode<K, V> bottom = null;
        while (!update.isEmpty()) {
            SkipListIndexNode prevNode = update.pollLast();
            SkipListIndexNode curLevelIndex = indexNode.genIndexNodeByLevel(prevNode.level);
            curLevelIndex.right = prevNode.right;
            prevNode.right = curLevelIndex;
            curLevelIndex.down = bottom;
            bottom = curLevelIndex;
        }
    }
​
​
    public void delete(K key) {
        LinkedList<SkipListIndexNode> update = new LinkedList<SkipListIndexNode>();
        SkipListIndexNode<K, V> cur = head;
        while (cur != null) {
            while (cur.right != null && cur.right.getKey().compareTo(key) < 0) {
                cur = cur.right;
            }
            update.add(cur);
            cur = cur.down;
        }
​
        while (!update.isEmpty()) {
            SkipListIndexNode skipListIndexNode = update.pollLast();
            if (skipListIndexNode.right != null && skipListIndexNode.right.getKey().compareTo(key) == 0) {
                skipListIndexNode.right = skipListIndexNode.right.right;
            }
        }
        while (head.right == null) {
            level--;
            head = head.down;
        }
    }
​
​ // 分层打印skiplist
    public void printList() {
        SkipListIndexNode bottom = head;
        LinkedList<SkipListIndexNode> stack = new LinkedList<SkipListIndexNode>();
        while (bottom.down != null) {
            bottom = bottom.down;
        }
        SkipListIndexNode printLevel = head;
        while (printLevel != null) {
            SkipListIndexNode printLeveltail = printLevel;
            System.out.printf("%-5s", "head->");
            SkipListIndexNode bottomTail = bottom;
            while (printLeveltail != null && bottomTail != null) {
                if (printLeveltail.right != null && printLeveltail.right.getKey().compareTo(bottomTail.right.getKey()) == 0) {
                    System.out.printf("%-5s", printLeveltail.right.getKey() + "->");
                    printLeveltail = printLeveltail.right;
                } else {
                    System.out.printf("%-5s", "");
                }
                bottomTail = bottomTail.right;
​
            }
            printLevel = printLevel.down;
            System.out.println();
        }
​
    }
​ // 通过key查询当前的节点,如果没有,则返回null
    private SkipListDataNode searchNode(K key) {
        if (key == null) {
            return null;
        }
        SkipListIndexNode<K, V> cur = head;
        while (cur != null) {
            while (cur.right != null && cur.right.getKey().compareTo(key) < 0) {
                cur = cur.right;
            }
            if (cur.right != null && cur.right.getKey().compareTo(key) == 0) {
                return cur.right.dataNode;
            }
            cur = cur.down;
        }
        return null;
    }
​ // 出现需要扩大当前层数的情况下,创建新的head
    private void genNewHead() {
        SkipListIndexNode<K, V> skipListIndexNode = new SkipListIndexNode<K, V>(null, ++level);
        skipListIndexNode.down = head;
        head = skipListIndexNode;
​
    }
​
​   // 按照概率生成level
    private int randomLevel() {
        int level = 1;
        while (random.nextDouble() < probability && level < maxLevel) {
            level++;
        }
        return level;
    }
​
​
    public static void main(String[] args) {
        SkipList<Integer, Integer> skipList = new SkipList<Integer, Integer>(12, 0.5);
        for (int i = 1; i <= 200; i++) {
            skipList.put(new Random().nextInt(200), 100 + i);
        }
        skipList.printList();
        ;
        System.out.println(skipList.get(10));
        System.out.println(skipList.get(11));
        System.out.println(skipList.get(12));
        System.out.println(skipList.getMin());
        System.out.println(skipList.getMax());
        for (int i = 0; i <= 20; i += 2) {
            skipList.delete(i);
        }
        System.out.println(skipList.get(10));
        System.out.println(skipList.get(11));
        System.out.println(skipList.get(12));
        System.out.println(skipList.getMin());
        System.out.println(skipList.getMax());
​
    }
​  // 存储数据的节点,每一份数据只保存一次
    class SkipListDataNode<K extends Comparable<K>, V> {
        private K key;
        private V value;
​
        SkipListDataNode(K key, V value) {
            this.key = key;
            this.value = value;
        }
​
    }
​// 索引节点,用来创建skiplist的结构
    class SkipListIndexNode<K extends Comparable<K>, V> {
        SkipListDataNode<K, V> dataNode;
        int level = 0;
        SkipListIndexNode<K, V> right;
        SkipListIndexNode<K, V> down;
​
        public SkipListIndexNode(SkipListDataNode<K, V> dataNode, int level) {
            this.dataNode = dataNode;
            this.level = level;
        }
​
        public SkipListIndexNode genIndexNodeByLevel(int level) {
            return new SkipListIndexNode(this.dataNode, level);
        }
​
        public V getValue() {
            return dataNode.value;
        }
​
        public K getKey() {
            return dataNode.key;
        }
    }
}
​

上面是可以直接run 起来,最后按层数打印当前的skiplist

java 复制代码
head->                                                            107->                         
head->8->                 41->                                    107->                         
head->8->                 41->                          98->      107->     177->               
head->8->                 41->      67-> 70->      92-> 98->      107->120->177->     181->     
head->8->  17-> 34-> 37-> 41-> 45-> 67-> 70-> 76-> 92-> 98-> 101->107->120->177->179->181->  
相关推荐
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk4 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*5 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue5 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man5 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer086 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml47 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠8 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#