问题描述
如果一个队列是用链表实现的,但链表只有一个 对 head 的引用,enqueue 的运行时间是多少? 出队的运行时间?
解决方法
让我们从 Wikipedia's article on Queue 的引用开始:
FIFO 队列有几种有效的实现方式。一种有效的实现是可以在 O(1) 时间内执行入队和出队操作。
-
链接列表
- [...]
- 常规单向链表只有在一端具有高效的插入和删除功能。 [...]
这真的是本质。由于入队和出队操作会影响列表两端的列表,而我们只有这两端之一的引用,我们将不得不“走”到另一端才能在那里执行其他操作。
我们可以选择链表的哪一端作为项目入队(添加)的一端,而另一端作为项目出队(提取)的一端。链表的 head
可以是——这是一个设计选择。
假设enqueue会在链表的末尾添加一个条目(在链表的当前尾部之后),并且dequeue 将返回头条目,并从列表中删除该节点。那么代码会是这样的——我以 Python 为例:
class Node:
def __init__(self,value):
self.value = value
self.next = None
class Queue:
def __init__(self):
self.head = None
def enqueue(self,value):
if self.head is None:
self.head = Node(value)
else:
node = self.head
while node.next is not None:
node = node.next
node.next = Node(value)
def dequeue(self):
if self.head is None:
raise ValueError("Queue is empty")
else:
value = self.head.value
self.head = self.head.next
return value
所以对于enqueue
,我们需要遍历列表,从head
节点开始,找到列表的尾部在哪里。然后我们为给定的值创建一个节点并将它链接到该尾节点之后,使列表更长一个条目。由于我们需要遍历整个列表才能执行加法,因此时间复杂度为 O(n)。
对于dequeue
,我们立即得到了需要获取值的节点:它是head
节点。所以这里不需要遍历列表。我们只需更改 head
引用,以便它引用列表中的下一个节点,并从中删除第一个节点。此操作的时间复杂度为 O(1)。
现在,我们还可以采取替代设计决策,其中链表将向另一个方向增长,enqueue
将在当前 head
节点之前添加一个新节点。 dequeue
方法然后必须获取尾节点中的值并从列表中删除该尾节点。如您所见,现在 enqueue
操作的时间复杂度为 O(1),而 dequeue
的时间复杂度为 O(n)。
所以无论哪种方式,两者中都会有一个的时间复杂度为 O(n),而另一个的时间复杂度为 O(1)。
,实际上可以构建一个由链表支持的队列,该队列只存储指向列表头部的指针,但入队和出队都需要时间 O(1)。这个想法是使用一个循环链接的双向链表,使得列表的第一个元素向后指向最后一个元素,列表的最后一个元素向前指向第一个元素。例如:
head
| +---+ +---+ +---+ +---+
+--> | 1 | <-> | 2 | <-> | 3 | <-> | 4 |
+---+ +---+ +---+ +---+
^ ^
| |
+-----------------------------+
要在 O(1) 时间内执行出队,我们只需删除并返回第一个链表单元格。正如我们所做的那样,我们在它之前和之后重新连接单元格以相互指向,如下所示:
head -------------+
|
v
+---+ +---+ +---+
| 2 | <-> | 3 | <-> | 4 |
+---+ +---+ +---+
^ ^
| |
+-------------------+
要在 O(1) 时间内执行入队,请在第一个元素和最后一个元素之间插入一个新值,如下所示:
head -------------+
|
v
+---+ +---+ +---+
| 2 | <-> | 3 | <-> | 4 |
+---+ +---+ +---+
^ ^
| +---+ |
+-----> | 5 | <-----+
+---+
我会将实际的指针杂耍细节留给您来整理,但它们非常简单,可以说比常规链表队列更容易,因为要检查的边界条件更少。