1. 概念
跳表是由 William Pugh 发明的一种查找数据结构,支持对数据的快速查找,插入和删除。
跳表的期望空间复杂度为 O ( n ) O(n) O(n), 跳表的查询,插入和删除操作的期望时间复杂度都为 O ( l o g n ) O(logn) O(logn)。
2. 基本思想
顾名思义,跳表是一种类似于链表的数据结构。更加准确的说,跳表是对有序链表的改进。
为方便讨论,后续所有有序链表默认为升序排序。
一个有序链表的查找操作,就是从头部开始逐个比较,直到当前节点的值大于或者等于目标节点的值。很明显,这个操作的复杂度是 O ( n ) O(n) O(n)
跳表在有序链表的基础上,引入了分层的概念。首先,跳表的每一层都是一个有序链表,特别的,最底层是初始的有序链表。每个位于第 i i i 层的节点由 p p p 的概率上升到第 i + 1 i + 1 i+1 层, p p p 为常数。
记在 n n n 个节点的跳表中,期望包含 1 p \frac{1}{p} p1 个元素的层为第 L ( n ) L(n) L(n) 层,易得 L ( n ) = l o g 1 p n L(n) = log_{\frac{1}{p}}n L(n)=logp1n。
在跳表中查找,就是从第 L ( n ) L(n) L(n) 层开始,水平地逐个比较直至当前节点的下一个节点大于等于目标节点,然后移动至下一层。重复这个过程直至到达第一层且无法继续进行操作。此时,若下一个节点是目标节点,则成功查找;反之,则元素不存在。这样一来,查找的过程中会跳过一些没有必要的比较,所以相比于有序链表的查询,跳表的查询更快。可以证明,跳表查询的平均复杂度为 O ( l o g n ) O(logn) O(logn)。
3. 算法实现
3. 1 跳表节点结构
跳表的节点类型是链表,但由于在跳表中,一个节点可能有 p p p 的概率上升,因此它的指针可能有多个,指向它上升的层。为了便于跳表的操作,要保持这些指针的相对顺序,因此用数组来存储。
next[i] != null
表示 当前节点上升到了第i
层,且指针也指向了第i
层的下一个。
// 跳表节点,存储元素,一个元素最大可能有 maxLevel 层
class SkiplistNode{
int val;
// 指向后面的指针,由于一个元素可能占据多层,所以有多个,
SkiplistNode[] next;
// maxLevel 表示上升的层数
public SkiplistNode(int val, int maxLevel){
this.val = val;
this.next = new SkiplistNode[maxLevel];
}
}
3.2 节点上升的层数
新加入的节点有 p p p 的概率上升,因此节点可能会上升多层,使用
Random
函数来模拟概率累计上升的层数,最大上升到设定的最大层数。
/**
* 功能:返回一个新插入的节点可能会上升的层数
*/
private int randomLevel(){
int lv = 1;
/*
随机生成 lv
*/
while (random.nextDouble() < P_FACTOR && lv < MAX_LEVEL){
lv ++;
}
return lv;
}
3.3 跳表查询target
从跳表的最上层
level
开始,每一层水平的遍历,直到当前节点的下一个节点大于或等于target
。然后转到下一层,重复上述过程,直到第一层遍历完成。之后当前节点在第一层的下一个节点就是第一个大于或等于target
的节点,如果这个节点存在且值等于target
,则返回true
,否则返回false
。
public boolean search(int target) {
// 指向虚拟头节点
SkiplistNode curr = this.head;
for (int i = level - 1; i >= 0; i --) {
/* 找到第 i 层小于且最接近 num 的元素*/
while (curr.next[i] != null && curr.next[i].val < target){
curr = curr.next[i];
}
}
// 经过上面的遍历,curr 现在指向第一层 最后一个小于 target 的节点,
// 因此现在向后移一位到达第一个大于或等于 target 的节点。
curr = curr.next[0];
// target 存在
if(curr != null && curr.val == target){
return true;
}
return false;
}
3.4 跳表插入num
从跳表的最上层
level
开始,每一层水平的遍历,直到当前节点的下一个节点大于或等于target
。然后转到下一层,重复上述过程,直到第一层遍历完成。在每一层的遍历中,创建一个update
节点数组。update[i]
用来存储每一层最后一个小于num
的值。因此num
至少需要插入update[0]
的后面。不过跳表的节点会上升,使用上面的算法求出给节点的上升数lv
,如果lv
超过当前最大层level
则还要更新level
。因此要插入的区间就是update[0] ~ update[lv -1]
这些层的后面。创建一个跳表节点newNode
, 它的next
指针数是lv
, 之后插入对应的update[i]
后面。
public void add(int num) {
SkiplistNode[] update = new SkiplistNode[MAX_LEVEL];
Arrays.fill(update, head);
SkiplistNode curr = this.head;
for (int i = level - 1; i >= 0; i--) {
/* 找到第 i 层小于且最接近 num 的元素*/
while (curr.next[i] != null && curr.next[i].val < num){
curr = curr.next[i];
}
/* 记录每一层最后一个小于 num 的节点 */
update[i] = curr;
}
// 节点在 P_FACTOR 概率下可能上升的层数,最大上升 MAX_LEVEL 层
int lv = randomLevel();
// 更新当前最大节点
level = Math.max(level, lv);
// 生成 num 跳表节点,该节点有 lv 层, 进行插入
SkiplistNode newNode = new SkiplistNode(num, lv);
for (int i = 0; i < lv; i++) {
/* 新节点第 i 层的 next 节点指向 update[i] 的 next */
newNode.next[i] = update[i].next[i];
/* update[i] 的 next 指向 newNode */
update[i].next[i] = newNode;
}
}
3.5 跳表删除num
从跳表的最上层
level
开始,每一层水平的遍历,直到当前节点的下一个节点大于或等于target
。然后转到下一层,重复上述过程,直到第一层遍历完成。在每一层的遍历中,创建一个update
节点数组。update[i]
用来存储每一层最后一个小于num
的值。遍历结束之后当前节点在第一层的下一个节点就是第一个大于或等于num
的节点,如果不存在或值不是num
,说明删除的节点不存在,如果存在。则要将第一层以及它的上升层的节点全部删掉。之后再遍历层数,维护level
public boolean erase(int num) {
SkiplistNode[] update = new SkiplistNode[MAX_LEVEL];
SkiplistNode curr = this.head;
for (int i = level - 1; i >= 0; i --) {
/* 找到第 i 层小于且最接近 num 的元素*/
while (curr.next[i] != null && curr.next[i].val < num){
curr = curr.next[i];
}
update[i] = curr;
}
// curr 指向第 1 层 要删除的 num 节点
curr = curr.next[0];
/* 如果值不存在则返回 false */
if(curr == null || curr.val != num){
return false;
}
// 从第一层开始,向上删除 num 节点
for (int i = 0; i < level; i++) {
// 上面没有了,说明 num 不在上升了,删除完了
if(update[i].next[i] != curr){
break;
}
// 删除节点
update[i].next[i] = curr.next[i];
}
// 更新当前的 level
while (level > 1 && head.next[level - 1] == null){
level --;
}
return true;
}
3. 6 完整版代码
import java.util.Arrays;
import java.util.Random;
public class Skiplist {
// 最大层数
static final int MAX_LEVEL = 32;
// 节点上升的概率
static final double P_FACTOR = 0.25;
// 跳表节点,存储元素的基本单位
private SkiplistNode head;
// 当前最大层
private int level;
// 随机概率
private Random random;
public Skiplist() {
// 跳表存储空间初始化
this.head = new SkiplistNode(-1, MAX_LEVEL);
this.level = 0;
this.random = new Random();
}
/**
* 跳表查询API
* 从最大层 level 开始查找,在当前层水平逐个比较,
* 直到当前节点的下一个节点大于等于目标节点,然后移动至下一层,
* 重复这个过程直到到达第一层。
* 此时,若第一层的下一个节点的值等于target,则返回 true;反之返回 false
* @param target
* @return
*/
public boolean search(int target) {
// 指向虚拟头节点
SkiplistNode curr = this.head;
for (int i = level - 1; i >= 0; i --) {
/* 找到第 i 层小于且最接近 num 的元素*/
while (curr.next[i] != null && curr.next[i].val < target){
curr = curr.next[i];
}
}
// 经过上面的遍历,curr 现在指向第一层 最后一个小于 target 的节点,
// 因此现在向后移一位到达第一个大于或等于 target 的节点。
curr = curr.next[0];
// target 存在
if(curr != null && curr.val == target){
return true;
}
return false;
}
/**
* 跳表插入API
* 从最大层 level 开始查找,在当前层水平逐个比较,
* 直到当前节点的下一个节点大于等于目标节点,然后移动至下一层,
* 重复这个过程直到到达第一层。
* 设新加入的节点为 newNode,我们需要计算出此次节点插入的层数 lv,
* 如果 level 小于 lv,则同时需要更新 level。
* 用数组 update 保存每一层查找的最后一个节点,
* 第 i 层最后的节点为 update[i]。我们将 newNode 的后续节点指向 update[i] 的下一个节点,
* 同时更新 update[i]update[i]update[i] 的后续节点为 newNode
* @param num
*/
public void add(int num) {
SkiplistNode[] update = new SkiplistNode[MAX_LEVEL];
Arrays.fill(update, head);
SkiplistNode curr = this.head;
for (int i = level - 1; i >= 0; i--) {
/* 找到第 i 层小于且最接近 num 的元素*/
while (curr.next[i] != null && curr.next[i].val < num){
curr = curr.next[i];
}
/* 记录每一层最后一个小于 num 的节点 */
update[i] = curr;
}
// 节点在 P_FACTOR 概率下可能上升的层数,最大上升 MAX_LEVEL 层
int lv = randomLevel();
// 更新当前最大节点
level = Math.max(level, lv);
// 生成 num 跳表节点,该节点有 lv 层, 进行插入
SkiplistNode newNode = new SkiplistNode(num, lv);
for (int i = 0; i < lv; i++) {
/* 新节点第 i 层的 next 节点指向 update[i] 的 next */
newNode.next[i] = update[i].next[i];
/* update[i] 的 next 指向 newNode */
update[i].next[i] = newNode;
}
}
/**
* 跳表删除API
* 首先查找当前元素是否在跳表中。如果在:
* 用数组 update 保存每一层最后一个大于等于 target 的节点,
* 第 i 层最后的节点为 update[i]
* 此时,第 i 层的下一个节点的值为 num, 需要进行删除
* 由于第 i 层的以 p 的概率出现在第 i + 1 层,因此应当从第 1 层
* 开始向上进行更新,将 num 从 update[i] 的下一条中删除
* 同时更新 update[i] 的后续节点,直到当前层的链表中没有出现 num 的节点位置。
* 最后还需要更新跳表中当前的最大层数 level
* @param num
* @return
*/
public boolean erase(int num) {
SkiplistNode[] update = new SkiplistNode[MAX_LEVEL];
SkiplistNode curr = this.head;
for (int i = level - 1; i >= 0; i --) {
/* 找到第 i 层小于且最接近 num 的元素*/
while (curr.next[i] != null && curr.next[i].val < num){
curr = curr.next[i];
}
update[i] = curr;
}
// curr 指向第 1 层 要删除的 num 节点
curr = curr.next[0];
/* 如果值不存在则返回 false */
if(curr == null || curr.val != num){
return false;
}
// 从第一层开始,向上删除 num 节点
for (int i = 0; i < level; i++) {
// 上面没有了,说明 num 不在上升了,删除完了
if(update[i].next[i] != curr){
break;
}
// 删除节点
update[i].next[i] = curr.next[i];
}
// 更新当前的 level
while (level > 1 && head.next[level - 1] == null){
level --;
}
return true;
}
/**
* 功能:返回一个新插入的节点可能会上升的层数
*/
private int randomLevel(){
int lv = 1;
/*
随机生成 lv
*/
while (random.nextDouble() < P_FACTOR && lv < MAX_LEVEL){
lv ++;
}
return lv;
}
// 跳表节点,存储元素,一个元素最大可能有 maxLevel 层
class SkiplistNode{
int val;
// 指向后面的指针,由于一个元素可能占据多层,所以有多个,
SkiplistNode[] next;
public SkiplistNode(int val, int maxLevel){
this.val = val;
this.next = new SkiplistNode[maxLevel];
}
}
}