数据结构学习-队列(Queue)

1,287 阅读6分钟

接口定义

队列是一种先进先出(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 复杂度:

操作平均情况最差情况
enqueueO(1)O(n)
dequeueO(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)
    }
}

复杂度分析

可以看到实现代码都很简单。下面是复杂度的分析:

操作平均情况最差情况
enqueueO(1)O(1)
dequeueO(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()
    }
}

复杂度分析

下面是复杂度的分析:

操作平均情况最差情况
enqueueO(1)O(1)
dequeueO(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()
    }
}

复杂度分析

下面是复杂度的分析:

操作平均情况最差情况
enqueueO(1)O(1)
dequeueO(1)O(1)
空间复杂度O(n)O(n)

enqueue 操作添加到右边的数组末尾,不用移动数组已有的元素, O(1) 操作。

dequeue 操作中,左边数组的数据是右边数据的倒序,所以左边数组的 leftArray.popLast() 不用移动已有的数组元素位置,返回的也是最新 enqueue 的数据。

对比单数组实现的队列,双数组实现的 dequeue 操作为 O(1); 对比链表实现的的队列,双数组实现的队列内存区域是连续的,系统的内存管理更加的高效;对比循环缓冲区实现的队列,双数组实现的队列不用在初始化的时候固定队列的元素个数。