golang/java实现跳表的数据结构
1、跳表数据结构说明
最近在写一款中间件,类似于redis的中间件,我准备用golang语言来编写,目前已经实现了redis的大部分数据结构,比如string、hash、set、list,而开始实现zset的时候发现要使用跳表,在网上实现的跳表数据结构感觉都不太合适,所以就自己来实现了一个,我也是瞅着空闲的时间来实现了一把,我也不知道性能如何,还没来得及测试,欢迎大家在此基础上进行修正和讨论;
跳表(Skip List)是一种概率性的动态数据结构,通常用于实现有序集合(如集合、映射等)的查找、插入和删除操作。它能够在平均情况下提供对数时间复杂度(O(log n)),比起传统的链表和二叉搜索树,它在实现上更简单。
1.1、跳表的基本结构
跳表由多个层级的链表构成,每一层都是对下一层链表的“跳跃”,因此得名“跳表”。最底层是一个有序的链表,其上层链表则是从下层链表中按一定规则选出的元素组成。跳表的结构通常由以下几个层级构成:
第0层(底层):这是最基础的一层,包含了所有的元素,形成一个有序链表。
第1层及以上:这些层是通过概率性的方式从第0层元素中选择一部分元素构成的。在每一层,节点的出现概率通常是固定的(例如每个节点有50%的概率出现在上一层)。
1.2、跳表的操作
查找(Search):
从最上层的链表开始查找,按照节点的值进行比较,如果当前节点的值比目标值小,就跳到下一个节点,否则下降到下一层继续查找。
这种方式可以减少查找的时间复杂度,期望时间为 O(log n)。
插入(Insert):
首先进行查找操作,找到插入位置。
然后根据一定的概率决定是否在上层创建新节点,最终插入节点到每一层对应的链表中。插入操作也有期望时间 O(log n)。
删除(Delete):
类似于查找,找到待删除的节点后,将其从每一层的链表中移除,时间复杂度为 O(log n)。
1.3、跳表的优点
简单易实现:跳表比起平衡二叉树(如AVL树、红黑树等)实现起来更加简单,尤其在处理动态变化的数据时。
动态平衡:跳表是基于概率的动态数据结构,不需要像平衡树那样进行显式的重平衡操作。
较好的空间利用:相比于红黑树的每个节点存储颜色、父子指针,跳表的每个节点只需要存储向后和向下的指针。
1.4、跳表的缺点
空间开销:由于需要维护多层链表,跳表的空间复杂度是 O(n),其中 n 是元素的数量。对于每个元素,可能需要在不同层级上维护多个指针。
不适用于非常小的数据集:对于非常小的数据集,跳表可能并不如线性结构(如链表)高效。
跳表在很多场景中都能提供较好的性能,特别是在需要频繁插入和删除操作的有序集合中,常用于数据库、缓存系统、分布式系统等场景。
1.5、我的实现和设计
我这边设计是按照我跳表的思想进行设计的,首先有一个垂直指针指向下一层,每一层有一个水平的单向指针指向了下一个元素,每个节点是作为一个Entry来构成,Entry里面包含了两个元素,一个是element,一个score,我这边只想做redis的zset的数据结构,所以就没考虑其他数据类型,当然是可以抽出一个通用的跳表结构,我优点忙,就难得整了,设计大概如下:
从图中可以清楚的指导,节点SkipListNode里面的有一个right指针和down指针分别对应水平指针和垂直指针,用来和水平节点和上下节点建立关系的;每一层的head节点本身不存在实际的数据,只作为一个头结点存在,我是根据自身的一些常见设计的,可能毕竟简单,但是够用足以,各位看官如果觉得哪里不合理的或者有更好的方式请在评论区讨论;
2、java的实现
本来我最早是用golang实现的,但是golang写起来感觉还是没java那么舒服,所以就先用java实现一下。
2.1、SkipListNode.java
class SkipListNode {
String element; //元素的名称
double score; //元素的分数
SkipListNode right;//水平指针
SkipListNode down;//垂直指针
public SkipListNode(String element, double score) {
this.element = element;
this.score = score;
this.right = null;
this.down = null;
}
}
2.2、SkipList.java
class SkipList {
private static final int MAX_LEVEL = 8; // 最大层数
private int currentMaxLevel = 1; // 当前最大层数
private SkipListNode head;
private int levels;
private Map<String, SkipListNode> ref;
private int size;
public SkipList() {
this.head = new SkipListNode(null, Double.NEGATIVE_INFINITY); // 头节点,起始无穷小
this.levels = 1;
this.ref = new HashMap<>();
size = 0;
}
// 随机生成层数
private int randomLevel() {
int level = 1;
int maxAllowedLevel = Math.min(currentMaxLevel + 1, MAX_LEVEL);
while (level < maxAllowedLevel && Math.random() < 0.5) {
level++;
}
return level;
}
// 插入元素
public void insert(String element, double score) {
// Step 1: 查找插入位置,并记录路径
List<SkipListNode> path = new ArrayList<>();
SkipListNode current = head;
// 查找插入位置并记录路径
while (current != null) {
while (current.right != null && current.right.score < score) {
current = current.right;
}
path.add(current); // 记录路径
current = current.down;
}
// Step 2: 生成新节点的层数
int newLevel = randomLevel();
currentMaxLevel = Math.max(currentMaxLevel, newLevel); // 更新当前最大层数
// Step 3: 创建新节点,并逐层插入
SkipListNode downNode = null; // 用来连接下层的节点
SkipListNode newNode = new SkipListNode(element, score); // 只创建一次新节点
// Step 4: 按层插入节点
for (int i = 0; i < newLevel; i++) {
// 如果路径中没有足够的层,则扩展头节点
if (i >= path.size()) {
SkipListNode newHead = new SkipListNode(null, Double.NEGATIVE_INFINITY); // 创建新头节点
newHead.down = head; // 将原头节点挂到新头节点下层
head = newHead; // 更新头节点
levels++;
path.add(0, newHead); // 将新头节点添加到路径中
}
// 当前层的前一个节点
SkipListNode prev = path.get(path.size() - 1 - i);
// 插入当前层的新节点
newNode.right = prev.right; // 将当前节点的 right 指向原来节点的 right
prev.right = newNode; // 将前一个节点的 right 指向新节点
newNode.down = downNode; // 将新节点的 down 指向下层节点
downNode = newNode; // 更新下层节点为当前新节点
// 如果新层还需要节点,继续创建新节点实例
if (i < newLevel - 1) {
newNode = new SkipListNode(element, score); // 在此处复用对象
}
}
ref.put(element, newNode);
size++;
}
// 查找单个元素
public SkipListNode find(String element) {
SkipListNode current = head;
while (current != null