: [None]
这个字典的键是源顶点,每个键的值是一个列表。这个列表通常实现为链表。
Note:在上面的示例中,您可以避免存储None值,但是为了清晰和与后面的示例保持一致,我们在这里保留了它们。
在速度和内存方面,使用邻接表来实现图是非常有效的,例如,与邻接矩阵相比。这就是链表在图形实现中如此有用的原因。
性能比较:列表和链表
在大多数编程语言中,链表和数组在内存中的存储方式有明显的不同。然而,在Python中,列表是动态数组。这意味着列表和链表的内存使用非常相似。
参考阅读:http://www.laurentluce.com/posts/python-list-implementation/
由于列表和链表在内存使用方面的差异非常小,所以在时间复杂度方面,最好关注它们的性能差异。
元素的插入和删除
在Python中,可以使用.insert()或.append()将元素插入到列表中。要从列表中删除元素,可以使用对应的.remove()和.pop()。
这些方法之间的主要区别在于,使用.insert()和.remove()在列表的特定位置插入或删除元素,而使用.append()和.pop()只在列表的末尾插入或删除元素。
现在,关于Python列表,您需要知道的是,插入或删除不在列表末尾的元素需要在后台进行一些元素移动,这使得操作花费的时间更复杂。为了更好地理解.insert()、.remove()、.append()和.pop()的实现如何影响它们的性能,可以阅读上面提到的关于Python中如何实现列表的文章。
记住这一切,即使使用.append()或.insert()在列表的末尾插入元素,其时间复杂度为O(1),当你在列表的开头插入一个元素,平均时间复杂度会随着列表的大小而变化,即O(n)。
另一方面,链表在插入和删除链表开头或结尾的元素时要简单得多,它们的时间复杂度始终是常数O(1)。
由于这个原因,在实现队列(FIFO)时,链表比普通列表具有性能优势,在队列(FIFO)中,元素会在链表的开始处不断插入和删除。但在实现堆栈(LIFO)时,它们的执行类似于列表,在堆栈中,在列表的末尾插入和删除元素。
在元素查找方面,列表比链表的性能要好得多。当您知道要访问哪个元素时,list可以在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(['a','b','c'])
deque('abc')
deque([{'data': 'a'}, {'data': 'b'}])
初始化deque对象时,可以传递任意可迭代对象作为输入,比如字符串(也是可迭代对象)或对象列表。
现在您已经知道了如何创建deque对象,您可以通过添加或删除元素来与它进行交互。你可以创建一个abcde链表,并像这样添加一个新元素f:
llist = deque("abcde")
llist
llist.append("f")
llist
llist.pop()
llist
append()和pop()都是从链表右侧添加或删除元素。不过,你也可以使用deque快速添加或删除列表左侧或头部的元素:
llist.appendleft("z")
llist
llist.popleft()
llist
使用deque对象从列表的两端添加或删除元素非常简单。现在您已经准备好学习如何使用collections.deque来实现队列或堆栈。
如何实现队列和堆栈
如上所述,队列和堆栈之间的主要区别在于从每个队列检索元素的方式。接下来,您将了解如何使用collections.deque实现这两种数据结构。
对于队列,您希望向列表添加值(enqueue),当时机合适时,您希望删除列表中最长的元素(dequeue)。例如,想象在一家时髦而客满的餐厅里排队。如果你想为客人安排一个公平的座位,那么你可以先排一个队,然后在他们到达的时候添加一些人:
from collections import deque
queue = deque()
queue
queue.append("Mary")
queue.append("John")
queue.append("Susan")
queue
现在玛丽、约翰和苏珊都在排队。记住,由于队列是FIFO,第一个进入队列的人应该是第一个离开队列的人。
现在想象一下,过了一段时间,出现了一些可用的表。在此阶段,您希望按照正确的顺序将人员从队列中删除。你可以这样做:
queue.popleft()
queue
queue.popleft()
queue
每次调用popleft()时,都会从链表中删除head元素,模拟真实的队列。
如果您想要创建一个堆栈呢?这个想法或多或少和队列是一样的。唯一的区别是堆栈使用后进先出(LIFO)方法,这意味着最后插入堆栈的元素应该是第一个被移除的元素。
假设你正在创建一个web浏览器的历史记录功能,在这个功能中存储用户访问的每个页面,这样他们就可以很容易地回到过去。假设这些是随机用户在浏览器上的操作:
访问Real Python的网站
导航到Pandas:如何读取和写入文件
单击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
在本例中,您创建了一个空的历史对象,并且每次用户访问新站点时,您都使用appendleft()将其添加到历史变量中。这样做可以确保每个新元素都被添加到链表的头。
现在假设用户在阅读了这两篇文章之后,想要回到Real Python主页选择一篇新的文章来阅读。知道你有一个堆栈,想要使用LIFO删除元素,你可以做以下事情:
history.popleft()
history.popleft()
history
你走吧!使用popleft(),可以从链表的头部删除元素,直到到达Real Python主页。
从上面的例子中,您可以看到在工具箱中有collections.deque是多么有用,所以下次遇到基于队列或堆栈的挑战时,一定要使用它。
实现你自己的链表
既然您已经知道如何使用collections.deque来处理链表,您可能会想,为什么要在Python中实现自己的链表呢?有几个理由:
练习你的Python算法技能
学习数据结构理论
为工作面试做准备
如果您对上面的任何内容都不感兴趣,或者您已经熟练地用Python实现了自己的链表,可以跳过下一节。否则,是时候实现一些链表了!
如何创建链表
首先,创建一个类来表示你的链表:
class LinkedList:
def __init__(self):
self.head = None
对于链表,您需要存储的唯一信息是链表开始的位置(链表的头部)。接下来,创建另一个类来表示链表的每个节点:
class Node:
def __init__(self, data):
self.data = data
self.next = None
在上面的类定义中,您可以看到每个节点的两个主要元素:data和next。你也可以在这两个类中添加__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
first_node = Node("a")
llist.head = first_node
llist
second_node = Node("b")
third_node = Node("c")
first_node.next = second_node
second_node.next = third_node
llist
通过定义节点的数据和下一个值,您可以非常快速地创建一个链表。这些LinkedList和Node类是我们实现的起点。从现在开始,我们要做的就是增加它们的功能。
下面是对链表的__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
通过上述修改,创建链表以在下面的示例中使用将会快得多。
如何遍历链表
使用链表最常见的事情之一就是遍历它。遍历意味着遍历每个节点,从链表的头开始,到下一个值为None的节点结束。
遍历只是迭代的一种更花哨的说法。所以,记住这一点,创建一个__iter__来添加与普通链表相同的行为:
def __iter__(self):
node = self.head
while node is not None:
yield node
node = node.next
上面的方法遍历列表并yield每个节点。关于这个__iter__,要记住的最重要的事情是,您需要始终验证当前节点不是None。当该条件为True时,表示已到达链表的末尾。
在生成当前节点之后,您希望移动到列表中的下一个节点。这就是为什么要添加node = node.next。下面是一个遍历随机列表并打印每个节点的示例:
llist = LinkedList(["a", "b", "c", "d", "e"])
llist
for node in llist:
print(node)
在其他文章中,您可能会看到将遍历定义为一个名为traverse()的特定方法。然而,使用Python的内置方法来实现上述行为会使这个链表实现更具Python风格。
如何插入新节点
将新节点插入链表有不同的方法,每种方法都有自己的实现和复杂程度。这就是为什么您会看到它们被分割成特定的方法,用于在列表的开头、末尾或节点之间插入。
在开头插入
在列表的开始插入一个新节点可能是最简单的插入,因为您不需要遍历整个列表来完成它。只需要创建一个新节点,然后将列表头指向它。
看一下LinkedList类中add_first()的实现:
def add_first(self, node):
node.next = self.head
self.head = node
在上面的例子中,你设置了self。head作为新节点的下一个引用,这样新节点就会指向旧的self.head。在此之后,您需要声明列表的新头部是插入的节点。
下面是它如何使用:
llist = LinkedList()
llist
llist.add_first(Node("b"))
llist
llist.add_first(Node("a"))
llist
如您所见,add_first()总是将节点添加到列表的头部,即使列表之前是空的。
在末尾插入
在列表末尾插入新节点将迫使您首先遍历整个链表,并在到达链表末尾时添加新节点。你不能像在普通列表中那样在末尾添加内容,因为在链表中你不知道哪个节点是最后的。
下面是一个将节点插入到链表末尾的函数的示例实现:
def add_last(self, node):
if self.head is None:
self.head = node
return
for current_node in self:
current_node.next = node
首先,您希望遍历整个列表,直到到达末尾(也就是说,直到for循环引发StopIteration异常)。接下来,要将current_node设置为列表上的最后一个节点。最后,您希望添加新节点作为current_node的下一个值。
下面是一个add_last()的例子:
llist = LinkedList(["a", "b", "c", "d"])
llist
llist.add_last(Node("e"))
llist
llist.add_last(Node("f"))
llist
在上面的代码中,首先创建一个有四个值(a、b、c和d)的列表。然后,当使用add_last()添加新节点时,可以看到节点总是被添加到列表的末尾。
节点间插入
在两个节点之间插入会给已经很复杂的链表插入增加一层复杂性,因为你可以使用两种不同的方法:
在已有节点后插入
在现有节点前插入
将它们分成两个方法似乎有些奇怪,但链表的行为与普通链表不同,每种情况都需要不同的实现。
下面是一个方法,它将一个节点添加到一个具有特定数据值的现有节点之后:
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)
在上面的代码中,您遍历链表,寻找具有指示要在何处插入新节点的数据的节点。当找到要查找的节点时,将立即插入新节点,并重新连接下一个引用,以保持列表的一致性。
唯一的例外是,如果列表为空,则不可能在现有节点之后插入新节点,或者如果列表不包含您正在搜索的值。下面是add_after()的一些例子:
llist = LinkedList()
llist.add_after("a", Node("b"))
llist = LinkedList(["a", "b", "c", "d"])
llist
llist.add_after("c", Node("cc"))
llist
llist.add_after("f", Node("g"))
在空列表上使用add_after()会导致异常。当您试图在不存在的节点之后添加时,也会发生同样的情况。其他一切都按预期工作。
现在,如果你想实现add_before(),那么它看起来像这样:
def add_before(self, target_node_data, new_node):
if self.head is None:
raise Exception("List is empty")
if self.head.data == target_node_data:
return self.add_first(new_node)
prev_node = self.head
for node in self:
if node.data == target_node_data:
prev_node.next = new_node
new_node.next = node
return
prev_node = node
raise Exception("Node with data '%s' not found" % target_node_data)
在实现上面的方法时,有一些事情需要记住。首先,与add_after()一样,如果链表为空(第2行)或查找的节点不存在(第16行),则需要确保引发异常。
其次,如果您试图在列表头之前添加一个新节点(第5行),那么您可以重用add_first(),因为您所插入的节点将是列表的新头。
最后,对于任何其他情况(第9行),您应该使用prev_node变量跟踪最后检查的节点。然后,在找到目标节点时,可以使用prev_node变量重新连接下一个值。
再一次,一个例子胜过千言万语:
llist = LinkedList()
llist.add_before("a", Node("a"))
llist = LinkedList(["b", "c"])
llist
llist.add_before("b", Node("a"))
llist
llist.add_before("b", Node("aa"))
llist.add_before("c", Node("bb"))
llist
llist.add_before("n", Node("m"))
有了add_before(),您现在就有了在列表中任何位置插入节点所需的所有方法。
如何移除节点
要从链表中删除一个节点,首先需要遍历该列表,直到找到想要删除的节点。找到目标后,希望链接它的上一个和下一个节点。这种重新链接将目标节点从列表中删除。
这意味着在遍历列表时需要跟踪上一个节点。看看一个示例实现:
def remove_node(self, target_node_data):
if self.head is None:
raise Exception("List is empty")
if self.head.data == target_node_data:
self.head = self.head.next
return
previous_node = self.head
for node in self:
if node.data == target_node_data:
previous_node.next = node.next
return
previous_node = node
raise Exception("Node with data '%s' not found" % target_node_data)
在上面的代码中,首先检查列表是否为空(第2行)。如果为空,则抛出异常。然后,检查要删除的节点是否为列表的当前头(第5行),如果是,则希望列表中的下一个节点成为新的头。
如果没有出现上述情况,则开始遍历列表,寻找要删除的节点(第10行)。如果找到它,则需要更新它的上一个节点以指向它的下一个节点,从而自动从列表中删除找到的节点。最后,如果遍历整个列表而没有找到要删除的节点(第16行),则会引发异常。
注意,在上面的代码中,您是如何使用previous_node来跟踪上一个节点的。这样做可以确保当您找到要删除的正确节点时,整个过程将更加直观。
下面是一个例子:
llist = LinkedList()
llist.remove_node("a")
llist = LinkedList(["a", "b", "c", "d", "e"])
llist
llist.remove_node("a")
llist
llist.remove_node("e")
llist
llist.remove_node("c")
llist
llist.remove_node("a")
就是这样!现在您知道了如何实现链表以及遍历、插入和删除节点的所有主要方法。如果你对自己所学的知识感到满意,并且渴望更多,那么可以选择以下挑战之一:
创建一个方法来从特定的位置检索元素:get(i)或者llist[i]。
创建一个方法来反转链表:list.reverse()。
使用enqueue()和dequeue()方法创建继承本文链接列表的Queue()对象。
除了很好的练习,自己做一些额外的挑战也是吸收你所学知识的有效方法。如果你想重新使用本文中的所有源代码,那么你可以从下面的链接下载你需要的一切:https://realpython.com/bonus/linked-lists/
使用高级链表
到目前为止,您一直在学习一种特定类型的链表,称为单链表。但是还有更多类型的链表可以用于稍微不同的目的。
如何使用双链表
双链表不同于单链表,它们有两个引用:
前面的字段引用前面的节点。
下一个字段引用下一个节点。
最终结果是这样的:
如果你想实现上面的内容,那么你可以对现有的Node类做一些修改,以包含以前的字段:
class Node:
def __init__(self, data):
self.data = data
self.next = None
self.previous = None
这种实现将允许您从两个方向遍历列表,而不是只使用next遍历列表。你可以用next来前进,用previous来后退。
在结构方面,这是一个双链表的样子:
如何使用循环链表
循环链表是一种链表,它的最后一个节点指向链表的头,而不是指向None。这就是为什么它们是圆形的。循环链表有很多有趣的用例:
多人游戏中每个玩家的回合
管理给定操作系统的应用程序生命周期
实现斐波那契堆
这是一个循环链表的样子:
循环链表的优点之一是可以从任何节点开始遍历整个链表。由于最后一个节点指向列表的头部,因此需要确保在到达起始点时停止遍历。否则,您将进入一个无限循环。
在实现方面,循环链表与单链表非常相似。唯一的区别是你可以在遍历列表时定义起点:
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()
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()
circular_llist.print_list(b)
circular_llist.print_list(d)
这就对了!在遍历列表时,您将注意到不再有None。这是因为循环列表没有特定的结束。您还可以看到,选择不同的开始节点将呈现相同列表的略有不同的表示。
在本文中,您学到了不少东西!最重要的是:
什么是链表,什么时候应该使用链表
如何使用collections.deque来实现队列和堆栈
如何实现你自己的链表和节点类,加上相关的方法
其他类型的链表是什么?它们的用途是什么
如果你想了解更多关于链表的知识,请查看Vaidehi Joshi’s Medium post(https://medium.com/basecs/whats-a-linked-list-anyway-part-1-d8b7e6508b9d),它提供了一个很好的视觉解释。如果你对更深入的指南感兴趣,那么Wikipedia article(https://en.wikipedia.org/wiki/Linked_list)是相当全面的。最后,如果您对collections.deque当前实现背后的原因感到好奇,那么请查看Raymond Hettinger’s thread(https://mail.python.org/pipermail/python-dev/2007-November/075244.html)。