接口定义
队列是一种先进先出(first in first out FIFO)的线性数据结构。主要接口设计是头部移除数据,尾部添加数据。下面是大致需要实现的接口:
protocol Queue {
associatedtype Element
var isEmpty: Bool { get }
var peek: Element? { get }
mutating func enqueue(_ element: Element) -> Bool
mutating func dequeue() -> Element?
}
可以看到需要实现的接口其实和之前实现的 栈数据结构 是非常类似的。
由于不同存储数据的方式实现相同方法的复杂度有可能是不一样的,下面会使用4种不同方式实现队列数据结构对比使用。
使用数组实现队列
基本代码
struct QueueArray<T> {
var array: [T] = [] // 数据存储使用一个数组
}
实现协议
extension QueueArray: Queue {
var isEmpty: Bool {
array.isEmpty
}
var peek: T? {
array.first
}
mutating func enqueue(_ element: T) -> Bool {
array.append(element)
return true // 返回 true,表示数据添加成功(其实可以看情况是否有返回值)
}
mutating func dequeue() -> T? {
array.removeFirst()
}
}
复杂度分析
使用 Swift 内置的数组数据类型实现队列很简单,没什么好说的。下面主要分析一下使用数组实现的队列的 enqueue 和 dequeue 复杂度:
| 操作 | 平均情况 | 最差情况 |
|---|---|---|
| enqueue | O(1) | O(n) |
| dequeue | O(n) | O(n) |
| 空间复杂度 | O(n) | O(n) |
enqueue 总体复杂度为 O(1) 是因为数据直接添加到数组末尾,最差情况为 O(n) 是因为如果当前数组内存已经填满则需要扩容新内存,再把之前的数据都拷贝到新内存去。但是扩容操作执行的次数相对还是少数,所以 enqueue 的时间复杂度为 O(1)
dequeue 直接移除数组的头部元素,这会导致所有头部元素后面的元素都前向移动1位,所以 dequeue 的时间复杂度为 O(n)。
空间复杂度是需要分配的内存空间为 n。
下面是测试代码:
func queueTest() {
var queue = QueueArray<String>()
queue.enqueue("Tom")
queue.enqueue("Jerry")
queue.enqueue("Jack")
print("list: \(queue)")
queue.dequeue()
print("after dequeue, list: \(queue)")
print("peek list = \(String(describing: queue.peek))")
}
输出:
list: QueueArray<String>(array: ["Tom", "Jerry", "Jack"])
after dequeue, list: QueueArray<String>(array: ["Jerry", "Jack"])
peek list = Optional("Jerry")
使用双向链表实现队列
在 链表数据结构 中实现的是一个单向链表,它的结构是这样的:
而双向链表是这样的:
单向链表中节点只有下一个节点的引用,而双向链表每个节点都有下一个节点的引用和上一个节点的引用。
由于目前需要实现的是队列数据结构,下面就直接贴出双向链表的代码实现:
public class Node<T> {
public var value: T
public var next: Node<T>? // 下一个节点的引用
public var previous: Node<T>? // 上一个节点的引用
public init(value: T) {
self.value = value
}
}
extension Node: CustomStringConvertible {
public var description: String {
String(describing: value)
}
}
public class DoublyLinkedList<T> {
private var head: Node<T>?
private var tail: Node<T>?
public init() { }
public var isEmpty: Bool {
head == nil
}
public var first: Node<T>? {
head
}
public func append(_ value: T) {
let newNode = Node(value: value)
guard let tailNode = tail else {
head = newNode
tail = newNode
return
}
newNode.previous = tailNode
tailNode.next = newNode
tail = newNode
}
public func remove(_ node: Node<T>) -> T {
let prev = node.previous
let next = node.next
if let prev = prev {
prev.next = next
} else {
head = next
}
next?.previous = prev
if next == nil {
tail = prev
}
node.previous = nil
node.next = nil
return node.value
}
}
extension DoublyLinkedList: CustomStringConvertible {
public var description: String {
var string = ""
var current = head
while let node = current {
string.append("\(node.value) -> ")
current = node.next
}
return string + "end"
}
}
public class LinkedListIterator<T>: IteratorProtocol {
private var current: Node<T>?
init(node: Node<T>?) {
current = node
}
public func next() -> Node<T>? {
defer { current = current?.next }
return current
}
}
extension DoublyLinkedList: Sequence {
public func makeIterator() -> LinkedListIterator<T> {
LinkedListIterator(node: head)
}
}
基本代码
以双向链表为底层数据存储的基本代码:
struct QueueLinkedList<T> {
var list = DoublyLinkedList<T>() // 数据存储使用双向链表
}
实现协议
extension QueueLinkedList: Queue {
var isEmpty: Bool {
list.isEmpty
}
var peek: T? {
list.first?.value
}
mutating func enqueue(_ element: T) -> Bool {
list.append(element)
return true
}
mutating func dequeue() -> T? {
guard let first = list.first else {
return nil
}
return list.remove(first)
}
}
复杂度分析
可以看到实现代码都很简单。下面是复杂度的分析:
| 操作 | 平均情况 | 最差情况 |
|---|---|---|
| enqueue | O(1) | O(1) |
| dequeue | O(1) | O(1) |
| 空间复杂度 | O(n) | O(n) |
由于双向链表不需要动态一定的倍数扩容,删除和添加节点的时候也不需要把所有节点进行移动,所以 enqueue 和 dequeue 操作的时间复杂度为 O(1), 空间复杂度为 O(n)。
但是使用双向链表每次 enqueue 操作都需要动态分配当前节点的内存,而数组是在已经分配好的内存添加内容。
(数组内存连续分配便于管理,链表不一定连续分配)
使用循环缓冲区(Ring Buffer)实现队列
循环缓冲区的初始化内存大小是固定的。它大致的书数据结构图如下:
-
读指针指向当前要读的位置;
-
写指针指向下一个要写入的位置;
如果读指针和写指针指向相同位置,那么数据为空。如果写入指针指向到内存末尾了,那么再次写入的时候写指针会指向数据开始的位置形成一个循环(读操作也是循环读取的)。
比如写操作从这样:
到这样:
所以如果当前写入位置有数据的话,写入操作可能失败。
下面直接把循环缓冲区贴过来:
public struct RingBuffer<T> {
private var array: [T?]
private var readIndex = 0
private var writeIndex = 0
public init(count: Int) {
array = Array<T?>(repeating: nil, count: count)
}
public var first: T? {
array[readIndex]
}
public mutating func write(_ element: T) -> Bool {
if !isFull {
array[writeIndex % array.count] = element
writeIndex += 1
return true
} else {
return false
}
}
public mutating func read() -> T? {
if !isEmpty {
let element = array[readIndex % array.count]
readIndex += 1
return element
} else {
return nil
}
}
private var availableSpaceForReading: Int {
writeIndex - readIndex
}
public var isEmpty: Bool {
availableSpaceForReading == 0
}
private var availableSpaceForWriting: Int {
array.count - availableSpaceForReading
}
public var isFull: Bool {
availableSpaceForWriting == 0
}
}
extension RingBuffer: CustomStringConvertible {
public var description: String {
let values = (0..<availableSpaceForReading).map {
String(describing: array[($0 + readIndex) % array.count]!)
}
return "[" + values.joined(separator: ", ") + "]"
}
}
基本代码
以循环缓冲区为底层数据存储的基本代码:
struct QueueRingBuffer<T> {
private var ringBuffer: RingBuffer<T> // 数据存储使用循环缓冲区
// 初始化的时候需要确定size
init(count: Int) {
ringBuffer = RingBuffer(count: count)
}
}
实现协议
extension QueueRingBuffer: Queue {
var isEmpty: Bool {
ringBuffer.isEmpty
}
var peek: T? {
ringBuffer.first
}
mutating func enqueue(_ element: T) -> Bool {
ringBuffer.write(element)
}
mutating func dequeue() -> T? {
ringBuffer.read()
}
}
复杂度分析
下面是复杂度的分析:
| 操作 | 平均情况 | 最差情况 |
|---|---|---|
| enqueue | O(1) | O(1) |
| dequeue | O(1) | O(1) |
| 空间复杂度 | O(n) | O(n) |
enqueue 和 dequeue 操作的时间复杂度为 O(1), 空间复杂度为 O(n)。而且由于初始化的时候就确定了元素的数量,所以底层的数组结构也不需要动态扩容,但是有一个缺陷就是写入操作可能失败。
使用两个数组实现队列
使用两个数组实现队列功能的重点是一个数组进行 enqueue, 另外一个数组进行 dequeue。
基本代码
struct QueueDoubleArray<T> {
private var leftArray: Array<T> = [] // 左边数组进行 dequeue
private var rightArray: Array<T> = [] // 右边数组进行 enqueue
}
实现协议
extension QueueDoubleArray: Queue {
var isEmpty: Bool {
leftArray.isEmpty && rightArray.isEmpty
}
var peek: T? {
leftArray.isEmpty ? rightArray.last : leftArray.last
}
mutating func enqueue(_ element: T) -> Bool {
rightArray.append(element)
return true
}
mutating func dequeue() -> T? {
if leftArray.isEmpty {
// 虽然这个操作是 O(n),但是这个操作执行的次数相对较少,所以总体时间复杂度为 O(1)
leftArray = rightArray.reversed() // 右边enqueue数组元素 反序添加到 左边dequeue数组
rightArray.removeAll() // 清空右边enqueue数组
}
return leftArray.popLast()
}
}
复杂度分析
下面是复杂度的分析:
| 操作 | 平均情况 | 最差情况 |
|---|---|---|
| enqueue | O(1) | O(1) |
| dequeue | O(1) | O(1) |
| 空间复杂度 | O(n) | O(n) |
enqueue 操作添加到右边的数组末尾,不用移动数组已有的元素, O(1) 操作。
dequeue 操作中,左边数组的数据是右边数据的倒序,所以左边数组的 leftArray.popLast() 不用移动已有的数组元素位置,返回的也是最新 enqueue 的数据。
对比单数组实现的队列,双数组实现的 dequeue 操作为 O(1); 对比链表实现的的队列,双数组实现的队列内存区域是连续的,系统的内存管理更加的高效;对比循环缓冲区实现的队列,双数组实现的队列不用在初始化的时候固定队列的元素个数。