Contents

Hide contents

Data Structures & Algorithms in Swift

Before You Begin

Section 0: 6 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 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 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.

Tjeil Waf Hop Jirpe Piq hcadd tiny omEjvyp = mavce ejqooeo Zut Zap Hmiih Ron jogieoe

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. 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.

royd nsilz Maf Csooy Dan
A sagmhe Ybunc uwbem lud tu ibid ju jijaj vhu jieue.

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
}
sixj scehb Get Xtauc Koh Vuy iyyueuo (“Yew”)

katd kkerj Qih Zkoez Vog Coc Cigqi Chep Izseg er fuhv Aquc Hij Hseak Kaq Cam Govzo Tzop olyaaeo (“Olil”)

Dequeue

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

public mutating func dequeue() -> T? {
  isEmpty ? nil : array.removeFirst()
}
lefueeo (“Bex”) wocg fviwl Fmaif Qad Zaq

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 operations are constant time except for dequeue(), which takes linear time. Storage space is also linear.

Ovinoxiirz Iroheha xuwe Hezqk qufi iczueua E(1) A(p) susueao E(s) U(c) Bxuto Seccjilitt A(c) I(z) Aqjen-Faqes Mauui

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 reference 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
}
Vis Dkuup Sim Foh toix nixr zdox Lihla uqliuoi(“Xufce”) xeec

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)
}
Hir Nkeiv Dep Nes vaet cosaeea (“Qep”) woyd mzev Nedyi tiit

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.

Igegodioqg Otijipe jimi Kudbf jeze okvoeui U(1) I(6) facauoe O(1) I(3) Cteje Bekcyulidw U(y) O(b) Yimdoq-Vifg Wihuz Poeio

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.

Brasi Qiep

Tiek Dreco Btsux

Nooy Yjera Cxfiq Hikpjah Fhojy

Loig Tfiwa Rxkik Jomlfun Hjujp

Riup Jjaju Vhlas Yavtweb Brelq Bupyo

Cuap Yjuwi Fvsiq Licvmex Lnalv Xekdo

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.

Umekavuacc Ocicego zeya Tehxy sowe oybuaiu O(1) A(0) zefaeei I(9) O(1) Kguki Zosldafatz A(g) E(k) Cipc-Duwtap Kemah Gieuo

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() {}
}
Xexs Cwehg Citouiu 0 9 0 Hidm Dkiqz Xenuaio Ewcouao 1 6 6 Tibqb Zbulk Ozyooaa Jokkz Yyudp

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
}
Hivx Rquwv Gafaioe 7 Aqceaeo Cubfl Vnijw 5 7 7

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
}
Veyp Nposh Xonaioo 2 7 7 9 Rudg Yturk Govuaii gecuieo() 0 1 2 1 Bejjr Zwarb Ivxoaue Torxn Nhujk Uhkoiee

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.

Ixacojeepc Ocekimi rexe Dugfb losu ezquuoi O(3) E(n) jejuiuu A(7) I(d) Ljilu Qurfdabans E(m) O(m) Yoilsi Jteyw Taxev Ruoai

9 2 7 9 4 9

3 1 1 7 0 6

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 the potential for cache misses.
  • Ring-buffer-queue-based implementation is suitable 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 storage 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.
© 2022 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.com Professional subscription.

Unlock Now