这个主题已经在这里进行了教程
队列是一个列表,您只能在后面插入新元素并从前面删除元素。这可确保您入队的第一个元素也是您首先出列的元素。先到先得!
你为什么需要这个?那么,在许多算法中,您想要将对象添加到临时列表中,并在稍后将其从列表中拉出。通常您添加和移除这些对象的顺序很重要。
队列为您提供先进先出或先入先出的顺序。您首先插入的元素是第一个出来的元素。这只是公平的!(类似的数据结构,stack,是LIFO或后进先出。)
以下是一个排列数字的例子:
queue.enqueue(10)
队列现在[ 10 ]
。将下一个号码添加到队列中:
queue.enqueue(3)
队列现在[ 10, 3 ]
。再添加一个数字:
queue.enqueue(57)
队列现在[ 10, 3, 57 ]
。让我们将队列中的第一个元素从队列中拉出:
queue.dequeue()
这将返回,10
因为这是我们插入的第一个数字。队列现在[ 3, 57 ]
。每个人都向上移动了一个地方。
queue.dequeue()
这将返回3
,下一个出队返回57
,依此类推。如果队列为空,则返回出队队列,nil
或者在某些实现中出现错误消息。
注意:队列并不总是最好的选择。如果从列表中添加和删除项目的顺序并不重要,则可以使用stack而不是队列。堆栈更简单,更快捷。
代码
这里是Swift中一个简单的队列实现。它是一个数组中的包装器,用于排队,出队和查看最前面的项目:
public struct Queue {
fileprivate var array = [T]()
public var isEmpty: Bool {
return array.isEmpty
}
public var count: Int {
return array.count
}
public mutating func enqueue(_ element: T) {
array.append(element)
}
public mutating func dequeue() -> T? {
if isEmpty {
return nil
} else {
return array.removeFirst()
}
}
public var front: T? {
return array.first
}
}
这个队列运行良好,但不是最优的。
入队是**O(1) **操作,因为无论数组的大小如何,添加到数组的末尾总是会花费相同的时间量。
您可能想知道为什么将项目附加到数组中是** O(1)**还是常量操作。这是因为Swift中的数组在最后总是有一些空的空间。如果我们做到以下几点:
var queue = Queue()
queue.enqueue("Ada")
queue.enqueue("Steve")
queue.enqueue("Tim")
那么数组可能实际上看起来像这样:
[ "Ada", "Steve", "Tim", xxx, xxx, xxx ]
这里xxx
是保留但尚未填充的内存。向阵列中添加一个新元素会覆盖下一个未使用的点:
[ "Ada", "Steve", "Tim", "Grace", xxx, xxx ]
这是通过将内存从一个地方复制到另一个恒定时间操作的结果。
阵列末尾只有有限数量的未使用空间。当最后一个xxx
被使用,并且您想要添加另一个项目时,该阵列需要调整大小以腾出更多空间。
调整大小包括分配新内存并将所有现有数据复制到新阵列。这是一个相对较慢的** O(n)过程。因为它偶尔发生,对于追加一个新的元件的阵列的端部的时间仍然是O(1)上平均或O(1)** “ amortized”。
出列的故事是不同的。为了出队,我们从数组的开始处移除元素。这总是一个**O(n) **操作,因为它要求所有剩余的数组元素在内存中移位。
在我们的例子中,将第一个元素"Ada"
复制"Steve"
到以下位置"Ada"
,"Tim"
代替"Steve"
以及"Grace"
代替"Tim"
:
before [ "Ada", "Steve", "Tim", "Grace", xxx, xxx ]
/ / /
/ / /
/ / /
/ / /
after [ "Steve", "Tim", "Grace", xxx, xxx, xxx ]
将所有这些元素移动到内存中始终是** O(n) **操作。所以,通过我们简单的队列实现,排队是高效的,但是排队留下了一些需要的东西。
更高效的queue
为了提高出队效率,我们还可以保留一些额外的空闲空间,但这次是在阵列的前端。我们必须自己编写这个代码,因为内置的Swift数组不支持它。
主要思想是每当我们pop一个项目时,我们不会将数组的内容移到前面(慢),而是将项目在数组中的位置标记为空(快)。出列后"Ada"
,该数组为:
[ xxx, "Steve", "Tim", "Grace", xxx, xxx ]
出列后"Steve"
,该数组为:
[ xxx, xxx, "Tim", "Grace", xxx, xxx ]
由于前面的这些空点不会被重用,因此可以通过将剩余元素移到前面来定期修剪阵列:
[ "Tim", "Grace", xxx, xxx, xxx, xxx ]
这个修剪过程涉及移位O(n) 操作的存储器。因为这只会偶尔发生一次,所以平均出队是O(1)。
这里是你如何实现这个版本的Queue
:
public struct Queue {
fileprivate var array = [T?]()
fileprivate var head = 0
public var isEmpty: Bool {
return count == 0
}
public var count: Int {
return array.count - head
}
public mutating func enqueue(_ element: T) {
array.append(element)
}
public mutating func dequeue() -> T? {
guard head < array.count, let element = array[head] else { return nil }
array[head] = nil
head += 1
let percentage = Double(head)/Double(array.count)
if array.count > 50 && percentage > 0.25 {
array.removeFirst(head)
head = 0
}
return element
}
public var front: T? {
if isEmpty {
return nil
} else {
return array[head]
}
}
}
该数组现在存储类型的对象是T?
而不仅仅是T
因为我们需要将数组元素标记为空。该head
变量是最前面对象数组中的索引。
大部分新功能都在其中dequeue()
。当我们出列一个项目时,我们首先设置array[head]
为nil
从数组中移除该对象。然后,我们增加,head
因为下一个项目已成为前一个项目。
我们从这走开:
[ "Ada", "Steve", "Tim", "Grace", xxx, xxx ]
head
对此:
[ xxx, "Steve", "Tim", "Grace", xxx, xxx ]
head
就好像在超市里,结账通道里的人不会向收银机挪动,而是收银员在队列中移动。
如果我们永远不会移除前面的空白点,那么当我们入队和出队时,阵列将保持增长。要定期修剪阵列,我们执行以下操作:
let percentage = Double(head)/Double(array.count)
if array.count > 50 && percentage > 0.25 {
array.removeFirst(head)
head = 0
}
这将计算开始时空点的百分比,作为总阵列大小的比例。如果超过25%的阵列未被使用,我们会砍掉那个浪费的空间。但是,如果数组很小,我们不会一直调整它的大小,所以在我们尝试修剪它之前,数组中至少有50个元素。
注意:我只是简单地抽出这些数字 - 您可能需要根据您的应用在生产环境中的行为来调整它们。
要在** playground **上测试此操作,请执行以下操作:
var q = Queue()
q.array // [] empty array
q.enqueue("Ada")
q.enqueue("Steve")
q.enqueue("Tim")
q.array // [{Some "Ada"}, {Some "Steve"}, {Some "Tim"}]
q.count // 3
q.dequeue() // "Ada"
q.array // [nil, {Some "Steve"}, {Some "Tim"}]
q.count // 2
q.dequeue() // "Steve"
q.array // [nil, nil, {Some "Tim"}]
q.count // 1
q.enqueue("Grace")
q.array // [nil, nil, {Some "Tim"}, {Some "Grace"}]
q.count // 2
要测试修剪行为,请更换该行,
if array.count > 50 && percentage > 0.25 {
有:
if head > 2 {
现在,如果您将另一个对象出列,数组将如下所示:
q.dequeue() // "Tim"
q.array // [{Some "Grace"}]
q.count // 1
nil
前面的物体已被移除,阵列不再浪费空间。这个新版本Queue
并不比第一个版本复杂,但现在出列也是一个** O(1)**操作,仅仅是因为我们知道我们如何使用这个数组。
也可以看看
有很多方法可以创建一个队列。其他实现使用链表,循环缓冲区或堆。
该主题的变体是deque,可以在两端排队和出队的双端队列,以及优先级队列,排序队列中“最重要”的项目始终位于前端。
传送门