零 跳表介绍
跳表在实践中用得较少。我所知的两处应用,一个是redis中zset的实现,一个是java的ConcurrentSkipListMap与ConcurrentSkipListSet。跳表的发明者是William Pugh于1990年发明于论文Skip Lists: A Probabilistic Alternative to Balanced Trees。
跳表底层是一个链表。这个链表是按大小顺序排列的。
然后在底层数据链表上建N层索引,如图所示:
每层索引都有个起始节点,叫做头。各层的头都是对齐的,都指向第一个节点。
对于底层链表节点来说,除了value以外,只有一个属性next。
而上层的索引有三个属性value,next和down。
一 跳表的添加
跳表的添加,第一步是快速找到插入点,然后将数据插入进去。
这个因为有前几级索引,都很简单。
而什么时候建上一级索引呢?
跳表的逻辑竟然是随机数。以JDK自带的跳表为例子,其代码是这样的:
int rnd = ThreadLocalRandom.nextSecondarySeed();
if ((rnd & 0x80000001) == 0) {/*省略内部建索引代码*/}
对于跳表来说,利息的是隔一个位置建一个索引。但是如果严格这样来的话,代码会非常复杂。所以为了简化逻辑,都用随机数。
随机生成上层索引的代码。
// 生成上层
while (random.nextBoolean()) {
// 随机决定是否生成索引
final Node<T> index = new Node<>();
index.value = t;
index.down = node;
index.level = node.level + 1;
if (head.level < index.level) {
head = newHead(index);
} else {
final Node<T> tNode = findNode(t, node.level + 1);
tNode.append(index);
}
node = index;
}
如果不用随机数改成布尔值变量,一正一反会有问题的。因为用while循环去建立上级索引,所以不会出现连续两 e m s p ; emsp; emsp;emsp;个true,也就是说索引永远只有一级。
二 跳表的删除
跳表的删除,也是找。
如果找到了索引,那么一直往下删除就行了。
例如,以下跳表删除2:
如果没有找到索引,那么删最底层的链表就行了。
如果删除头,不需要创建新的头索引,只需要将索引重新指向第二个元素的索引就行。上图中就是将上两行的0改成1,再将第二行的0指向第三行的1.就完成了头部的删除。
删除头部的结果如下:
所以就是删除头的逻辑比较复杂一点。
删除头部代码如下:
if (node == head) {
// 每层都找下一个
LinkedList<Node<T>> list = new LinkedList<>();
forEachHeads(list::add);
// 最底层的第二个
final Node<T> next = list.getLast().next;
final Node<T> newHead = list.stream().filter(x -> x.next != null && x.next.value.compareTo(next.value) == 0)
.findFirst().get();
// 在新的头上建头
// 上一层指向它
// 从上一层开始把指改了。
forEachHeads(x->x.value = newHead.next.value);
// 上一层的指向新的head
final Node<T> tNode = list.stream().filter(x -> x.level == newHead.level + 1).findFirst().get();
tNode.down = newHead.next;
return;
}
源码仓库
Git地址:https://e.coding.net/buildt/learn/skiplist.git