geekdoc-python-zh/docs/realpython/linked-lists-python.md

32 KiB

Python 中的链表:简介

原文:https://realpython.com/linked-lists-python/

*立即观看**本教程有真实 Python 团队创建的相关视频课程。和书面教程一起看,加深理解: 用 Python 处理链表

链表就像是链表的一个鲜为人知的表亲。它们既不流行也不酷,你甚至可能不记得算法课上的它们。但在合适的背景下,他们真的可以大放异彩。

在这篇文章中,你将了解到:

  • 什么是链表,什么时候应该使用它们
  • 如何使用collections.deque来满足你所有的链表需求
  • 如何实现你自己的链表
  • 其他类型的链表是什么,它们可以用来做什么

如果你想为一次工作面试提高你的编码技能,或者如果你想学习更多关于 Python 数据结构的知识,除了通常的字典列表,那么你来对地方了!

您可以通过下载下面链接中的源代码来学习本教程中的示例:

获取源代码: 点击此处获取源代码,您将使用在本教程中学习链表。

理解链表

链表是对象的有序集合。那么是什么让它们不同于普通的列表呢?链表与列表的不同之处在于它们在内存中存储元素的方式。列表使用连续的内存块来存储对其数据的引用,而链表将引用存储为其自身元素的一部分。

Remove ads

主要概念

在深入了解什么是链表以及如何使用它们之前,您应该首先了解它们是如何构造的。链表的每个元素称为一个节点,每个节点有两个不同的字段:

  1. 数据包含要存储在节点中的值。
  2. Next 包含对列表中下一个节点的引用。

典型的节点如下所示:

Example Node of a Linked List

Node

链表是节点的集合。第一个节点叫做 head ,它被用作遍历列表的起点。最后一个节点必须有其指向 Nonenext引用来确定列表的结尾。它看起来是这样的:

Example Structure of a Linked List

Linked List

现在您已经知道了链表是如何构造的,您已经准备好查看它的一些实际用例了。

实际应用

链表在现实世界中有多种用途。它们可以用来实现(剧透警报!)队列或以及图形。它们对于更复杂的任务也很有用,比如操作系统应用程序的生命周期管理。

队列或堆栈

队列和堆栈的区别仅在于检索元素的方式。对于一个队列,您使用一种先进/先出 (FIFO)方法。这意味着插入列表的第一个元素是第一个被检索的元素:

Example Structure of a Queue

Queue

在上图中,你可以看到队列的元素。当你把新元素添加到队列中时,它们会被放到队列的最后。当您检索元素时,它们将从队列的前面取出。

对于堆栈,使用后进/先出 (LIFO)方法,这意味着插入到列表中的最后一个元素首先被检索:

Example Structure of a Stack

Stack

在上图中,你可以看到堆栈中插入的第一个元素(索引0)在底部,插入的最后一个元素在顶部。因为堆栈使用 LIFO 方法,最后插入的元素(在顶部)将首先被取出。

由于从队列和堆栈的边缘插入和检索元素的方式,链表是实现这些数据结构的最方便的方式之一。在本文的后面,您将看到这些实现的例子。

图表

图形可用于显示对象之间的关系或表示不同类型的网络。例如,一个图形的可视化表示(比如有向无环图(DAG))可能如下所示:

Example Directed Graph

Directed Acyclic Graph

有不同的方法来实现上面这样的图,但是最常用的方法之一是使用一个邻接表。本质上,邻接表是一个链表的列表,其中图的每个顶点都存储在连接的顶点的集合旁边:

顶点 顶点链表
one 2 → 3 →无
Two 4 →无
three 没有人
four 5 → 6 →无
five 6 →无
six 没有人

在上表中,左栏列出了图形的每个顶点。右列包含一系列链表,这些链表存储了与左列中相应顶点相连接的其他顶点。这个邻接表也可以用代码表示,使用一个dict:

>>> graph = {
...     1: [2, 3, None],
...     2: [4, None],
...     3: [None],
...     4: [5, 6, None],
...     5: [6, None],
...     6: [None]
... }

这个字典的键是源顶点,每个键的值是一个列表。这个列表通常被实现为一个链表。

**注意:**在上面的例子中,你可以避免存储None值,但是为了清晰和与后面的例子保持一致,我们在这里保留了它们。

就速度和内存而言,与邻接矩阵相比,使用邻接表实现图是非常有效的。这就是链表对于图形实现如此有用的原因。

Remove ads

性能比较:列表与链表

在大多数编程语言中,链表和数组在内存中的存储方式有明显的不同。然而,在 Python 中,列表是动态数组。这意味着链表和链表的内存使用非常相似。

延伸阅读: Python 的动态数组的实现非常有趣,绝对值得一读。一定要看一看,并利用这些知识在下次公司聚会上脱颖而出!

由于链表和链表在内存使用上的差异是如此的微不足道,所以当涉及到时间复杂度时,如果你关注它们的性能差异会更好。

元素的插入和删除

在 Python 中,可以使用.insert().append() 将元素插入到列表中。要从列表中删除元素,可以使用它们的对应物:.remove().pop()

这些方法之间的主要区别在于,您使用.insert().remove()在列表中的特定位置插入或移除元素,但是您使用.append().pop()仅在列表的末尾插入或移除元素。

现在,关于 Python 列表,您需要知道的是,在列表末尾插入或删除不是的元素需要在后台进行一些元素移动,这使得操作在时间上更加复杂。你可以阅读上面提到的关于列表如何在 Python 中实现的文章,以更好地理解.insert().remove().append().pop()的实现如何影响它们的性能。

考虑到这一点,即使使用.append().insert()在列表末尾插入元素会有固定的时间, O (1),当您尝试在更靠近列表或列表开头的位置插入元素时,平均时间复杂度会随着列表的大小而增加: O ( n )。

另一方面,当在列表的开头或结尾插入和删除元素时,链表要简单得多,因为它们的时间复杂度总是恒定的: O (1)。

由于这个原因,当实现队列(FIFO)时,链表比普通的链表具有更好的性能,在队列中,元素在链表的开头不断地插入和删除。但是在实现栈(LIFO)时,它们的表现类似于链表,在栈中,元素在链表的末尾被插入和删除。

元素检索

当涉及到元素查找时,列表的性能比链表好得多。当你知道你想要访问哪个元素时,列表可以在 O (1)时间内执行这个操作。尝试对一个链表做同样的事情需要花费 O ( n )因为你需要遍历整个链表来找到元素。

然而,在搜索特定元素时,链表和链表的表现非常相似,时间复杂度为 O ( n )。在这两种情况下,您都需要遍历整个列表来找到您正在寻找的元素。

collections.deque简介

在 Python 中,collections模块中有一个可以用于链表的特定对象,名为 德克 (读作“deck”),代表双端队列

collections.deque使用了一个链表的实现,在这个链表中,你可以在不变的 O (1)性能下访问、插入或移除列表开头或结尾的元素。

如何使用collections.deque

默认情况下,有相当多的方法带有一个deque对象。然而,在本文中,您将只触及其中的一些,主要是添加或删除元素。

首先,您需要创建一个链表。您可以使用下面这段代码通过deque来做到这一点:

>>> from collections import deque
>>> deque()
deque([])

上面的代码将创建一个空的链表。如果您想在创建时填充它,那么您可以给它一个 iterable 作为输入:

>>> deque(['a','b','c'])
deque(['a', 'b', 'c'])

>>> deque('abc')
deque(['a', 'b', 'c'])

>>> deque([{'data': 'a'}, {'data': 'b'}])
deque([{'data': 'a'}, {'data': 'b'}])

当初始化一个deque对象时,您可以将任何 iterable 作为输入传递,比如一个字符串(也是 iterable)或者一个对象列表。

现在您已经知道如何创建一个deque对象,您可以通过添加或删除元素来与它交互。你可以创建一个abcde链表并添加一个新元素f,如下所示:

>>> llist = deque("abcde")
>>> llist
deque(['a', 'b', 'c', 'd', 'e'])

>>> llist.append("f")
>>> llist
deque(['a', 'b', 'c', 'd', 'e', 'f'])

>>> llist.pop()
'f'

>>> llist
deque(['a', 'b', 'c', 'd', 'e'])

append()pop()都在链表的右侧添加或移除元素。但是,您也可以使用deque快速添加或删除列表左侧的元素,或使用head:

>>> llist.appendleft("z")
>>> llist
deque(['z', 'a', 'b', 'c', 'd', 'e'])

>>> llist.popleft()
'z'

>>> llist
deque(['a', 'b', 'c', 'd', 'e'])

使用deque对象在列表两端添加或删除元素非常简单。现在您已经准备好学习如何使用collections.deque来实现队列或堆栈。

Remove ads

如何实现队列和堆栈

正如您在上面了解到的,队列和堆栈之间的主要区别在于您从两者中检索元素的方式。接下来,您将了解如何使用collections.deque来实现这两种数据结构。

队列

对于队列,您希望向列表中添加值(enqueue),当时机合适时,您希望删除列表中最长的元素(dequeue)。例如,想象一下在一家时尚且客满的餐厅排队。如果您试图实现一个为客人安排座位的公平系统,那么您应该从创建一个队列并在客人到达时添加人员开始:

>>> from collections import deque
>>> queue = deque()
>>> queue
deque([])

>>> queue.append("Mary")
>>> queue.append("John")
>>> queue.append("Susan")
>>> queue
deque(['Mary', 'John', 'Susan'])

现在你有玛丽、约翰和苏珊在排队。记住,因为队列是先进先出的,所以第一个进入队列的人应该第一个出来。

现在想象一段时间过去了,有几张桌子空了出来。在此阶段,您希望以正确的顺序从队列中删除人员。你应该这样做:

>>> queue.popleft()
'Mary'

>>> queue
deque(['John', 'Susan'])

>>> queue.popleft()
'John'

>>> queue
deque(['Susan'])

每次调用popleft()时,您都从链表中移除 head 元素,模拟现实生活中的队列。

堆栈

如果您想创建一个堆栈呢?这个想法和排队差不多。唯一的区别是堆栈使用 LIFO 方法,这意味着堆栈中插入的最后一个元素应该首先被移除。

假设您正在创建一个 web 浏览器的历史功能,其中存储了用户访问的每个页面,以便他们可以轻松地回到过去。假设这些是随机用户在其浏览器上采取的操作:

  1. 访问真实 Python 的网站
  2. 导航到熊猫:如何读写文件
  3. 点击链接在 Python 中读写 CSV 文件

如果您想将此行为映射到堆栈中,那么您可以这样做:

>>> from collections import deque
>>> history = deque()

>>> history.appendleft("https://realpython.com/")
>>> history.appendleft("https://realpython.com/pandas-read-write-files/")
>>> history.appendleft("https://realpython.com/python-csv/")
>>> history
deque(['https://realpython.com/python-csv/',
 'https://realpython.com/pandas-read-write-files/',
 'https://realpython.com/'])

在这个例子中,您创建了一个空的history对象,每次用户访问一个新站点时,您使用appendleft()将它添加到您的history变量中。这样做确保了每个新元素都被添加到链表的head中。

现在假设用户阅读了两篇文章后,他们想返回到真正的 Python 主页去挑选一篇新文章来阅读。知道您有一个堆栈并想使用 LIFO 删除元素,您可以执行以下操作:

>>> history.popleft()
'https://realpython.com/python-csv/'

>>> history.popleft()
'https://realpython.com/pandas-read-write-files/'

>>> history
deque(['https://realpython.com/'])

这就对了。使用popleft(),从链表的head中移除元素,直到到达真正的 Python 主页。

从上面的例子中,您可以看到在您的工具箱中有collections.deque是多么有用,所以下次您有基于队列或堆栈的挑战需要解决时,一定要使用它。

Remove ads

实现你自己的链表

既然你已经知道了如何使用collections.deque来处理链表,你可能想知道为什么你要在 Python 中实现你自己的链表。这样做有几个原因:

  1. 练习您的 Python 算法技能
  2. 学习数据结构理论
  3. 准备工作面试

如果您对以上任何内容都不感兴趣,或者您已经成功地用 Python 实现了自己的链表,请随意跳过下一节。不然就该实现一些链表了!

如何创建链表

首先,创建一个来表示你的链表:

class LinkedList:
    def __init__(self):
        self.head = None

你需要为一个链表存储的唯一信息是链表的起始位置(链表的head)。接下来,创建另一个类来表示链表的每个节点:

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

在上面的类定义中,你可以看到每个单个节点的两个主要元素: datanext 。您还可以在两个类中添加一个__repr__,以便更好地表示对象:

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

    def __repr__(self):
        return self.data

class LinkedList:
    def __init__(self):
        self.head = None

    def __repr__(self):
        node = self.head
        nodes = []
        while node is not None:
            nodes.append(node.data)
            node = node.next
        nodes.append("None")
        return " -> ".join(nodes)

看一个使用上面的类快速创建一个有三个节点的链表的例子:

>>> llist = LinkedList()
>>> llist
None

>>> first_node = Node("a")
>>> llist.head = first_node
>>> llist
a -> None

>>> second_node = Node("b")
>>> third_node = Node("c")
>>> first_node.next = second_node
>>> second_node.next = third_node
>>> llist
a -> b -> c -> None

通过定义节点的datanext值,您可以非常快速地创建一个链表。这些LinkedListNode类是我们实现的起点。从现在开始,一切都是为了增加他们的功能。

这里对链表的__init__()做了一个小小的改变,让你可以快速创建一些数据的链表:

def __init__(self, nodes=None):
    self.head = None
    if nodes is not None:
        node = Node(data=nodes.pop(0))
        self.head = node
        for elem in nodes:
            node.next = Node(data=elem)
            node = node.next

通过上面的修改,创建下面例子中使用的链表将会快得多。

如何遍历链表

你对链表做的最常见的事情之一就是遍历它。遍历意味着遍历每一个节点,从链表的head开始,到next值为None的节点结束。

遍历只是迭代的一种更好的说法。因此,记住这一点,创建一个__iter__来向链表添加与普通列表相同的行为:

def __iter__(self):
    node = self.head
    while node is not None:
        yield node
        node = node.next

上面的方法遍历列表,产生每个节点的。关于这个__iter__要记住的最重要的事情是,你需要始终验证当前的node不是None。当条件为True时,意味着已经到达链表的末尾。

在生成当前节点后,您希望移动到列表中的下一个节点。这就是你加node = node.next的原因。下面是一个遍历随机列表并打印每个节点的例子:

>>> llist = LinkedList(["a", "b", "c", "d", "e"])
>>> llist
a -> b -> c -> d -> e -> None

>>> for node in llist:
...     print(node)
a
b
c
d
e

在其他文章中,您可能会看到遍历被定义到一个名为traverse()的特定方法中。然而,使用 Python 的内置方法来实现上述行为,使得这个链表的实现更加Python 化

Remove ads

如何插入新节点

将新节点插入链表有不同的方法,每种方法都有自己的实现和复杂程度。这就是为什么你会看到它们被分成特定的方法,用于在列表的开头、结尾或节点之间插入。

在开头插入

在列表的开头插入一个新的节点可能是最直接的插入方式,因为你不需要遍历整个列表。这都是关于创建一个新节点,然后将列表的head指向它。

看看下面这个类LinkedListadd_first()实现:

def add_first(self, node):
    node.next = self.head
    self.head = node

在上面的例子中,您将self.head设置为新节点的next引用,以便新节点指向旧的self.head。之后,您需要声明列表的新head是插入的节点。

下面是它在示例列表中的表现:

>>> llist = LinkedList()
>>> llist
None

>>> llist.add_first(Node("b"))
>>> llist
b -> None

>>> llist.add_first(Node("a"))
>>> llist
a -> b -> None

如您所见,add_first()总是将节点添加到列表的head中,即使列表之前是空的。

在末尾插入

在列表末尾插入一个新节点会迫使您首先遍历整个链表,并在到达末尾时添加新节点。你不能像普通链表那样追加到末尾,因为在链表中你不知道哪个节点是最后一个。

下面是一个在链表末尾插入节点的函数的实现示例:

def add_last(self, node):
    if self.head is None:
        self.head = node
        return
    for current_node in self:
        pass
    current_node.next = node

首先,你要遍历整个列表,直到到达末尾(也就是说,直到 for循环引发一个StopIteration异常)。接下来,您希望将current_node设置为列表中的最后一个节点。最后,您希望添加新节点作为那个current_nodenext值。

这里有一个add_last()的例子:

>>> llist = LinkedList(["a", "b", "c", "d"])
>>> llist
a -> b -> c -> d -> None

>>> llist.add_last(Node("e"))
>>> llist
a -> b -> c -> d -> e -> None

>>> llist.add_last(Node("f"))
>>> llist
a -> b -> c -> d -> e -> f -> None

在上面的代码中,首先创建一个包含四个值的列表(abcd)。然后,当您使用add_last()添加新节点时,您可以看到这些节点总是被追加到列表的末尾。

在两个节点之间插入

在两个节点之间插入又增加了一层复杂性,因为有两种不同的方法可以使用:

  1. 在现有节点后插入 ** 在现有节点之前插入

*将它们分成两个方法可能看起来很奇怪,但是链表的行为不同于普通的链表,每种情况都需要不同的实现。

下面是一个方法,它在具有特定数据值的现有节点后添加一个节点*:*

def add_after(self, target_node_data, new_node):
    if self.head is None:
        raise Exception("List is empty")

    for node in self:
        if node.data == target_node_data:
            new_node.next = node.next
            node.next = new_node
            return

    raise Exception("Node with data '%s' not found" % target_node_data)

在上面的代码中,您正在遍历链表,寻找带有数据的节点,该数据指示您想要插入新节点的位置。当您找到您正在寻找的节点时,您将在它之后立即插入新节点,并重新连接next引用以保持列表的一致性。

唯一的例外是列表为空,使得无法在现有节点后插入新节点,或者列表不包含要搜索的值。以下是几个关于add_after()行为的例子:

>>> llist = LinkedList()
>>> llist.add_after("a", Node("b"))
Exception: List is empty

>>> llist = LinkedList(["a", "b", "c", "d"])
>>> llist
a -> b -> c -> d -> None

>>> llist.add_after("c", Node("cc"))
>>> llist
a -> b -> c -> cc -> d -> None

>>> llist.add_after("f", Node("g"))
Exception: Node with data 'f' not found

试图在空列表上使用add_after()会导致异常。当您试图在一个不存在的节点后添加时,也会发生同样的情况。其他一切都按预期运行。

现在,如果你想实现add_before(),那么它看起来会像这样:

 1def add_before(self, target_node_data, new_node):
 2    if self.head is None: 3        raise Exception("List is empty")
 4
 5    if self.head.data == target_node_data: 6        return self.add_first(new_node)
 7
 8    prev_node = self.head
 9    for node in self: 10        if node.data == target_node_data:
11            prev_node.next = new_node
12            new_node.next = node
13            return
14        prev_node = node
15
16    raise Exception("Node with data '%s' not found" % target_node_data)

在实现上述内容时,有一些事情需要记住。首先,和add_after()一样,如果链表是空的(第 2 行)或者您正在寻找的节点不存在(第 16 行),您希望确保引发一个异常。

其次,如果您试图在列表的头部(第 5 行)之前添加一个新节点,那么您可以重用add_first(),因为您插入的节点将是列表的新head

最后,对于任何其他情况(第 9 行),您应该使用prev_node变量跟踪最后检查的节点。然后,当您找到目标节点时,您可以使用那个prev_node变量来重新连接next值。

再说一次,一个例子胜过千言万语:

>>> llist = LinkedList()
>>> llist.add_before("a", Node("a"))
Exception: List is empty

>>> llist = LinkedList(["b", "c"])
>>> llist
b -> c -> None

>>> llist.add_before("b", Node("a"))
>>> llist
a -> b -> c -> None

>>> llist.add_before("b", Node("aa"))
>>> llist.add_before("c", Node("bb"))
>>> llist
a -> aa -> b -> bb -> c -> None

>>> llist.add_before("n", Node("m"))
Exception: Node with data 'n' not found

有了add_before(),您现在拥有了在列表中任意位置插入节点所需的所有方法。

Remove ads

如何删除一个节点

要从链表中删除一个节点,首先需要遍历链表,直到找到要删除的节点。找到目标后,您希望链接它的上一个和下一个节点。这种重新链接是从列表中删除目标节点的原因。

这意味着在遍历列表时,需要跟踪上一个节点。请看一个示例实现:

 1def remove_node(self, target_node_data):
 2    if self.head is None: 3        raise Exception("List is empty")
 4
 5    if self.head.data == target_node_data: 6        self.head = self.head.next
 7        return
 8
 9    previous_node = self.head
10    for node in self: 11        if node.data == target_node_data:
12            previous_node.next = node.next
13            return
14        previous_node = node
15
16    raise Exception("Node with data '%s' not found" % target_node_data)

在上面的代码中,您首先检查您的列表是否为空(第 2 行)。如果是,那么您将引发一个异常。之后,检查要删除的节点是否是列表的当前head(第 5 行)。如果是,那么您希望列表中的下一个节点成为新的head

如果上述情况都没有发生,那么您开始遍历列表,寻找要删除的节点(第 10 行)。如果您找到了它,那么您需要更新它的前一个节点以指向它的下一个节点,自动从列表中删除找到的节点。最后,如果遍历整个列表,却没有找到要删除的节点(第 16 行),那么就会引发一个异常。

注意在上面的代码中,你如何使用previous_node来跟踪上一个节点。这样做可以确保当您找到要删除的正确节点时,整个过程会简单得多。

下面是一个使用列表的示例:

>>> llist = LinkedList()
>>> llist.remove_node("a")
Exception: List is empty

>>> llist = LinkedList(["a", "b", "c", "d", "e"])
>>> llist
a -> b -> c -> d -> e -> None

>>> llist.remove_node("a")
>>> llist
b -> c -> d -> e -> None

>>> llist.remove_node("e")
>>> llist
b -> c -> d -> None

>>> llist.remove_node("c")
>>> llist
b -> d -> None

>>> llist.remove_node("a")
Exception: Node with data 'a' not found

就是这样!现在,您已经知道了如何实现一个链表,以及遍历、插入和删除节点的所有主要方法。如果你对你所学的感到满意,并且渴望更多,那么请随意选择下面的一个挑战:

  1. 创建一个方法从特定的位置检索元素:get(i)甚至llist[i]
  2. 创建反转链表的方法:llist.reverse()
  3. enqueue()dequeue()方法创建一个继承这篇文章的链表的Queue()对象。

除了是很好的练习,自己做一些额外的挑战也是吸收你所获得的所有知识的有效方法。如果您想重用本文中的所有源代码,那么您可以从下面的链接下载您需要的所有内容:

获取源代码: 点击此处获取源代码,您将使用在本教程中学习链表。

使用高级链表

到目前为止,你已经学习了一种叫做单链表的特定类型的链表。但是有更多类型的链表可以用于稍微不同的目的。

如何使用双向链表

双向链表不同于单向链表,因为它们有两个引用:

  1. previous字段引用前一个节点。
  2. next字段引用下一个节点。

最终结果如下所示:

Example Node of a Doubly Linked List

Node (Doubly Linked List)

如果您想要实现上述内容,那么您可以对现有的Node类进行一些更改,以便包含一个previous字段:

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None
        self.previous = None

这种实现将允许您双向遍历列表,而不仅仅是使用next来遍历。你可以用next前进,用previous后退。

就结构而言,双向链表应该是这样的:

Example Structure of a Doubly Linked List

Doubly Linked List

您之前了解到collections.deque使用链表作为其数据结构的一部分。这就是它使用的那种链表。使用双向链表,deque能够在队列的两端插入或删除元素,并且具有恒定的 O (1)性能。

Remove ads

如何使用循环链表

循环链表是最后一个节点指向链表的head而不是指向None的一种链表。这就是为什么它们是圆形的。循环链表有很多有趣的用例:

  • 在多人游戏中,每个玩家轮流玩
  • 管理给定操作系统的应用程序生命周期
  • 实现一个斐波那契堆

循环链表是这样的:

Example Structure of a Circular Linked List

Circular Linked List

循环链表的优点之一是可以从任意节点开始遍历整个链表。由于最后一个节点指向列表的head,所以需要确保在到达起点时停止遍历。否则,你会陷入无限循环。

在实现方面,循环链表和单链表非常相似。唯一的区别是,您可以在遍历列表时定义起点:

class CircularLinkedList:
    def __init__(self):
        self.head = None

    def traverse(self, starting_point=None):
        if starting_point is None:
            starting_point = self.head
        node = starting_point
        while node is not None and (node.next != starting_point):
            yield node
            node = node.next
        yield node

    def print_list(self, starting_point=None):
        nodes = []
        for node in self.traverse(starting_point):
            nodes.append(str(node))
        print(" -> ".join(nodes))

现在遍历列表会收到一个额外的参数starting_point,用于定义迭代过程的开始和结束(因为列表是循环的)。除此之外,大部分代码和我们在LinkedList类中的一样。

作为最后一个例子的总结,看看当你给它一些数据时,这种新类型的列表是如何表现的:

>>> circular_llist = CircularLinkedList()
>>> circular_llist.print_list()
None

>>> a = Node("a")
>>> b = Node("b")
>>> c = Node("c")
>>> d = Node("d")
>>> a.next = b
>>> b.next = c
>>> c.next = d
>>> d.next = a
>>> circular_llist.head = a
>>> circular_llist.print_list()
a -> b -> c -> d

>>> circular_llist.print_list(b)
b -> c -> d -> a

>>> circular_llist.print_list(d)
d -> a -> b -> c

你有它!您会注意到在遍历列表时不再有None。这是因为循环列表没有特定的结尾。您还可以看到,选择不同的起始节点将呈现相同列表的稍微不同的表示。

结论

在这篇文章中,你学到了不少东西!最重要的是:

  • 什么是链表,什么时候应该使用它们
  • 如何使用collections.deque实现队列和堆栈
  • 如何实现你自己的链表和节点类,以及相关的方法
  • 其他类型的链表是什么,它们可以用来做什么

如果你想了解更多关于链表的知识,那么看看 Vaidehi Joshi 的博文中一个漂亮的视觉解释。如果你对更深入的指南感兴趣,那么维基百科文章相当全面。最后,如果你对当前实现collections.deque背后的推理感到好奇,那么看看雷蒙德·赫廷格的线程

您可以点击以下链接下载本教程中使用的源代码:

获取源代码: 点击此处获取源代码,您将使用在本教程中学习链表。

欢迎在下面留下任何问题或评论。快乐的蟒蛇!

立即观看本教程有真实 Python 团队创建的相关视频课程。和书面教程一起看,加深理解: 用 Python 处理链表********