Python数据结构与算法之跳表详解

目录
  • 0. 学习目标
  • 1. 跳表的基本概念
    • 1.1 跳表介绍
    • 1.2 跳表的性能
    • 1.3 跳表与普通链表的异同
  • 2. 跳表的实现
    • 2.1 跳表结点类
    • 2.2 跳表的初始化
    • 2.3 获取跳表长度
    • 2.4 读取指定位置元素
    • 2.5 查找指定元素
    • 2.6 在跳表中插入新元素
    • 2.7 删除跳表中指定元素
    • 2.8 其它一些有用的操作
  • 3. 跳表应用
    • 3.1 跳表应用示例

0. 学习目标

在诸如单链表、双线链表等普通链表中,查找、插入和删除操作由于必须从头结点遍历链表才能找到相关链表,因此时间复杂度均为O(n)。跳表是带有附加指针的链表,使用这些附加指针可以跳过一些中间结点,用以快速完成查找、插入和删除等操作。本节将介绍跳表的相关概念及其具体实现。

通过本节学习,应掌握以下内容:

  • 跳表的基本概念及其与普通链表的区别
  • 跳表及其操作的实现

1. 跳表的基本概念

跳表 (Skip List) 是一种可以快速进行查找、插入和删除等操作的有序链表(链表中的数据项按照某种顺序进行排列的链表称为有序链表)。跳表的核心思想是以更大的跨度遍历链表,以便可以跳过中间结点。

1.1 跳表介绍

首先,让我们回想一下如何在以下单链表中查找数据元素,即使链表中的数据已经排好序,我们也需要从头结点开始遍历链表。

例如要在以上单链表中查找 15,查找过程为: 0-->3-->5 -->7-->10-->15。

为了提高查找的效率,我们可以抽取链表中的一部分结点建立一个起“索引”作用的链,称为“索引层”或简称“索引”:

此时我们查找 15,首先在索引层中遍历,当我们扫描到值为 10 的结点时,由于下一结点值为 27,因此我们知道 15 应当在这两个结点之间,然后回到原链表中遍历即可找到值为 15 的结点,遍历过程如下图绿色箭头所示:

可以看到通过建立索引链,需要遍历的结点数得以减少,为了进一步减少所需遍历的结点数,可以通过对索引链添加索引,跳表是对链表叠加多级索引的数据结构。

1.2 跳表的性能

1.3 跳表与普通链表的异同

我们可以将跳表理解为排序后的链表,但与普通链表相比,有以下不同点:

  • 普通链表中的结点只有一个 next 指针,跳表中的结点有多个 next 指针;
  • 给定结点的 next 指针的数量是随机确定的。

跳表中结点的每个 next 指针称为一层,结点中的层数称为结点的大小,其决定了结点在链表中的层级;在普通的有序链表中,插入、删除和查找操作需要对链表依次遍历,因此每个操作的时间复杂度均为O(n),跳表允许在遍历时跳过一些中间节点,每个操作的时间复杂度为O(㏒n)。

2. 跳表的实现

跳表中的每个数据元素由一个结点表示,结点的层级在结点插入时随机选择,而不考虑数据结构中数据元素的数量。第i级结点有i个next 指针(在结点实现时使用大小为i的数组 next 存储i个 next 指针),因此我们不需要在结点中存储结点的层级,使用最大层数 max_level 来限制层级上限。跳表的头结点具有从 1 级到 max_level 级的 next 指针。

2.1 跳表结点类

跳表的结点示意图如下所示,每个结点都有一组用于指向其它层级结点的指针域列表 next:

class Node:
    def __init__(self, data, level=0):
        self.data = data
        self.next = [None] * level

    def __str__(self):
        return "Node({}, {})".format(self.data, len(self.next))

next 数组用于保存指向不同层级结点的指针,其中每个元素就是一个 next 指针,而 data 变量用于存储数据,重载 __str__ 方法用于便于打印结点对象,len(self.next) 可以用于表示结点中的层数。

2.2 跳表的初始化

跳表的初始化是建立一个空的带头结点的跳表,其表长 length 初始化为 0,此时链表中没有元素结点,只有一个头结点:

class SkipList:
    def __init__(self, max_level=8):
        self.max_level = max_level
        node = Node(Node, max_level)
        self.head = node
        self.length = 0

创建跳表 SkipList 对象的时间复杂度为O(1)。

2.3 获取跳表长度

求取跳表长度只需要重载 __len__ 从对象返回 length 的值,因此时间复杂度为O(1):

    def __len__(self):
        return self.length

2.4 读取指定位置元素

由于跳表中的每个结点的层级是在结点插入时随机选择的,因此读取指定位置的数据元素和单链表类似,需要顺序遍历第0层结点,操作的复杂度为O(n),如果索引不在可接受的索引范围内,将引发 IndexError 异常::

    def __getitem__(self, index):
        if index > self.length - 1 or index < 0:
            raise IndexError("UnrolledLinkedList assignment index out of range")
        else:
            count = 0
            node = self.head.next[0]
            while count < index:
                node = node.next[0]
                count += 1
            return node.data

2.5 查找指定元素

跳表的查找操作从最上层开始,如果待查元素 value 小于当前层的后继结点的 data,则平移至当前层的后继结点;否则下移进入下一层,继续比较,直至第一层。

    def locate(self, value):
        node = self.head
        # 由于索引从0开始,因此第i层级的索引为i-1
        level = self.max_level - 1
        while level >= 0:
            while node.next[level] != None and node.next[level].data < value:
                # 平移到当前层的后继结点
                node = node.next[level]
            # 下移进入下一层
            level -= 1
        if node.next[0].data == value:
            return node.next[0]
        else:
            raise ValueError("{} is not in skip list".format(value))

2.6 在跳表中插入新元素

与普通链表不同,在跳表中插入新元素不能指定位置(需要保证有序性),需要通过查找确定新数据元素的插入位置。

插入元素前,首先需要确定该元素所占据的层数 level k,这是通过算法随机生成的,这样可以减少算法实现的复杂度,同时也可以保证跳表性能。

    def random_level(self, max_level):
        num = random.randint(1, 2**max_level -1)
        lognum = math.log(num, 2)
        level = int(math.floor(lognum))
        return max_level - level

然后需要在 level 1, level 2, ..., level k 各层的链表都插入新元素,为了达到此目的,我们需要在查找插入位置时维护一个列表 update,update 中的每个元素 update[i] 指向 level i 中小于插入元素 data 的最右边结点,即插入位置的左侧,如下图所示,插入元素 data = 9,随机层数 k=3:

    def update_list(self, data):
        update = [None] * self.max_level
        node = self.head
        level = self.max_level - 1
        while level >= 0:
            while node.next[level] != None and node.next[level].data < data:
                node = node.next[level]
            update[level] = node
            level -= 1
        return update

可以看到维护 update 列表的过程与查找元素类似,根据 update 列表可以判断跳表中是否已存在元素 data,如果不存在将其插入跳表中:

    def insert_node(self, data):
        # 产生随机 level,并构造结点
        level = self.random_level(self.max_level)
        node = Node(data, level)
        # 获取 update 列表
        update = self.update_list(data)
        # 插入结点
        if len(update) == 0 or update[0].next[0] == None or update[0].next[0].data != data:
            self.length += 1
            for i in range(level):
                node.next[i] = update[i].next[i]
                update[i].next[i] = node

2.7 删除跳表中指定元素

删除指定元素与插入操作类似:

    def delete_node(self, data):
        # 获取 update 列表
        update = self.update_list(data)
        if len(update) > 0:
            node = update[0].next[0]
            # 删除指定元素结点
            if node != None and node.data == data:
                self.length -= 1
                for i in range(len(node.next)):
                    update[i].next[i] = node.next[i]
                del node

2.8 其它一些有用的操作

1.跳表指定层元素输出

将跳表指定层转换为字符串以便进行打印,编写 print_level 方法打印指定层中数据元素:

def get_level(self, level):
        """辅助函数,用于根据给定层构造结果字符串"""
        node = self.head.next[level]
        result = ''
        while node:
            result = result + str(node.data) + '-->'
            node = node.next[level]
        result += 'None'
        return result

    def print_level(self, level):
        print('level {}'.format(level))
        result = self.get_level(level)
        print(result)

2.跳表各层元素输出

可以借助上述辅助函数 get_level,使用 str 函数调用对象上的 __str__ 方法可以创建适合打印的字符串表示:

    def __str__(self):
        result = ''
        for i in list(range(self.max_level))[self.max_level-1:0:-1]:
            result = result + self.get_level(i) + '\n'
        result += self.get_level(0)
        return result

3. 跳表应用

接下来,我们将测试上述实现的链表,以验证操作的有效性。

3.1 跳表应用示例

首先初始化一个跳表 sl,并在其中插入若干元素:

sl = SkipList(4)
for i in range(0, 30, 3):
    sl.insert_node(i)

我们可以直接打印跳表中的数据元素、链表长度等信息:

print('跳表 sl 各层元素为:')
print(sl)
print('跳表 sl 长度为:', len(sl))
print('跳表 sl 第 2 个元素为:', sl[2])

以上代码输出如下:

跳表 sl 各层元素为:
9-->None
0-->9-->18-->None
0-->9-->18-->27-->None
0-->3-->6-->9-->12-->15-->18-->21-->24-->27-->None
跳表 sl 长度为: 10
跳表 sl 第 2 个元素为: 6

接下来,我们将演示在添加/删除元素、以及如何查找指定元素等:

# 插入元素
sl.insert_node(14)
print('在跳表 sl 中插入数据 14 后:')
print(sl)
# 删除元素
sl.delete_node(14)
print('删除跳表 sl 中数据 14 后:')
print(sl)
# 查找数据元素 15
print('查找跳表 sl 中数据元素 15:', sl.locate(15))

以上代码输出如下:

在跳表 sl 中插入数据 14 后:
9-->None
0-->9-->18-->None
0-->9-->14-->18-->27-->None
0-->3-->6-->9-->12-->14-->15-->18-->21-->24-->27-->None
删除跳表 sl 中数据 14 后:
9-->None
0-->9-->18-->None
0-->9-->18-->27-->None
0-->3-->6-->9-->12-->15-->18-->21-->24-->27-->None
查找跳表 sl 中数据元素 15: Node(15, 1)

以上就是Python数据结构与算法之跳表详解的详细内容,更多关于Python跳表的资料请关注我们其它相关文章!

(0)

相关推荐

  • c++如何实现跳表

    引言 二分查找底层依赖的是数组随机访问的特性,所以只能用数组来实现.如果数据存储在链表中,就真的没法用二分查找算法了吗?实际上,只需要对链表稍加改造,就可以支持类似"二分"的查找算法.改造之后的数据结构叫作跳表. 定义 跳表是一个随机化的数据结构.它允许快速查询一个有序连续元素的数据链表.跳跃列表的平均查找和插入时间复杂度都是O(log n),优于普通队列的O(n).性能上和红黑树,AVL树不相上下,但跳表的原理非常简单,目前Redis和LevelDB中都有用到. 跳表是一种可以替代平

  • Python数据结构之循环链表详解

    目录 0. 学习目标 1. 循环链表简介 2. 循环单链表实现 2.1 循环单链表的基本操作 2.2 简单的实现方法 2.3 循环单链表应用示例 2.4 利用循环单链表基本操作实现复杂操作 3. 循环双链表实现 3.1 循环双链表的基本操作 3.2 循环双链表应用示例 0. 学习目标 循环链表 (Circular Linked List) 是链式存储结构的另一种形式,它将链表中最后一个结点的指针指向链表的头结点,使整个链表头尾相接形成一个环形,使链表的操作更加方便灵活.我们已经介绍了单链表和双向

  • Python数据结构之双向链表详解

    目录 0. 学习目标 1. 双向链表简介 1.1 双向链表介绍 1.2 双向链表结点类 1.3 双向链表优缺点 2. 双向链表实现 2.1 双向链表的初始化 2.2 获取双向链表长度 2.3 读取指定位置元素 2.4 查找指定元素 2.5 在指定位置插入新元素 2.6 删除指定位置元素 2.7 其它一些有用的操作 3. 双向链表应用 3.1 双向链表应用示例 3.2 利用双向链表基本操作实现复杂操作 0. 学习目标 单链表只有一个指向直接后继的指针来表示结点间的逻辑关系,因此可以方便的从任一结点

  • Java数据结构之实现跳表

    1.跳表的定义 跳跃表是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好. SkipList(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的.SkipList让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过"空间来换取时间"的一个算法,在每个节点中增加了向前的指针,在插入.删除.查找时可以忽略一些不可能涉及到的结点,从而提高了效率. 在Java的API中已经有了实

  • python实现跳表SkipList的示例代码

    跳表 跳表,又叫做跳跃表.跳跃列表,在有序链表的基础上增加了"跳跃"的功能,由William Pugh于1990年发布,设计的初衷是为了取代平衡树(比如红黑树). Redis.LevelDB 都是著名的 Key-Value 数据库,而Redis中 的 SortedSet.LevelDB 中的 MemTable 都用到了跳表. 对比平衡树,跳表的实现和维护会更加简单,跳表的搜索.删除.添加的平均时间复杂度是 O(logn). 跳表的结构如图所示: 可以发现,对于一个节点Node,其含有多

  • Python数据结构与算法之跳表详解

    目录 0. 学习目标 1. 跳表的基本概念 1.1 跳表介绍 1.2 跳表的性能 1.3 跳表与普通链表的异同 2. 跳表的实现 2.1 跳表结点类 2.2 跳表的初始化 2.3 获取跳表长度 2.4 读取指定位置元素 2.5 查找指定元素 2.6 在跳表中插入新元素 2.7 删除跳表中指定元素 2.8 其它一些有用的操作 3. 跳表应用 3.1 跳表应用示例 0. 学习目标 在诸如单链表.双线链表等普通链表中,查找.插入和删除操作由于必须从头结点遍历链表才能找到相关链表,因此时间复杂度均为O(

  • Python数据结构与算法中的栈详解(3)

    目录 前序.中序和后序表达式是什么? 我们为什么要学习前/后序表达式? 从中序向前序和后序转换 用Python实现从中序表达式到后序表达式的转换​ 计算后序表达式 总结 前序.中序和后序表达式是什么? 对于像B∗C 这样的算术表达式,可以根据其形式来正确地运算.在B∗C 的例子中,由于乘号出现在两个变量之间,因此我们知道应该用变量 B 乘以变量 C .​ 因为运算符出现在两个操作数的中间 ,所以这种表达式被称作中序表达式 .​ 来看另一个中序表达式的例子:A+B∗C.虽然运算符 “ + ” 和

  • Python数据结构与算法中的栈详解

    目录 0.学习目标 1.栈的基本概念 1.1栈的基本概念 1.2栈抽象数据类型 1.3栈的应用场景 2.栈的实现 2.1顺序栈的实现 2.1.1栈的初始化 2.1.2求栈长 2.1.3判栈空 2.1.4判栈满 2.1.5入栈 2.1.6出栈 2.1.7求栈顶元素 2.2链栈的实现 2.2.1栈结点 2.2.2栈的初始化 2.2.3求栈长 2.2.4判栈空 2.2.5入栈 2.2.6出栈 2.3栈的不同实现对比 3.栈应用 3.1顺序栈的应用 3.2链栈的应用 3.3利用栈基本操作实现复杂算法 总

  • Python数据结构与算法中的栈详解(2)

    目录 匹配括号 匹配符号 总结 匹配括号 接下来,我们使用栈解决实际的计算机科学问题.​ 比如我们都写过这样所示的算术表达式, ( 5 + 6 ) ∗ ( 7 + 8 ) / ( 4 + 3 ) (5 + 6) * (7 + 8) / (4 + 3) (5+6)∗(7+8)/(4+3),其中的括号用来改变计算顺序,或提升运算优先级.​ 匹配括号是指每一个左括号都有与之对应的一个右括号,并且括号对有正确的嵌套关系. 正确的嵌套关系: ( ( ) ( ) ( ) ( ) ) (()()()()) (

  • Python数据结构与算法中的栈详解(1)

    目录 什么是栈 构建一个栈 总结 什么是栈 栈有时也被称作“下推栈”.它是有序集合,添加操作和移除操作总发生在同一端,即栈的 “顶端”,栈的另一端则被称为 “底端”.所以最新添加的元素将被最先移除,而且栈中的元素离底端越近,代表其在栈中的时间越长. 这种排序原则被称作LIFO(last-in first-out),即后进先出.它提供了一种基于在集合中的时间来排序的方式. 最近添加的元素靠近顶端,旧元素则靠近底端. 栈的例子在日常生活中比比皆是.几乎所有咖啡馆都有一个由托盘或盘子构成的栈,你可以从

  • Python数据结构与算法中的队列详解(2)

    目录 传土豆 总结 传土豆 队列的一个典型方法是模拟需要以 FIFO 方式管理数据的真实场景.考虑这样一个游戏:传土豆.在这个游戏中,成员们围成一圈,并依次尽可能快地传递一个土豆.在某个时刻,大家停止传递,此时手里有土豆的成员就得退出游戏. 重复上述过程,直到只剩下一个成员. 我们将针对传土豆游戏实现通用的模拟程序.该程序接受一个名字列表和一个用于计数的常量 num ,并且返回最后剩下的那个人的名字. 我们使用队列来模拟一个环.即假设握着土豆的人位于队列的头部.在模拟传土豆的过程中,程序将这个人

  • Python数据结构与算法中的队列详解(1)

    目录 什么是队列? 构建一个队列 总结 什么是队列? 队列,与栈类似,是有序集合.添加操作发生在 “尾部”,移除操作只发生在 “头部”.新元素只从尾部进入队列,然后一直向前移动到头部,直到成为下一个被移除的元素.​ 最新添加的元素必须在队列的尾部等待,在队列中时间最长的元素则排在最前面.这种排序原则被称作FIFO(first-in first-out),即先进先出,也称先到先得.在日常生活中,我们经常排队,这便是最简单的队列例子.进电影院要排队,在超市结账要排队,买咖啡也要排队.好的队列只允许一

  • Python数据结构之图的存储结构详解

    一.图的定义 图是一种比树更复杂的一种数据结构,在图结构中,结点之间的关系是任意的,任意两个元素之间都可能相关,因此,它的应用极广.图中的数据元素通常被称为顶点 ( V e r t e x ) (Vertex) (Vertex), V V V是顶点的有穷非空集合, V R VR VR是两个顶点之间的关系的集合(可以为空),可以表示为图 G = { V , { V R } } G=\{V,\{VR\}\} G={V,{VR}}. 二.相关术语 2.1 无向图 给定图 G = { V , { E }

  • Python数据结构之优先级队列queue用法详解

    一.基本用法 Queue类实现了一个基本的先进先出容器.使用put()将元素增加到这个序列的一端,使用get()从另一端删除.具体代码如下所示: import queue q = queue.Queue() for i in range(1, 10): q.put(i) while not q.empty(): print(q.get(), end=" ") 运行之后,效果如下: 这里我们依次添加1到10到队列中,因为先进先出,所以出来的顺序也与添加的顺序相同. 二.LIFO队列 既然

  • Python实现的数据结构与算法之基本搜索详解

    本文实例讲述了Python实现的数据结构与算法之基本搜索.分享给大家供大家参考.具体分析如下: 一.顺序搜索 顺序搜索 是最简单直观的搜索方法:从列表开头到末尾,逐个比较待搜索项与列表中的项,直到找到目标项(搜索成功)或者 超出搜索范围 (搜索失败). 根据列表中的项是否按顺序排列,可以将列表分为 无序列表 和 有序列表.对于 无序列表,超出搜索范围 是指越过列表的末尾:对于 有序列表,超过搜索范围 是指进入列表中大于目标项的区域(发生在目标项小于列表末尾项时)或者指越过列表的末尾(发生在目标项

随机推荐