Queue介绍

这个主题已经在这里进行了教程

队列是一个列表,您只能在后面插入新元素并从前面删除元素。这可确保您入队的第一个元素也是您首先出列的元素。先到先得!

你为什么需要这个?那么,在许多算法中,您想要将对象添加到临时列表中,并在稍后将其从列表中拉出。通常您添加和移除这些对象的顺序很重要。

队列为您提供先进先出或先入先出的顺序。您首先插入的元素是第一个出来的元素。这只是公平的!(类似的数据结构,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,可以在两端排队和出队的双端队列,以及优先级队列,排序队列中“最重要”的项目始终位于前端。

传送门

你可能感兴趣的:(Queue介绍)