Python 中的链表:简介

举报
Yuchuan 发表于 2021/12/14 22:29:14 2021/12/14
【摘要】 在这篇文章中,你学到了很多东西!最重要的是: 什么是链表以及何时应该使用它们 如何使用collections.deque实现队列和栈 如何实现自己的链表和节点类,以及相关方法 其他类型的链表是什么以及它们的用途

目录

链表就像一个鲜为人知的列表表亲。它们不那么受欢迎或那么酷,你甚至可能不记得你的算法课上的它们。但在正确的背景下,它们真的可以发光。

在本文中,您将了解:

  • 什么是链表以及何时应该使用它们
  • 如何使用collections.deque您所有的链表需求
  • 如何实现自己的链表
  • 其他类型的链表是什么以及它们的用途

如果您想在求职面试中提高编码技能,或者除了常用的字典列表之外,还想了解更多关于Python 数据结构的知识,那么您来对地方了!

了解链表

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

主要概念

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

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

下面是一个典型的节点:

链表的示例节点

节点

链表是节点的集合。第一个节点称为head,它用作遍历列表的任何迭代的起点。最后一个节点必须有它的next引用指向None以确定列表的结尾。这是它的外观:

链表的示例结构

链表

既然您已经了解了链表的结构,您就可以查看它的一些实际用例了。

实际应用

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

队列或堆栈

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

队列结构示例

队列

在上面的图中,可以看到前面后面的队列的元素。当您向队列中添加新元素时,它们将进入后端。当您检索元素时,它们将从队列的前面取出。

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

堆栈的示例结构

在上图中,您可以看到堆栈中插入的第一个元素(索引0)在底部,而最后插入的元素在顶部。由于堆栈使用 LIFO 方法,插入的最后一个元素(在顶部)将首先被检索。

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

图表

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

示例有向图

有向无环图

有多种方法可以实现上述图形,但最常见的一种方法是使用邻接表。邻接表本质上是一个链表列表,其中图的每个顶点都与一组连接的顶点一起存储:

顶点 顶点链表
1 2 → 3 → 无
2 4 → 无
3 没有任何
4 5 → 6 → 无
5 6 → 无
6 没有任何

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

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

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

注意:在上面的示例中,您可以避免存储None值,但为了与后面的示例保持清晰和一致性,我们将它们保留在此处。

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

性能比较:列表与链接列表

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

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

由于列表和链表之间的内存使用差异如此微不足道,因此在涉及时间复杂度时,您最好关注它们的性能差异。

元素的插入和删除

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

这些方法之间的主要区别在于您使用.insert()and.remove()插入或删除列表中特定位置的元素,但您使用.append()and.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模块中有一个特定对象可用于链表,称为deque(发音为“deck”),它代表双端队列

collections.deque使用链表的实现,您可以在其中访问、插入或删除列表开头或结尾的元素,性能恒定为O (1)。

如何使用 collections.deque

默认情况下,有很多方法带有deque对象。但是,在本文中,您将只涉及其中的几个,主要用于添加或删除元素。

首先,您需要创建一个链表。您可以使用以下代码段来执行此操作deque

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

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

>>>
>>> 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对象时,您可以将任何可迭代对象作为输入传递,例如字符串(也是可迭代对象)或对象列表。

既然您知道如何创建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来实现队列或堆栈。

如何实现队列和堆栈

正如您在上面了解到的,队列和堆栈之间的主要区别在于您从每个队列中检索元素的方式。接下来,您将了解如何使用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'])

现在,队列中有 Mary、John 和 Susan。请记住,由于队列是先进先出的,第一个进入队列的人应该最先出去。

现在想象一下,随着时间的流逝,有几张桌子可用。在此阶段,您希望以正确的顺序从队列中删除人员。这是你将如何做到这一点:

>>>
>>> queue.popleft()
'Mary'

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

>>> queue.popleft()
'John'

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

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

堆栈

如果你想创建一个堆栈呢?嗯,这个想法或多或少与队列相同。唯一的区别是堆栈使用 LIFO 方法,这意味着要插入堆栈的最后一个元素应该是第一个被删除的元素。

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

  1. 访问Real Python 的网站
  2. 导航到Pandas:如何读写文件
  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对象,并且每次用户访问新站点时,您都​​使用 将其添加到history变量中appendleft()。这样做可以确保每个新元素都添加到head链表的 中。

现在假设用户阅读完这两篇文章后,他们想返回 Real 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链表的 中删除元素,直到您到达 Real Python 主页。

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

实现你自己的链表

既然您知道如何使用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 的内置方法来实现上述行为会使这个链表实现更加Pythonic

如何插入新节点

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

在开头插入

在列表的开头插入一个新节点可能是最直接的插入,因为您不必遍历整个列表来执行此操作。这一切都是关于创建一个新节点,然后head将列表的 指向它。

看看add_first()类的以下实现LinkedList

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

在上面的示例中,您设置self.headnext新节点的引用,以便新节点指向旧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为列表中的最后一个节点。最后,您想添加新节点作为nextthat的值current_node

下面是一个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. 现有节点之后插入
  2. 现有节点之前插入

将它们分成两种方法似乎很奇怪,但是链接列表的行为与普通列表不同,并且您需要针对每种情况使用不同的实现。

这是具有特定数据值的现有节点之后添加节点的方法:

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(),您现在拥有了在列表中任意位置插入节点所需的所有方法。

如何删除节点

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

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

 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. Queue()enqueue()dequeue()方法创建一个继承本文链表的对象。

除了是很好的练习之外,自己做一些额外的挑战是吸收所有知识的有效方法。如果您想通过重用本文中的所有源代码来抢先一步,那么您可以从以下链接下载所需的一切:

使用高级链表

到目前为止,您一直在学习一种称为单向链表的特定类型的链表。但是有更多类型的链表可用于略有不同的目的。

如何使用双向链表

双链表与单链表的不同之处在于它们有两个引用:

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

最终结果如下所示:

双向链表的示例节点

节点(双向链表)

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

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

这种实现将允许您在两个方向上遍历列表,而不仅仅是使用next. 你可以next用来前进和previous后退。

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

双向链表的示例结构

双向链表

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

如何使用循环链表

循环链表是一种链表,其中最后一个节点指向head链表的 ,而不是指向None。这就是使它们循环的原因。循环链表有很多有趣的用例:

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

这是一个循环链表的样子:

循环链表的示例结构

循环链表

循环链表的优点之一是可以从任意节点开始遍历整个链表。由于最后一个节点指向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 的 Medium 帖子以获得一个很好的视觉解释。如果您对更深入的指南感兴趣,那么维基百科文章非常详尽。最后,如果您对 的当前实现背后的推理感到好奇collections.deque,请查看Raymond Hettinger 的线程

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。