队列
队列的本质是一个数组,但只能从队尾添加元素,从队首移除元素。这保证了第一个入队的元素总是第一个出队。先到先得!
有什么使用场景呢? 比如我们排队买票,总要设置一个公平的规则,让先来的人先得到票.在很多算法的实现中,你可能需要将某些对象放到一个临时的列表中,之后再将其取出。通常加入和取出元素的顺序非常重要。
队列和栈有些不一样,队列可以保证元素存入和取出的顺序是先进先出(first-in first-out, FIFO)的,第一个入队的元素总是第一个出队,公平合理!栈,它是一个后进先出(last-in, first-out, LIFO)的结构。
下面用swift来实现一个基本的队列.
struct Queue<T> {
var array = [T]()
// 入队列
mutating func enqueue(_ element: T) {
array.append(element)
}
// 出队列
mutating func dequeue() -> T?{
return array.isEmpty ? nil : array.removeFirst()
}
// 获取队列首元素
func peek() -> T? {
return array.first
}
}
上面实现的队列只是可以正常工作,入队操作的时间复杂度为 O(1),因为在数组的尾部添加元素只需要固定的时间,跟数组的大小无关.因为在 Swift 的内部实现中,数组的尾部总是有一些预设的空间可供使用.举个例子:
var queue = Queue<String>()
queue.enqueue("a")
queue.enqueue("b")
queue.enqueue("c")
我们向队列插入若干条数据,实际上数组的空间可能是下面这个样子的:
["a","b","c","xxx"]
xxx代表已经申请,但是还未使用的内存,再尾部插入一个元素就会使用到这个未被使用的内存.所以,简单的拷贝内存的工作,只需要固定的常量时间.
当然,数组尾部的未使用内存的大小是有限的,如果最后一块未使用内存也被占用的时候,再添加元素会使得数组重新调整大小来获取更多的空间,这时候,重新调整的过程包括申请新的内存,将已有数据迁移到新内存中,这个操作的时间复杂度是 O(n).实际开发过程中,这种情况并不常见,所以,这个操作的时间复杂度依然是 O(1) 的,或者说是近似 O(1) 的.
但是出队列的操作是将数组头部元素移除,以为这回导致内存移位的操作,所以这个操作时间复杂度是O(n).
一个更加高效的队列
为了让队列的出列更加高效,我们可以使用入队类似的技巧,也就是在队首保留一些额外的内存空间,因为swift并没有提供这种机制,所以我们要手动修改一下:
struct FastQueue<T> {
var array = [T?]()
private var head = 0
mutating func enqueue(_ element: T) {
array.append(element)
}
mutating func dequeue() -> T? {
guard head < array.count , let element = array[head] else {
return nil
}
array[head] = nil
head += 1
return element
}
func peek() -> T? {
return array.isEmpty ? nil : array[head]
}
}
上述代码简单的实现了一个出队列不操作内存移位,而用空值来进行头部的占位.
针对 dequeue()
函数,在将队首元素出队时,我们首先将 array[head]
设置为 nil
来将这个元素从数组中移除。然后将 head
的值加一,使得下一个元素变成新的队首。
但是,如果我们从不移除队首的空位,随着不断地入队和出队,队列所占空间将不断增长。为了周期性地清理无用空间,我们将上面的代码再优化一下:
mutating func dequeue() -> T? {
guard head < array.count , let element = array[head] else {
return nil
}
array[head] = nil
head += 1
let capacity = 50 //数组的容量
let persect = 0.2 //阈值
// 数组容量过大,或超过头部所占空间的阈值 , 使用原始出列方式以节约空间
if array.count > capacity , Double(head) / Double(array.count) > persect {
array.removeFirst()
head = 0
}
return element
}
当队列的数组超过我们设定的阈值,我们将使用原始的方式移除首部元素,以重新排列内存,因为移除的操作并不会很频繁,所以现在出队操作的复杂度已经从当初的 O(n) 变为了现在的 O(1).