Chapters

Hide chapters

Data Structures & Algorithms in Swift

Third Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

8. Queues
Written by Vincent Ngo

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

We are all familiar with waiting in line. Whether you are in line to buy tickets to your favorite movie or waiting for a printer to print a file, these real-life scenarios mimic the queue data structure.

Queues use FIFO or first-in first-out ordering, meaning the first element that was added will always be the first to be removed. Queues are handy when you need to maintain the order of your elements to process later.

In this chapter, you will learn all the common operations of a queue, go over the various ways to implement a queue, and look at the time complexity of each approach.

Common operations

Let’s establish a protocol for queues:

public protocol Queue {
  associatedtype Element
  mutating func enqueue(_ element: Element) -> Bool
  mutating func dequeue() -> Element?
  var isEmpty: Bool { get }
  var peek: Element? { get }
}

The protocol describes the core operations for a queue:

  • enqueue: Insert an element at the back of the queue. Returns true if the operation was successful.
  • dequeue: Remove the element at the front of the queue and return it.
  • isEmpty: Check if the queue is empty.
  • peek: Return the element at the front of the queue without removing it.

Notice that the queue only cares about removal from the front and insertion at the back. You don’t really need to know what the contents are in between. If you did, you would probably just use an array.

Example of a queue

The easiest way to understand how a queue works is to see a working example. Imagine a group of people waiting in line for a movie ticket.

Array-based implementation

The Swift standard library comes with a core set of highly optimized, primitive data structures that you can use to build higher-level abstractions with. One of them is Array, a data structure that stores a contiguous, ordered list of elements. In this section, you will use an array to create a queue.

A simple Swift array can be used to model the queue.
E wazwhu Mxemp uxbux zib ya uher qo huwox wqa diieo.

public struct QueueArray<T>: Queue {
  private var array: [T] = []
  public init() {}
}

Leveraging arrays

Add the following code to QueueArray:

public var isEmpty: Bool {
  array.isEmpty // 1
}

public var peek: T? {
  array.first // 2
}

Enqueue

Adding an element to the back of the queue is easy. Just append an element to the array. Add the following:

public mutating func enqueue(_ element: T) -> Bool {
  array.append(element)
  return true
}

Dequeue

Removing an item from the front requires a bit more work. Add the following:

public mutating func dequeue() -> T? {
  isEmpty ? nil : array.removeFirst()
}

Debug and test

For debugging purposes, you’ll have your queue adopt the CustomStringConvertible protocol. Add the following at the bottom of the page:

extension QueueArray: CustomStringConvertible {
  public var description: String {
    String(describing: array)
  }
}
var queue = QueueArray<String>()
queue.enqueue("Ray")
queue.enqueue("Brian")
queue.enqueue("Eric")
queue
queue.dequeue()
queue
queue.peek

Strengths and weaknesses

Here is a summary of the algorithmic and storage complexity of the array-based queue implementation. Most of the operations are constant time except for dequeue(), which takes linear time. Storage space is also linear.

Doubly linked list implementation

Switch to the QueueLinkedList playground page. Within the page’s Sources folder you will notice a DoublyLinkedList class. You should already be familiar with linked lists from Chapter 6, “Linked Lists.” A doubly linked list is simply a linked list in which nodes also contain a reference to the previous node.

public class QueueLinkedList<T>: Queue {
  private var list = DoublyLinkedList<T>()
  public init() {}
}

Enqueue

To add an element to the back of the queue, simply add the following:

public func enqueue(_ element: T) -> Bool {
  list.append(element)
  return true
}

Dequeue

To remove an element from the queue, add the following:

public func dequeue() -> T? {
  guard !list.isEmpty, let element = list.first else {
    return nil
  }
  return list.remove(element)
}

Checking the state of a queue

Similar to the array implementation, you can implement peek and isEmpty using the properties of the DoublyLinkedList. Add the following:

public var peek: T? {
  list.first?.value
}

public var isEmpty: Bool {
  list.isEmpty
}

Debug and test

For debugging purposes, you can add the following at the bottom of the page:

extension QueueLinkedList: CustomStringConvertible {
  public var description: String {
    String(describing: list)
  }
}
var queue = QueueLinkedList<String>()
queue.enqueue("Ray")
queue.enqueue("Brian")
queue.enqueue("Eric")
queue
queue.dequeue()
queue
queue.peek

Strengths and weaknesses

Let’s summarize the algorithmic and storage complexity of the doubly linked-list-based queue implementation.

Ring buffer implementation

A ring buffer, also known as a circular buffer, is a fixed-size array. This data structure strategically wraps around to the beginning when there are no more items to remove at the end.

public struct QueueRingBuffer<T>: Queue {
  private var ringBuffer: RingBuffer<T>

  public init(count: Int) {
    ringBuffer = RingBuffer<T>(count: count)
  }

  public var isEmpty: Bool {
    ringBuffer.isEmpty
  }

  public var peek: T? {
    ringBuffer.first
  }
}

Enqueue

Next, add the method below:

public mutating func enqueue(_ element: T) -> Bool {
  ringBuffer.write(element)
}

Dequeue

Next add the following:

public mutating func dequeue() -> T? {
  ringBuffer.read()
}

Debug and test

To see your results in the playground, add the following:

extension QueueRingBuffer: CustomStringConvertible {
  public var description: String {
   String(describing: ringBuffer)
  }
}
var queue = QueueRingBuffer<String>(count: 10)
queue.enqueue("Ray")
queue.enqueue("Brian")
queue.enqueue("Eric")
queue
queue.dequeue()
queue
queue.peek

Strengths and weaknesses

How does the ring-buffer implementation compare? Let’s look at a summary of the algorithmic and storage complexity.

Double-stack implementation

Open the QueueStack playground page and start by adding a generic QueueStack as shown below:

public struct QueueStack<T> : Queue {
  private var leftStack: [T] = []
  private var rightStack: [T] = []
  public init() {}
}

Leveraging arrays

Implement the common features of a queue, starting with the following:

public var isEmpty: Bool {
  leftStack.isEmpty && rightStack.isEmpty
}
public var peek: T? {
  !leftStack.isEmpty ? leftStack.last : rightStack.first
}

Enqueue

Next add the method below:

public mutating func enqueue(_ element: T) -> Bool {
  rightStack.append(element)
  return true
}

Dequeue

Removing an item from a two-stack-based implementation of a queue is tricky. Add the following method:

public mutating func dequeue() -> T? {
  if leftStack.isEmpty { // 1
    leftStack = rightStack.reversed() // 2
    rightStack.removeAll() // 3
  }
  return leftStack.popLast() // 4
}

Debug and test

To see your results in the playground, add the following:

extension QueueStack: CustomStringConvertible {
  public var description: String {
    String(describing: leftStack.reversed() + rightStack)
  }
}
var queue = QueueStack<String>()
queue.enqueue("Ray")
queue.enqueue("Brian")
queue.enqueue("Eric")
queue
queue.dequeue()
queue
queue.peek

Strengths and weaknesses

Let’s look at a summary of the algorithmic and storage complexity of your two-stack-based implementation.

Key points

  • Queue takes a FIFO strategy, an element added first must also be removed first.
  • Enqueue inserts an element to the back of the queue.
  • Dequeue removes the element at the front of the queue.
  • Elements in an array are laid out in contiguous memory blocks, whereas elements in a linked list are more scattered with potential for cache misses.
  • Ring-buffer-queue based implementation is good for queues with a fixed size.
  • Compared to other data structures, leveraging two stacks improves the dequeue(_:) time complexity to amortized O(1) operation.
  • Double-stack implementation beats out Linked-list in terms of spacial locality.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now