跳表 Skip List
跳表(Skip List)是一种数据结构,它在有序链表的基础上,通过增加多级索引来提高查找、插入和删除操作的效率。
跳表的平均时间复杂度为 O(log n),与平衡树(如红黑树、AVL树)相当,但实现起来更为简单。
跳表的结构
跳表由多层链表组成,每一层都是一个有序链表。底层(第0层)是一个完整的有序链表。每向上一层,链表中的节点数量减少,且上一层的节点都是下一层节点的子集。
示例图解
下面是一个简单的跳表结构的图示:
Level 3: [Header] ------------------------> [8] ------------------------> [null]
Level 2: [Header] --------------> [4] ------------------------> [8] -----> [null]
Level 1: [Header] --> [2] ------> [4] --> [5] --> [7] -------> [8] -----> [10] -----> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> [7] -> [8] -> [9] -> [10] -> [null]
解释
-
节点结构:每个节点包含多个指向不同层的前进指针。例如,节点
[4]
在第0层、第1层和第2层都有指针,但在第3层没有指针。 -
查找操作:
- 查找元素
7
:从最高层开始,顺着指针找到最接近且不超过7
的元素。在图示中,首先从Header
走到4
,然后从4
走到5
,再从5
走到第1层的7
。 - 查找路径:
Header -> 4 (Level 2) -> 5 (Level 1) -> 7 (Level 1)
。
- 查找元素
-
插入操作:
- 插入元素时,先找到该元素在每一层的插入位置,然后根据随机数决定该元素的高度,并插入到相应的层中。
-
删除操作:
- 删除元素时,先找到该元素在每一层的位置,然后将其从每一层中删除。假设要删除
7
,就需要从第0层、第1层中删除节点7
。
- 删除元素时,先找到该元素在每一层的位置,然后将其从每一层中删除。假设要删除
节点层数的选择标准
跳表的每一层节点都是上一层节点的子集,通常通过随机化策略来决定哪些节点会出现在更高层。具体来说,当插入一个新节点时,通过随机算法来决定这个节点的高度。每一层节点的选择标准是独立的,通常使用几何分布或类似的随机分布。
在 Java 中实现跳表时,常用的方式是通过一个随机函数来决定节点的层数。在经典的 SkipList 实现中,每一个节点的高度由一个随机函数生成,这个随机函数会在每一层上有一定概率(通常是 0.5)决定是否将节点提升到更高一层。
示例:Java 中跳表的实现
以下是一个完整的 Java 跳表实现示例,展示了如何使用随机化策略来决定节点的高度,并维护多级链表:
import java.util.Random;
class SkipListNode {
int value;
SkipListNode[] forward;
public SkipListNode(int value, int level) {
this.value = value;
this.forward = new SkipListNode[level + 1];
}
}
public class SkipList {
private static final int MAX_LEVEL = 16;
private final SkipListNode header = new SkipListNode(-1, MAX_LEVEL);
private int level = 0;
private final Random random = new Random();
// 插入方法
public void insert(int value) {
SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
SkipListNode current = header;
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null && current.forward[i].value < value) {
current = current.forward[i];
}
update[i] = current;
}
int nodeLevel = randomLevel();
if (nodeLevel > level) {
for (int i = level + 1; i <= nodeLevel; i++) {
update[i] = header;
}
level = nodeLevel;
}
SkipListNode newNode = new SkipListNode(value, nodeLevel);
for (int i = 0; i <= nodeLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
}
// 查找方法
public boolean search(int value) {
SkipListNode current = header;
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null && current.forward[i].value < value) {
current = current.forward[i];
}
}
current = current.forward[0];
return current != null && current.value == value;
}
// 删除方法
public void delete(int value) {
SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
SkipListNode current = header;
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null && current.forward[i].value < value) {
current = current.forward[i];
}
update[i] = current;
}
current = current.forward[0];
if (current != null && current.value == value) {
for (int i = 0; i <= level; i++) {
if (update[i].forward[i] != current) break;
update[i].forward[i] = current.forward[i];
}
while (level > 0 && header.forward[level] == null) {
level--;
}
}
}
// 随机层数生成方法
private int randomLevel() {
int lvl = 0;
while (lvl < MAX_LEVEL && random.nextInt(2) == 0) {
lvl++;
}
return lvl;
}
public static void main(String[] args) {
SkipList skipList = new SkipList();
skipList.insert(3);
skipList.insert(6);
skipList.insert(7);
skipList.insert(9);
skipList.insert(12);
skipList.insert(19);
System.out.println(skipList.search(6)); // true
System.out.println(skipList.search(15)); // false
skipList.delete(6);
System.out.println(skipList.search(6)); // false
}
}
解释
-
节点类 (
SkipListNode
):每个节点包含一个值和一个指针数组,指向每一层的下一个节点。 -
插入方法 (
insert
):- 使用
update
数组记录在每一层插入新节点的位置。 - 从最高层开始,找到需要插入的位置。
- 调用
randomLevel
方法随机生成节点的高度。 - 如果生成的节点高度比当前跳表的高度大,更新跳表的高度。
- 插入新节点,并更新相关的指针。
- 使用
-
查找方法 (
search
):- 从最高层开始,逐层查找目标值。
- 如果找到则返回
true
,否则返回false
。
-
删除方法 (
delete
):- 使用
update
数组记录在每一层删除节点的位置。 - 从最高层开始,找到需要删除的位置。
- 删除节点,并更新相关的指针。
- 如果某一层为空,减少跳表的高度。
- 使用
-
随机层数生成方法 (
randomLevel
):- 随机生成一个节点的高度,使用几何分布,概率为 0.5。
关键点
- 随机层数生成:通过
randomLevel
方法来决定新节点的高度,使用nextInt(2)
模拟掷硬币的概率,层数越高,节点数量越少。 - 多级链表:使用多层链表来加速查找、插入和删除操作,平均时间复杂度为 O(log n)。
- 更新指针:插入和删除操作需要更新多层链表的指针。
跳表的优点
- 高效:查找、插入和删除的时间复杂度平均为 (O(\log n))。
- 简单:相对于平衡树,跳表实现起来更为简单。
- 灵活:通过调整层数和概率,可以在空间和时间复杂度之间进行权衡。
跳表实现时为什么选择随机算法决定level
跳表选择随机算法决定节点的层级(level),而不是平均分布,主要基于以下几个考虑:
-
平衡性和性能:随机算法可以在一定程度上保证跳表的平衡性。通过随机确定每个节点的层级,可以避免节点集中在某一层,从而保持跳表在不同层次上的均衡分布。这种均衡性有助于提高搜索、插入和删除操作的性能,因为在平衡的情况下,操作的时间复杂度更稳定。
-
避免确定性结构:如果使用平均分布,每个节点的层级可能会逐渐递增或递减,导致跳表的结构不够随机化。这种确定性结构可能会在特定操作序列下导致性能的不稳定性或者退化。
-
简单性和效率:随机算法相对于确定性分布更加简单且高效。随机生成节点层级只需要简单的随机数生成器,而不需要复杂的计算或者存储结构,这在实现和维护上都更加方便。
-
概率均衡:虽然是随机的,但在实践中,随机算法可以通过调整概率分布来控制节点层级的期望分布。比如,可以调整随机算法的概率参数,以达到期望的分布特性,如更倾向于生成较低层次的节点,或者在特定场景下生成更高层次的节点。
跳表选择随机算法决定节点层级,是为了在保持操作效率和简单性的同时,保持跳表结构的均衡性和性能稳定性。这种随机化的设计使得跳表在实际应用中能够更好地应对不同数据分布和操作序列的挑战。
高层节点的值在低层一定会出现吗
不完全对。跳表中的节点分布并不是高层的节点会出现在每一个低层,而是每个节点在更高的层次上都有可能存在。
🙋 如果高层节点不会在低层都出现,那查询的时候也不会逐层向下查吗?
高层节点不一定会在每一个低层都出现,但查询操作仍然需要逐层向下查找。
🙋 如果低层不一定存在,那高层查到接近值的时候,下一层又需要从头开始查吗?
在跳表中,即使在较高的层级找到了接近目标值的节点,但在下一层仍然需要从头开始查找。这是因为跳表的每一层级都是一个独立的有序链表,查找操作必须按照链表的顺序进行,无法跳过中间节点。
🙋 为什么不在不同层级间都包含高层节点?
理解跳表为什么不在每个低层级都包含所有高层级的节点,而是选择随机分布节点的策略,涉及到跳表设计的核心思想和性能优化考虑:
-
空间效率:
- 如果每个低层级都包含所有高层级的节点,跳表将会占用大量的额外空间。跳表的空间复杂度与其高度相关,如果每个节点都在每一层出现,空间开销将会显著增加。
- 通过随机分布节点的策略,跳表可以在平均情况下保持较低的空间复杂度,只需存储每个节点在其随机确定的几个层级上的指针。
-
维护成本:
- 如果每个低层级都包含所有高层级的节点,那么在插入、删除等操作时,需要同时更新多个层级的指针,增加了操作的复杂性和维护成本。
- 随机分布节点的策略简化了插入和删除操作的实现,因为只需更新几个层级的指针,而不用考虑每个层级都要更新。
-
查询效率:
- 跳表的设计目标是通过多层级的跳跃指针来加速查找操作。即使某些低层级可能不包含高层级的节点,由于跳跃指针的存在,平均情况下仍能够快速定位到目标节点或接近目标节点的位置。
- 随机分布节点的策略能够平衡查询操作的性能和空间开销,保证了跳表在实际应用中的高效性。
-
概率均衡:
- 跳表的随机分布节点策略允许调整随机算法的参数,以达到期望的节点分布特性。可以根据具体的应用场景和性能需求,调整节点在每个层级上出现的概率,以优化跳表的性能和空间使用效率。
跳表选择随机分布节点的策略,而不是在每个低层级都包含所有高层级的节点,是为了优化空间复杂度、简化维护操作,并在平均情况下保持高效的查询性能。这种设计在兼顾空间和时间效率方面达到了一个良好的平衡。
🙋 那删除节点也需要每次遍历判断是否存在,然后决定是否需要删除吗?
删除节点需要在每个层级上找到待删除的节点,然后执行删除操作。跳表的设计允许通过跳跃指针快速定位到目标节点,使得删除操作在平均情况下能够以较高效率完成。
举例说明构建过程 包含1-100的跳表
构建一个包含1到100的跳表可以通过以下步骤来完成。我们将依次插入1到100的数字,同时随机决定每个节点的高度。下面是详细的步骤和图示。
步骤1:初始化跳表
首先,我们初始化跳表,包括头节点和各层指针。我们假设跳表的最大高度为4。
class SkipListNode {
int value;
SkipListNode[] forward;
public SkipListNode(int value, int level) {
this.value = value;
this.forward = new SkipListNode[level + 1];
}
}
public class SkipList {
private static final int MAX_LEVEL = 4;
private final SkipListNode header = new SkipListNode(-1, MAX_LEVEL);
private int level = 0;
// Random level generation, insert, and other methods
}
步骤2:随机生成节点高度
private int randomLevel() {
int lvl = 0;
while (lvl < MAX_LEVEL && Math.random() < 0.5) {
lvl++;
}
return lvl;
}
步骤3:插入节点
我们插入1到100的数字,并且为每个数字生成一个随机高度。
public void insert(int value) {
SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
SkipListNode current = header;
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null && current.forward[i].value < value) {
current = current.forward[i];
}
update[i] = current;
}
int nodeLevel = randomLevel();
if (nodeLevel > level) {
for (int i = level + 1; i <= nodeLevel; i++) {
update[i] = header;
}
level = nodeLevel;
}
SkipListNode newNode = new SkipListNode(value, nodeLevel);
for (int i = 0; i <= nodeLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
}
构建跳表的过程
我们以节点1到节点10为例来说明构建跳表的过程。
插入节点1
1的高度为0(假设随机生成高度为0)。
Level 0: [Header] -> [1] -> [null]
插入节点2
2的高度为1(假设随机生成高度为1)。
Level 1: [Header] -> [2] -> [null]
Level 0: [Header] -> [1] -> [2] -> [null]
插入节点3
3的高度为0(假设随机生成高度为0)。
Level 1: [Header] -> [2] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [null]
插入节点4
4的高度为2(假设随机生成高度为2)。
Level 2: [Header] -> [4] -> [null]
Level 1: [Header] -> [2] -> [4] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [null]
插入节点5
5的高度为1(假设随机生成高度为1)。
Level 2: [Header] -> [4] -> [null]
Level 1: [Header] -> [2] -> [4] -> [5] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [5] -> [null]
插入节点6
6的高度为0(假设随机生成高度为0)。
Level 2: [Header] -> [4] -> [null]
Level 1: [Header] -> [2] -> [4] -> [5] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> [null]
插入节点7
7的高度为1(假设随机生成高度为1)。
Level 2: [Header] -> [4] -> [null]
Level 1: [Header] -> [2] -> [4] -> [5] -> [7] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> [7] -> [null]
插入节点8
8的高度为3(假设随机生成高度为3)。
Level 3: [Header] -> [8] -> [null]
Level 2: [Header] -> [4] -> [8] -> [null]
Level 1: [Header] -> [2] -> [4] -> [5] -> [7] -> [8] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> [7] -> [8] -> [null]
插入节点9
9的高度为0(假设随机生成高度为0)。
Level 3: [Header] -> [8] -> [null]
Level 2: [Header] -> [4] -> [8] -> [null]
Level 1: [Header] -> [2] -> [4] -> [5] -> [7] -> [8] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> [7] -> [8] -> [9] -> [null]
插入节点10
10的高度为1(假设随机生成高度为1)。
Level 3: [Header] -> [8] -> [null]
Level 2: [Header] -> [4] -> [8] -> [null]
Level 1: [Header] -> [2] -> [4] -> [5] -> [7] -> [8] -> [10] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> [7] -> [8] -> [9] -> [10] -> [null]
最终跳表结构
完成所有节点的插入后,跳表结构将会类似以下形式:
Level 3: [Header] -> [8] -> [null]
Level 2: [Header] -> [4] -> [8] -> [null]
Level 1: [Header] -> [2] -> [4] -> [5] -> [7] -> [8] -> [10] -> [null]
Level 0: [Header] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> [7] -> [8] -> [9] -> [10] -> [null]
在这一跳表结构中,节点分布在不同层次上,每层的节点数目递减,而底层(Level 0)包含所有的节点。通过这种结构,跳表能够在较高的层次上快速缩小查找范围,提高查找效率。
应用场景
跳表是一种高效的数据结构,具有较快的查找、插入和删除操作,因此在许多常见类库和中间件中得到了广泛应用。以下是一些使用跳表的常见类库和中间件实例:
1. Java 中的 ConcurrentSkipListMap
Java 的 java.util.concurrent
包中的 ConcurrentSkipListMap
类是跳表的一个具体实现。ConcurrentSkipListMap
提供了一个线程安全的、基于跳表的 NavigableMap
实现,支持并发访问和修改。
2. Redis
Redis 是一个开源的内存数据结构存储系统,支持多种数据结构,包括字符串、哈希、列表、集合和有序集合。跳表在 Redis 中用于实现有序集合(sorted set)。有序集合通过跳表实现,使得其操作(如插入、删除、查找和范围查找)具有较高的效率。
3. LevelDB
LevelDB 是 Google 开发的一个开源的、基于磁盘的键值存储库,它广泛应用于需要高性能读写操作的场景。LevelDB 使用跳表来管理内存中的数据(memtable)。跳表允许高效的顺序访问和随机访问,同时支持快速插入和删除操作。
4. Apache Cassandra
Apache Cassandra 是一个开源的分布式数据库管理系统,设计用于处理大量的数据跨许多商品服务器。Cassandra 的内部存储引擎使用了跳表来实现内存表(memtable),以便在高并发情况下快速插入和查询数据。
5. ScyllaDB
ScyllaDB 是 Apache Cassandra 的一种高性能替代方案,专为现代硬件架构(如多核处理器和大容量内存)而设计。ScyllaDB 也使用跳表来实现内存表(memtable),以提高数据操作的效率。