MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis跳跃表在有序集合中的应用

2022-11-092.5k 阅读

Redis 有序集合概述

Redis 有序集合(Sorted Set)是 Redis 提供的一种非常特殊的数据结构,它类似于集合(Set),成员都是唯一的,但每个成员都会关联一个分数(Score)。这个分数在有序集合中起着至关重要的作用,它决定了成员的排列顺序。有序集合根据分数的大小对成员进行排序,分数越小的成员在集合中的位置越靠前。

有序集合在很多场景下都有广泛应用,比如排行榜系统。例如,在一个游戏中,我们可以根据玩家的积分来创建一个有序集合,积分就是分数,玩家的名字就是成员。这样我们就可以轻松地获取积分排名靠前的玩家,或者根据某个玩家的积分来查询其排名等操作。

Redis 跳跃表简介

跳跃表(Skip List)是一种随机化的数据结构,由 William Pugh 于 1990 年发明。它的设计目的是为了在保持简单的数据结构的同时,能够实现类似于平衡二叉树的对数级别的查找效率。

跳跃表的基本结构

跳跃表由多层链表组成,最底层的链表包含了所有的元素,并且按顺序排列。每一层链表都是下一层链表的“快速通道”,链表中的节点包含了指向同一层下一个节点以及下一层对应节点的指针。

以一个简单的跳跃表为例,假设有元素 1, 3, 5, 7, 9。最底层链表按顺序依次连接这些元素。在更高层的链表中,可能每隔几个元素就会出现一个“跳跃”节点,这些节点跳过中间的一些元素,直接连接到较远的节点,从而加快查找速度。

跳跃表的节点结构

在 Redis 中,跳跃表的节点结构定义如下(简化的 C 语言代码示例):

typedef struct zskiplistNode {
    sds ele;        // 成员对象,例如有序集合中的成员(如玩家名字)
    double score;   // 分数,用于排序
    struct zskiplistNode *backward;  // 后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前进指针
        unsigned int span;  // 跨度,表示当前节点到下一个节点之间的元素个数
    } level[];
} zskiplistNode;

这里 ele 存储了有序集合中的成员,score 是用于排序的分数。backward 指针用于从后向前遍历跳跃表,而 level 数组则存储了不同层的前进指针和跨度。

跳跃表的层数

跳跃表的层数并不是固定的,它是随机生成的。在 Redis 中,跳跃表的最大层数为 64 层,每次创建新节点时,会随机决定该节点的层数。这个随机过程是通过一个随机函数实现的,该函数以一定的概率(通常为 1/2)决定是否增加一层。

Redis 跳跃表在有序集合中的应用原理

在 Redis 的有序集合中,跳跃表主要用于实现高效的插入、删除和查找操作,同时能够保证元素的有序性。

查找操作

当在跳跃表中查找一个元素时,首先从最高层的链表开始。从链表头节点出发,比较当前节点的分数与要查找的分数。如果当前节点的分数大于要查找的分数,或者当前节点的下一个节点不存在,就下降到下一层链表继续查找。否则,就沿着当前层链表继续前进。

例如,在一个跳跃表中查找分数为 5 的元素。假设最高层链表中有节点 1, 7。从最高层开始,1 的分数小于 5,继续前进到 7,7 的分数大于 5,此时下降到下一层链表。在下一层链表中继续重复上述过程,直到找到分数为 5 的元素或者确定不存在为止。

插入操作

插入操作相对复杂一些。在插入一个新元素时,首先要确定该元素在跳跃表中的位置,这通过查找操作来完成。找到位置后,需要为新元素随机生成一个层数,并在相应的层插入新节点。

在插入节点的过程中,需要更新相关节点的指针。例如,新节点的前一个节点的 forward 指针要指向新节点,新节点的 forward 指针要指向下一个节点。同时,对于多层链表结构,每一层都要进行相应的指针更新。

此外,插入操作还需要更新跨度信息。跨度用于记录两个节点之间的元素个数,这在计算排名等操作时非常有用。

删除操作

删除操作首先要找到要删除的节点。找到节点后,需要更新该节点前后节点的指针,将其从链表中移除。对于多层链表,每一层都要进行相应的操作。

在删除节点后,还需要检查是否有一些层因为删除节点而变得“空洞”。如果某一层链表只剩下头节点和尾节点,那么这一层链表可以被删除,以优化空间占用。

代码示例(以 Python 模拟 Redis 有序集合跳跃表部分操作)

下面通过 Python 代码来模拟 Redis 有序集合中跳跃表的部分操作,包括插入、查找和打印跳跃表结构。

import random


class SkipListNode:
    def __init__(self, ele, score, level):
        self.ele = ele
        self.score = score
        self.forward = [None] * (level + 1)
        self.backward = None


class SkipList:
    def __init__(self, max_level=16, p=0.5):
        self.max_level = max_level
        self.p = p
        self.header = SkipListNode(None, float('-inf'), max_level)
        self.level = 0
        self.length = 0

    def random_level(self):
        level = 0
        while random.random() < self.p and level < self.max_level:
            level += 1
        return level

    def insert(self, ele, score):
        update = [self.header] * (self.max_level + 1)
        rank = [0] * (self.max_level + 1)
        x = self.header
        for i in range(self.level, -1, -1):
            while x.forward[i] and (x.forward[i].score < score or
                                    (x.forward[i].score == score and x.forward[i].ele < ele)):
                rank[i] += x.forward[i].span
                x = x.forward[i]
            update[i] = x

        x = x.forward[0]
        if x and x.score == score and x.ele == ele:
            print(f"Element {ele} with score {score} already exists")
            return False

        new_level = self.random_level()
        if new_level > self.level:
            for i in range(self.level + 1, new_level + 1):
                rank[i] = 0
                update[i] = self.header
                update[i].span = self.length
            self.level = new_level

        x = SkipListNode(ele, score, new_level)
        for i in range(new_level + 1):
            x.forward[i] = update[i].forward[i]
            update[i].forward[i] = x

        for i in range(0, new_level + 1):
            x.span = rank[0] - rank[i] + 1
            update[i].span = rank[0] - rank[i] + 1

        x.backward = update[0]
        if x.forward[0]:
            x.forward[0].backward = x

        self.length += 1
        return True

    def search(self, score):
        x = self.header
        for i in range(self.level, -1, -1):
            while x.forward[i] and x.forward[i].score < score:
                x = x.forward[i]

        x = x.forward[0]
        if x and x.score == score:
            return x.ele
        return None

    def print_skip_list(self):
        print("Skip List Structure:")
        for i in range(self.level + 1):
            node = self.header.forward[i]
            print(f"Level {i}: ", end='')
            while node:
                print(f"({node.ele}, {node.score}) ", end='')
                node = node.forward[i]
            print()


# 测试代码
if __name__ == "__main__":
    skip_list = SkipList()
    elements = [('A', 1), ('B', 2), ('C', 3)]
    for ele, score in elements:
        skip_list.insert(ele, score)

    skip_list.print_skip_list()
    print(f"Search result for score 2: {skip_list.search(2)}")

在这段代码中,SkipList 类实现了跳跃表的基本结构和操作。insert 方法用于插入新元素,search 方法用于查找元素,print_skip_list 方法用于打印跳跃表的结构。

跳跃表与其他数据结构的比较

在实现有序集合时,除了跳跃表,还可以考虑使用平衡二叉树等数据结构。

与平衡二叉树比较

平衡二叉树可以保证插入、删除和查找操作的时间复杂度为 O(log n),其中 n 是节点数。然而,平衡二叉树的实现相对复杂,需要维护树的平衡,例如通过旋转操作来调整树的结构。

相比之下,跳跃表的实现相对简单,虽然平均情况下的时间复杂度也是 O(log n),但它的性能在实际应用中表现良好,并且不需要像平衡二叉树那样进行复杂的平衡维护操作。

与普通链表比较

普通链表插入和删除操作的时间复杂度为 O(1),但查找操作的时间复杂度为 O(n)。而跳跃表通过多层链表结构,在保证插入和删除操作时间复杂度接近 O(1) 的同时,将查找操作的时间复杂度降低到了 O(log n),大大提高了查找效率。

Redis 跳跃表在有序集合中的优化与改进

在 Redis 实际应用中,对跳跃表进行了一些优化以提高性能和节省空间。

跨度优化

Redis 跳跃表中的跨度信息在很多操作中都非常有用。例如,在计算某个元素的排名时,可以通过跨度快速计算得出。在插入和删除操作中,对跨度的更新进行了优化,以减少不必要的计算。

空间优化

虽然跳跃表通过多层链表结构提高了查找效率,但同时也增加了空间占用。Redis 通过一些策略来优化空间使用。例如,在删除节点后,如果某一层链表变得过于稀疏,会将这一层链表删除,从而减少空间浪费。

总结跳跃表在 Redis 有序集合中的优势

综上所述,Redis 跳跃表在有序集合中具有以下显著优势:

  1. 高效的操作性能:插入、删除和查找操作的平均时间复杂度都为 O(log n),能够满足大多数应用场景对有序集合操作的性能需求。
  2. 简单的实现:相比于平衡二叉树等复杂的数据结构,跳跃表的实现相对简单,这使得 Redis 的开发者能够更轻松地维护和扩展相关代码。
  3. 灵活性:跳跃表的层数随机生成,这种特性使得跳跃表能够在不同的数据规模下都保持较好的性能。同时,它对数据的插入和删除操作相对友好,不需要像平衡二叉树那样进行复杂的结构调整。

因此,跳跃表成为 Redis 有序集合实现的重要数据结构,为各种应用场景提供了高效、可靠的有序集合操作支持。