摘要
跳表可以降低有序链表的添加、删除和搜索的平均时间复杂度。跳表的应用场景也是比较多,比如在 Redis 中使用。它的实现逻辑也是值得去学习的。
一个有序链表的搜索、添加和删除的平均时间复杂度为 O(n)。那么,是否可以利用二分搜索法优化有序链表,使得有序链表的搜索、添加和删除的平均时间复杂度降低为 O(logn)?
我们知道,数组是可以高效的随机访问的,因为数组是存在索引的。但是链表不具备索引,所以不能像有序数组那样直接使用二分搜优化。那么有什么其他方法可以让有序数组的搜索、添加和删除的平均时间复杂度为 O(logn) 呢?
答案是一定存在的,那就是跳表!!!
跳表也被称为跳跃表,或者跳跃列表,就是在有序链表的基础上增加了“跳跃”的功能。它是由 William Pugh 在 1990 年发布的,设计的初衷是为了取代平衡树(比如红黑树)。
著名的 Key-Value 数据库,比如 Redis 和 LevelDB。Redis 中的 SortedSet,LevelDB 中的 MemTable 都用到了跳表。
跳表相对于平衡树,跳表的实现和维护会更加简单。跳表的搜索、删除和添加的平均时间复杂度是 O(logn),如下图(图片来自网络):
在跳表中搜索时,按照以下步骤进行:
跳表中添加元素时,随机确定新添加元素的层数。删除元素时,整个跳表的层数也可能会降低。
跳表是按照层构建的,底层是一个普通的有序链表,高层相当于是低层的“快速通道”。
假设在第 i 层出现的元素 x,在第 i+1 层出现 x 的固定概率为 p,那么,产出越高的层数,概率就越低了,比如:
以此类推,一个元素的平均层数是 1 / (1 - p)。通常 p 的值设置为 1/2 或者 1/4,所以每个元素所包含的平均指针数量是 2 或者 1.33。
先创建一个类,在类中定义一些属性,并实现一些简单的函数:
public class SkipList {
// 最大层数
private static final int MAX_LEVEL = 32;
// 概率值
private static final double p = 0.5;
// 元素数量
private int size;
// 比较函数
private Comparator comparator;
// 有效层数
private int level;
// 不存放任何 K-V
private Node first;
public SkipList(Comparator comparator) {
this.comparator = comparator;
first = new Node<>(null, null, MAX_LEVEL);
first.nexts = new Node[MAX_LEVEL];
}
public SkipList() {
this(null);
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
comparator 属性定义时需要引入 java.util.Comparator 系统工具类。
在 SkipList 类的构造函数中,可以看到 Node 类的创建需要传入 3 个参数,其中一个是传递层数,因为 Node 中需要定义一个数组来存放指向不同层的指针:
private static class Node {
K key;
V value;
Node[] nexts;
public Node(K key, V value, int level) {
super();
this.key = key;
this.value = value;
this.nexts = new Node[level];
}
}
接下来实现添加元素函数,在实现过程中要这几步走:
public V put(K key, V value) {
keyCheck(key);
Node node = first;
Node[] prevs = new Node[level];
for (int i = level - 1; i >= 0; i--) {
int cmp = -1;
while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {
node = node.nexts[i];
}
if (cmp == 0) { // 节点存在
V oldV = node.nexts[i].value;
node.nexts[i].value = value;
return oldV;
}
prevs[i] = node;
}
// 新节点的层数
int newLevel = randomLevel();
// 添加新节点
Node newNode = new Node<>(key, value, newLevel);
// 设置前驱和后继
for (int i = 0; i < newLevel; i++) {
if (i >= level) {
first.nexts[i] = newNode;
} else {
newNode.nexts[i] = prevs[i].nexts[i];
prevs[i].nexts[i] = newNode;
}
}
// 节点数量增加
size++;
// 计算跳表的最终层数
level = Math.max(level, newLevel);
return null;
}
随机层数的函数需要 p 和 MAX_LEVEL 一起来确定:
private int randomLevel() {
int level = 1;
while (Math.random() < p && level < MAX_LEVEL) {
level ++;
}
return level;
}
搜索元素就简单很多了,就是一层层的遍历,每一层都从头遍历到尾部,直至找到元素或者全部遍历完:
public V get(K key) {
keyCheck(key);
Node node = first;
for (int i = level-1; i >= 0; i--) {
int cmp = -1;
while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {
node = node.nexts[i];
}
if (cmp == 0) return node.nexts[i].value;
}
return null;
}
删除元素和添加元素的第一步比较相似,需要多留意不同的地方:
public V remove(K key) {
keyCheck(key);
Node node = first;
Node[] prevs = new Node[level];
boolean exist = false;
for (int i = level-1; i >= 0; i--) {
int cmp = -1;
while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {
node = node.nexts[i];
}
prevs[i] = node;
if (cmp == 0) exist = true;
}
if (!exist) return null;
// 需要被删除的节点
Node removeNode = node.nexts[0];
// 设置前驱和后继 ???
for (int i = 0; i < removeNode.nexts.length; i++) {
prevs[i].nexts[i] = removeNode.nexts[i];
}
// 更新跳表的层数
int newLevel = level;
while (--newLevel >= 0 && first.nexts[newLevel] == null) {
level = newLevel;
}
// 数量减1
size--;
return removeNode.value;
}
为什么 Node
removeNode = node.nexts[0]; 就是要删除的节点? 因为跳表的最底层就是一个包含所以节点的有序链表,添加元素也是从底层开始的,node 已经是要删除节点的前驱节点了, nexts[0] 就是最底层的节点,必然指向要删除的节点。
留言与评论(共有 0 条评论) “” |