Swift Queue

描述

队列是一种只可以在队尾进行插入操作并且在队头进行删除操作的列表。这保证了你插入队列的第一个元素也是你出列的第一个元素。先进先出。

为什么需要队列呢?在许多算法中你想添加对象到一个临时的列表,随后再取出。通常添加和删除这些对象的顺序是重要的。

队列提供了先进先出(FIFO)的顺序。

下面是入队:

queue.enqueue(10)

现在队列是[10],添加下一个数字到队列中:

queue.enqueue(3)

现在队列是[10, 3],再添加一个数字:

queue.enqueue(57)

队列是[10, 3, 57],然后我们进行出列:

queue.dequeue()

这次返回10,因为这是第一个被插入的数据。队列现在时[3, 57],每个元素都向前移动了一位。

queue.dequeue()

这次是3,下次出列是57。如果队列为空,出列操作就是返回空或是返回一个错误信息。


代码实现

下面是一个Swift简单的实现队列。通过封装数组实现入列,出列和查看队头元素。

public struct Queue<T> {
    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<String>()
queue.enqueue("Ada")
queue.enqueue("Steve")
queue.enqueue("Tim")

数组实际是这样的:

[ "Ada", "Steve", "Tim", xxx, xxx, xxx ]

xxx是保留未填入的内存,添加一个新的元素就是覆写数组中一个未使用的位置。

这导致了拷贝内存从一个地方到另一个地方,耗时都是一样的。

在数组的末尾只有有限的一些未使用的空间。当最后一个xxx被使用后,如果你还要添加元素,数组就需要调整大小来获得更多的空间。

调整大小包括分配新内存并将所有现有数据复制到新数组。 这是一个相对较慢的O(n)的过程。 由于偶尔会发生这种情况,所以将新元素添加到数组末尾的时间大体仍然是O(1)或O(1)“摊销”。

出列的情况是不同的。出列,我们移除数组的第一个元素,总是O(n)的操作,因为它需要数组中的所有元素在内存中移位。

在我们的例子中,将“Ada”出列,然后拷贝“Steve”到“Aad”的位置,以此类推:

before   [ "Ada", "Steve", "Tim", "Grace", xxx, xxx ]
                   /       /      /
                  /       /      /
                 /       /      /
                /       /      /
 after   [ "Steve", "Tim", "Grace", xxx, xxx, xxx ]

在内存中移动这些元素是O(n)的操作。所以我们简单实现的队列,入列是效率好的,但是出列还需要提升。


更好的方案

为了使出列也有效率,我们也可以保留一些空余的空间在数组的头部。主要思想是,无论何时我们出列一个元素,我们不移动数组中元素,而是在出列的位置标记一个空。这样当出列“Ada”,数据时这样的:

[ xxx, "Steve", "Tim", "Grace", xxx, xxx ]

当出列“Steve”后,数组是:

[ xxx, xxx, "Tim", "Grace", xxx, xxx ]

因为数组前部的空位置是永远不会使用的,所有可以定期地将剩余的元素移动到前面(Trim)。

[ "Tim", "Grace", xxx, xxx, xxx, xxx ]

Trim这个操作是O(n),但是这个操作在一段时间内只会执行一次,所以总体上出列是O(1)。

我们可以重新定义新的队列:

public struct Queue<T> {
    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 precentage = Double(head) / Double(array.count)
        if array.count > 50 && precentage > 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,因为下一个元素成为了队首。

如果我们从不移除数组前部那些空的占位的元素,数组会越来越大,为了定期整理数组,做了以下操作:

let percentage = Double(head)/Double(array.count)
if array.count > 50 && percentage > 0.25 {
    array.removeFirst(head)
    head = 0
}   

[译]https://github.com/raywenderlich/swift-algorithm-club/tree/master/Queue

posted @ 2017-04-09 18:16  Lawerence  阅读(588)  评论(0编辑  收藏  举报