一、啥是跳表

咱先说说跳表是个啥。跳表其实就是一种数据结构,它能让我们在里面快速地查找、插入和删除数据。打个比方,你有一本超级厚的字典,你要找一个字,要是一页一页翻,那得翻到猴年马月。但如果这本字典有目录,你先从目录里找到大概位置,再去对应页码找,就快多啦。跳表就有点像有目录的字典,它在普通链表的基础上,多了几层索引,能让我们更快地找到想要的数据。

跳表的基本结构

跳表是由多层链表组成的。最底层是一个普通的链表,存储着所有的数据。上面的每一层都是下面一层的索引。比如说,最底层链表有 10 个节点,那上一层可能就只有 5 个节点,再上一层可能就只有 2 个节点,以此类推。这样,当我们要查找一个数据时,就可以先从最上层的索引开始找,快速缩小查找范围,然后再到下一层继续找,直到找到目标数据或者确定数据不存在。

示例代码(Python 技术栈)

import random

# 定义跳表节点类
class SkipListNode:
    def __init__(self, value, level):
        # 节点的值
        self.value = value
        # 节点的每一层的下一个节点
        self.forward = [None] * (level + 1)

# 定义跳表类
class SkipList:
    def __init__(self, max_level=16):
        # 最大层数
        self.max_level = max_level
        # 当前跳表的最高层数
        self.level = 0
        # 头节点,初始值为负无穷
        self.head = SkipListNode(float('-inf'), max_level)

    def random_level(self):
        # 随机生成节点的层数
        level = 0
        while random.random() < 0.5 and level < self.max_level:
            level += 1
        return level

    def insert(self, value):
        # 用于记录每一层需要更新的节点
        update = [None] * (self.max_level + 1)
        current = self.head
        # 从最高层开始查找插入位置
        for i in range(self.level, -1, -1):
            while current.forward[i] and current.forward[i].value < value:
                current = current.forward[i]
            update[i] = current
        current = current.forward[0]
        # 如果值已存在,不插入
        if current and current.value == value:
            return
        # 随机生成新节点的层数
        new_level = self.random_level()
        # 如果新节点的层数大于当前跳表的最高层数
        if new_level > self.level:
            for i in range(self.level + 1, new_level + 1):
                update[i] = self.head
            self.level = new_level
        # 创建新节点
        new_node = SkipListNode(value, new_level)
        # 更新每一层的指针
        for i in range(new_level + 1):
            new_node.forward[i] = update[i].forward[i]
            update[i].forward[i] = new_node

    def search(self, value):
        current = self.head
        # 从最高层开始查找
        for i in range(self.level, -1, -1):
            while current.forward[i] and current.forward[i].value < value:
                current = current.forward[i]
        current = current.forward[0]
        # 如果找到目标值,返回 True,否则返回 False
        if current and current.value == value:
            return True
        return False

    def delete(self, value):
        update = [None] * (self.max_level + 1)
        current = self.head
        # 从最高层开始查找要删除的节点
        for i in range(self.level, -1, -1):
            while current.forward[i] and current.forward[i].value < value:
                current = current.forward[i]
            update[i] = current
        current = current.forward[0]
        # 如果找到要删除的节点
        if current and current.value == value:
            # 更新每一层的指针
            for i in range(self.level + 1):
                if update[i].forward[i] != current:
                    break
                update[i].forward[i] = current.forward[i]
            # 如果删除节点后,最高层没有节点了,降低最高层数
            while self.level > 0 and not self.head.forward[self.level]:
                self.level -= 1


# 使用示例
skip_list = SkipList()
skip_list.insert(3)
skip_list.insert(1)
skip_list.insert(2)
print(skip_list.search(2))  # 输出: True
skip_list.delete(2)
print(skip_list.search(2))  # 输出: False

二、Redis 为啥选跳表实现有序集合

Redis 是一个非常流行的内存数据库,它的有序集合是一种很重要的数据类型。有序集合里的每个元素都有一个分数,元素会根据分数从小到大排序。那 Redis 为啥选择跳表来实现有序集合呢?

快速查找

跳表的查找效率很高,平均时间复杂度是 O(log n)。在 Redis 的有序集合里,我们经常需要根据分数查找元素,或者查找某个范围内的元素。用跳表的话,就能快速地定位到目标元素,提高查询效率。比如说,我们要查找分数在 10 到 20 之间的所有元素,跳表可以快速地找到分数为 10 的元素,然后依次往后找,直到找到分数大于 20 的元素为止。

插入和删除操作高效

跳表的插入和删除操作也很高效,平均时间复杂度也是 O(log n)。在 Redis 的有序集合里,元素是动态变化的,经常会有新元素插入,或者旧元素删除。跳表可以在插入和删除元素时,快速地调整索引结构,保证数据的有序性。比如说,我们要插入一个新元素,跳表会根据元素的分数,快速找到插入位置,然后更新索引。

实现简单

跳表的实现相对简单,代码量也比较少。Redis 是一个开源的项目,代码的可维护性很重要。用跳表来实现有序集合,可以让代码更简洁,更容易维护。

支持范围查询

Redis 的有序集合经常需要进行范围查询,比如查找分数在某个区间内的元素。跳表可以很方便地支持范围查询,只需要从索引中找到范围的起始位置,然后依次遍历链表,直到找到范围的结束位置。

三、跳表的应用场景

数据库索引

在数据库中,索引是提高查询效率的重要手段。跳表可以作为数据库的索引结构,快速定位到数据记录。比如说,在 MySQL 数据库中,B+ 树是常用的索引结构,但在某些场景下,跳表也可以作为一种替代方案。

缓存系统

在缓存系统中,跳表可以用来实现缓存的有序存储。比如说,Redis 就是用跳表来实现有序集合的,这样可以快速地查找和更新缓存数据。

分布式系统

在分布式系统中,跳表可以用来实现分布式索引。比如说,在分布式文件系统中,跳表可以用来快速定位文件的位置。

四、跳表的技术优缺点

优点

  • 查找、插入和删除效率高:跳表的平均时间复杂度是 O(log n),在大多数情况下,能满足我们对数据操作的性能要求。
  • 实现简单:跳表的代码相对简单,容易理解和维护。
  • 支持范围查询:跳表可以很方便地支持范围查询,这在很多应用场景中非常有用。

缺点

  • 空间开销大:跳表需要额外的空间来存储索引,随着数据量的增加,空间开销会越来越大。
  • 随机层数的不确定性:跳表的性能依赖于随机生成的层数,如果层数分布不合理,可能会影响跳表的性能。

五、使用跳表的注意事项

空间管理

由于跳表需要额外的空间来存储索引,所以在使用跳表时,要注意空间的管理。如果数据量很大,可能需要考虑优化跳表的结构,减少空间开销。

随机层数的调整

跳表的性能依赖于随机生成的层数,所以要合理调整随机层数的生成策略。可以根据实际情况,调整随机层数的概率,保证跳表的性能稳定。

并发操作

在多线程环境下使用跳表时,要注意并发操作的问题。跳表的插入、删除和查找操作可能会相互影响,需要使用锁或者其他并发控制机制来保证数据的一致性。

六、文章总结

跳表是一种非常实用的数据结构,它具有查找、插入和删除效率高,实现简单,支持范围查询等优点。Redis 选择跳表来实现有序集合,正是看中了跳表的这些优点。在实际应用中,跳表可以用于数据库索引、缓存系统、分布式系统等场景。但跳表也有空间开销大、随机层数不确定性等缺点,在使用时需要注意空间管理、随机层数的调整和并发操作等问题。