从零实现一个跳表。

概念

wiki:

In computer science, a skip list is a probabilistic data structure that allows O(logn) search complexity as well as O(logn) insertion complexity within an ordered sequence of n elements. Thus it can get the best features of a sorted array (for searching) while maintaining a linked list-like structure that allows insertion, which is not possible with a static array. Fast search is made possible by maintaining a linked hierarchy of subsequences, with each successive subsequence skipping over fewer elements than the previous one (see the picture below on the right).

跳表是一种优雅的数据结构。

简单来说,跳表是链表+二分的合体,结构以及查询的过程如图所示:

通过在一层链表基础上增加上层索引,使得查询可以利用结构上的二分。

今天我们来实现一个基本的跳表。

提供的功能包括:

  • 查询:V get(K key)
  • 插入:void put(K key, V val)

实现

为了方便理解,我这里以SimpleSkipList2为例。

生成随机level时,默认使用0.5的下一层生成概率。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/**
 * 基于上下左右指针结构的跳表
 *
 * @author dragonsong  @date 2022/6/28
 */
public class SimpleSkipList2<K extends Comparable<K>, V> {


    /**
     * 头节点位于跳表最左上,为查询起点
     */
    Node<K, V> head;
    /**
     * k-v 个数
     */
    int size;
    /**
     * 生成数据的最大层数
     */
    int maxLevel;

    public SimpleSkipList2() {
        head = new Node<>(MAX_LEVEL);
        Node<K, V> curr = head;
        // head每层都生成
        while (curr.level != 0) {
            Node<K, V> lower = curr.copyWithLowerLevel();
            curr.down = lower;
            lower.up = curr;
            curr = curr.down;
        }
        size = 0;
        maxLevel = 0;
    }

    V get(K key) {
        Node<K, V> node = getNode(key);
        if (node == null || node.key == null || node.key.compareTo(key) != 0) {
            return null;
        }
        return node.val;
    }

    /**
     * @param key 待查找的key
     * @return 查找链表中等值元素,或者查找链路上的prev前序节点
     */
    Node<K, V> getNode(K key) {
        Node<K, V> curr = head;
        while (curr != null) {
            // 先同层往右走
            Node<K, V> next = curr.next;

            if (next == null) {
                // 只有head的情况
                if (curr.down == null) {
                    return curr;
                }
                curr = curr.down;
            } else {
                // 已经有数据插入的情况
                // 先同层向右找
                while (next != null && next.key.compareTo(key) <= 0) {
                    curr = next;
                    next = next.next;
                }
                // 无down节点时,返回待插入位置的前序节点
                if (curr.key != null && curr.key.compareTo(key) == 0 || curr.down == null) {
                    return curr;
                }

                // 再往下走
                curr = curr.down;
            }

        }
        return null;
    }

    void put(K key, V val) {
        Node<K, V> prev = getNode(key);
        if (prev == null) {
            throw new IllegalStateException();
        }
        if (prev.key != null && prev.key.compareTo(key) == 0) {
            // 存在:直接更新
            prev.val = val;
            return;
        }
        int level = SkipListLevelUtils.genRandomLevel();
        maxLevel = Math.max(level, maxLevel);

        while (prev.level > 0) {
            prev = prev.down;
        }

        Node<K, V> down = null;
        // 从0往上创建
        for (int i = 0; i < level; i++) {
            Node<K, V> newNode = new Node<>(key, val, i);

            insertHorizontal(prev, newNode);
            insertVertical(down, newNode);

            down = newNode;
            // 寻找下一个prev,有上取上,无上往回倒
            while (prev.up == null) {
                prev = prev.prev;
            }
            prev = prev.up;
        }
        ++size;
    }

    /**
     * 纵向插入
     *
     * @param down    下面的节点
     * @param newNode 新节点
     */
    private void insertVertical(Node<K, V> down, Node<K, V> newNode) {
        if (down == null) {
            return;
        }
        newNode.down = down;
        down.up = newNode;
    }

    /**
     * 横向插入
     *
     * @param prev    原前序节点
     * @param newNode 新节点
     */
    private void insertHorizontal(Node<K, V> prev, Node<K, V> newNode) {
        Node<K, V> prevNext = prev.next;
        prev.next = newNode;
        if (prevNext != null) {
            prevNext.prev = newNode;
        }
        newNode.next = prevNext;
        newNode.prev = prev;
    }

    /**
     * 逐层打印跳表结构
     */
    public void print() {
        Node<K, V> start = head;
        while (start.level > maxLevel) {
            start = start.down;
        }

        System.out.println("maxLevel:" + maxLevel);

        Node<K, V> curr;
        while (start != null && start.level >= 0) {
            System.out.println("当前层数:" + start.level);
            curr = start;
            while (curr != null) {
                System.out.print(curr + "-->");
                curr = curr.next;
            }
            System.out.println();
            start = start.down;
        }
    }


    /**
     * 链表节点
     *
     * @param <K> 键类型,可排序
     */
    static class Node<K extends Comparable<K>, V> {
        /**
         * 保存的键类型
         */
        K key;
        /**
         * 保存的值类型
         */
        V val;
        /**
         * 层数
         */
        int level;
        /**
         * 上下前后节点引用
         */
        Node<K, V> prev, next, up, down;

        public Node(int level) {
            this.level = level;
        }

        public Node(K key, V val, int level) {
            this.key = key;
            this.val = val;
            this.level = level;
        }

        public Node<K, V> copyWithLowerLevel() {
            return new Node<>(this.key, this.val, this.level - 1);
        }

        @Override
        public String toString() {
            return "Node{" + "key=" + key + ", val=" + val + '}';
        }
    }
}

关键设计

链表节点 Node

在双向链表基础上,我们增加了上下指针,同时为了方便层数判断,也增加了一个节点的层数。

查询过程 getNode

getNode是实现跳表的核心算法。

核心过程:

  • 先同层往右走
  • 再往下找
  • 一直找到最后一层的最右,如果没有等值节点,返回查找最接近的前序节点
  • 注意边界

添加数据

getNode基础上增加了链表关联指针的逻辑、生成随机层数。

链表指针关联

代码中我们抽出了两个方法,熟悉链表的同学对这个应该没有问题:

1
2
insertHorizontal(prev, newNode);
insertVertical(down, newNode);

生成随机层数

我们实现的时候假定生成每层的概率是可调整的,复杂度分析按照0.5概率来判断。工程中这里可做调整,甚至可以优化。

这里可以看看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
* 1层概率为 DEFAULT_PROBABILITY
* 2层概率为 DEFAULT_PROBABILITY^2
* n层概率为 DEFAULT_PROBABILITY^n
* 越上层,生成概率越低,分母指数级上升
*
* @return level范围为 [MIN_LEVEL,MAX_LEVEL) 左闭右开
*/
public static int genRandomLevel() {
    int level = MIN_LEVEL;
    while (Math.random() <= DEFAULT_PROBABILITY && level < MAX_LEVEL) {
        ++level;
    }
    return level;
}

这里其实还可以多了解一点等概率,之前我写过一点基本的代码: https://github.com/redolog/algorithm-java/tree/main/src/main/java/com/algorithm/probability

复杂度分析

空间

每一层生成的基础概率为0.5。

此时我们的层数分布遵循:最高层维护两个数据,每降低一层 数量减半。 层高设为h,数据量n:

  • 第一层 n/2
  • 第二层 n/2^2
  • 第h层 n/(2^h)=2

所以 h=logn

n/2+n/4+n/8…+8+4+2=n-2 为各层节点的数量和。

空间复杂度 O(n),索引层为额外的空间。

时间

写入、读取复杂度一致,均取决于每层跳转的节点数 以及 跳表高度。

读取过程:

  • 从最上层一直往下寻找,最顶层只有 head/tail 元素,顶层最多跳转到tail,大部分情况是从head往下跳。
  • 到了倒数第二层,上一步已经跳过了一半元素,根据同样的步骤,查找元素时,要么不跳往下,要么跳一次(跳两次就到了上一次判断过的边) 因此,每一层跳跃,最多经过三个节点。

此时写入、读取的复杂度为 O(3*log2n),去掉常数阶、低阶,复杂度为 O(logn)

根据大O算法的衡量标准,跳表的时间复杂度与二分一致。

总结

以上,实现了最基础的跳表结构,并且分析了复杂度,同时简要提及了关键性的设计。

后续可以结合工业实现来进行优化、对比。

Ref