Swift Built-In Data Structures for Coding Interviews

May 29, 20269 min read
dsaalgorithmsinterview-prepswift
Swift Built-In Data Structures for Coding Interviews
TL;DR
  • Swift's standard library provides only three collection types (Array, Dictionary, Set); everything else requires a custom implementation or an external package.
  • removeFirst() is O(n) and silently turns BFS into O(n²); use a two-stack queue or head-pointer pattern to dequeue in O(1) amortized.
  • Dictionary subscripts return Optional; use dict[key, default: 0] for safe incrementing instead of force-unwrapping.
  • String has no integer subscript; convert to Array(s) at the start of any problem requiring random character access.
  • There is no built-in heap; a 30-line MinHeap<T: Comparable> struct covers every top-K and shortest-path problem in an interview.
  • Value semantics mean closures capture copies; use inout parameters when a function must mutate and propagate a collection back to the caller.

Swift has strong opinions about collections. It ships exactly three built-in data structures, no heap, no sorted map, no deque, and a String that actively refuses to behave like an array. Knowing what exists, what doesn't, and which workarounds to reach for before the 45-minute clock starts is most of the battle.

This guide is for engineers who already know Swift and want a precise coding interview reference for iOS and general engineering roles. No theory detours. Just the types, the traps, and the fixes.


Three Types. That's It.

Java ships 20-plus collection classes. Kotlin has them too. C++ has the entire STL. Swift's standard library gives you three, takes a bow, and calls it a day.

Use thisWhen you need
Array<T>Ordered list, stack, backtracking
Dictionary<K, V>Key-value lookup, frequency counting
Set<T>Membership checks, deduplication, set math
Array (two-stack wrapper)Queue or deque
Custom heap (30 lines)Priority queue
.keys.sorted()Sorted map iteration

That's the full picture. Everything else you write yourself or pull from the swift-collections package if the interview allows it. Spoiler: most don't.


Array: Actually Good Until You Use It as a Queue

Array<T> is a contiguous block of memory with value semantics and copy-on-write under the hood.

var nums = [3, 1, 4, 1, 5, 9] nums.append(2) // O(1) amortized nums.removeLast() // O(1) nums.removeFirst() // O(n), shifts every element left nums.insert(0, at: 0) // O(n) nums[2] // O(1) random access nums.count // O(1) nums.reserveCapacity(100) // avoids repeated reallocation

removeFirst() is O(n) and it will silently make your BFS quadratic. Every dequeue shifts all remaining elements left. If you're using a plain array as a queue, your "linear" traversal is actually O(n²) for large inputs. The interviewer will notice. You probably won't. Fix below.

Sorting is stable since Swift 5 (Timsort) and works in-place or returns a new array:

nums.sort() // ascending, in-place nums.sort { $0 > $1 } // descending, in-place let copy = nums.sorted() // ascending, new array let copy2 = nums.sorted { $0 > $1 }

A common mistake: calling nums.sorted() and discarding the result. It returns a new array. It does not touch nums. Swift will usually warn you, but under pressure these warnings have a way of disappearing.

For multi-key sorting, stability matters. Because Swift's sort is stable, you can sort by secondary key first, then primary key, and the relative order of ties is preserved.

See Dynamic Array Time Complexity for the full amortized analysis behind append.


Dictionary: Mind the Optional

Dictionary<K, V> is a hash map. Average O(1) for get, set, and delete. Keys must be Hashable.

var freq: [Character: Int] = [:] freq["a"] // Optional<Int>, nil if absent freq["a"]! // crashes if key absent freq["a", default: 0] += 1 // safe increment, the idiom to know freq.updateValue(5, forKey: "a") // returns old value as Optional freq.removeValue(forKey: "a") // returns removed value as Optional freq.keys // Collection<Character>, unordered freq.count // O(1)

The subscript returns Optional<Value>, not Value. Writing freq[c] += 1 does not compile when freq[c] might be nil. The [key, default: 0] subscript is the idiomatic fix. It returns the existing value if present or the default if not, then writes the result back. This is the pattern you'll use in nearly every frequency-counting problem.

Iteration order is undefined. If you need to process keys in sorted order:

for key in freq.keys.sorted() { print(key, freq[key]!) }

Custom struct keys need to conform to Hashable. The compiler synthesizes conformance automatically if all stored properties are themselves Hashable, which covers most cases.

For the internals of why O(1) lookup sometimes isn't, see Hash Map Time Complexity.


Set: O(1) Where Array Would Be O(n)

var visited: Set<Int> = [] visited.insert(5) // O(1) visited.contains(5) // O(1) visited.remove(5) // O(1), returns discarded value visited.count // O(1)

Checking array.contains(_:) is O(n). Checking set.contains(_:) is O(1). Any time you're tracking "have I seen this?" during a traversal, reach for Set over Array without thinking.

Set operations are first-class:

let a: Set = [1, 2, 3, 4] let b: Set = [3, 4, 5, 6] a.union(b) // [1, 2, 3, 4, 5, 6] a.intersection(b) // [3, 4] a.subtracting(b) // [1, 2] a.symmetricDifference(b) // [1, 2, 5, 6]

The form variants mutate in place (a.formUnion(b) modifies a). The plain variants return new sets. Either works in an interview. The plain variants are usually clearer to read.


String: The One That Won't Index

Swift's String is Unicode-correct. Each character is a Character, which can span multiple code units. Because character widths vary, Swift cannot jump to the nth character in O(1). It has to scan from a known position.

String does not support integer subscripting. s[2] does not compile.

This is not a bug. It is a philosophical stance. Apple decided that pretending strings are simple arrays would be dishonest. The Unicode Consortium agrees. Your interviewer is about to find out.

let s = "hello" // s[2] -- compile error s.first // Optional<Character> s.last // Optional<Character>

Indexing with String.Index works but is verbose and still O(n) to compute from scratch each time. For any problem requiring repeated random access, pay the one-time O(n) cost upfront:

var chars = Array(s) // [Character], O(1) access thereafter chars[2] // O(1) let result = String(chars) // convert back

This is the standard interview pattern. Convert at the start, work on the array, convert back at the end. The single O(n) conversion is worth it for everything that follows.

Character literals compare by Unicode value. Lowercase ASCII characters compare correctly with < and >. Mixing uppercase and lowercase without normalization produces unexpected orderings. For most LeetCode-style problems this is fine. Know it exists.


Data Structures Swift Doesn't Ship

Priority Queue

The standard library has no heap. You'll write one or note the gap. No pressure. You're just being timed. A minimal min-heap takes about 30 lines:

struct MinHeap<T: Comparable> { private var data: [T] = [] var isEmpty: Bool { data.isEmpty } var peek: T? { data.first } mutating func push(_ val: T) { data.append(val) siftUp(data.count - 1) } mutating func pop() -> T? { guard !data.isEmpty else { return nil } data.swapAt(0, data.count - 1) let val = data.removeLast() if !data.isEmpty { siftDown(0) } return val } private mutating func siftUp(_ i: Int) { var i = i while i > 0 { let parent = (i - 1) / 2 if data[parent] > data[i] { data.swapAt(parent, i); i = parent } else { break } } } private mutating func siftDown(_ i: Int) { var i = i while true { let left = 2 * i + 1, right = 2 * i + 2 var smallest = i if left < data.count && data[left] < data[smallest] { smallest = left } if right < data.count && data[right] < data[smallest] { smallest = right } if smallest == i { break } data.swapAt(i, smallest); i = smallest } } }

For a max-heap, flip the comparisons. If the interview permits external packages, the swift-collections package ships Heap<T: Comparable> with insert, min, max, removeMin, and removeMax, all O(log n). Ask the interviewer. Most will say yes.

For the underlying structure, see The Heap Data Structure.

Queue (FIFO)

Array plus removeFirst() looks right. It is wrong. Two clean alternatives:

Two-stack queue (O(1) amortized, no wasted memory):

struct Queue<T> { private var inbox: [T] = [] private var outbox: [T] = [] mutating func enqueue(_ item: T) { inbox.append(item) } mutating func dequeue() -> T? { if outbox.isEmpty { outbox = inbox.reversed(); inbox.removeAll() } return outbox.popLast() } var isEmpty: Bool { inbox.isEmpty && outbox.isEmpty } var front: T? { outbox.last ?? inbox.first } }

Elements go into inbox and come out of outbox. When outbox empties, you flip the whole inbox over into it. Each element crosses that bridge exactly once, making the average O(1) per operation.

Two-stack queue: inbox grows on the right, outbox serves from the back after a single reversal

New elements pile into inbox. When outbox runs dry, the whole inbox flips over. Each element makes the crossing exactly once.

Head-pointer (simpler, wastes memory, fine for interviews):

var queue = [Int]() var head = 0 // enqueue: queue.append(5) // dequeue: let front = queue[head]; head += 1

Sorted Map

There is no TreeMap in Swift. Most interview problems don't need one. For sorted iteration, sort the keys array once:

for key in freq.keys.sorted() { ... } // O(k log k) where k = key count

For range queries across many insertions, a sorted array with manual binary search works. It is O(n) for insertions and O(log n) for searches. For interview purposes, note the limitation and explain why a balanced BST would be needed in production.


Five Traps That Will Actually Cost You

1. Optional subscript crash. dict["key"]! crashes on a missing key. Force-unwrap only when you've already verified the key exists. Use dict["key", default: 0] for counters and if let val = dict["key"] for conditional access.

2. String doesn't index. s[i] is a compile error. The moment a problem involves random access into a string, put var chars = Array(s) at the top. Do it before you need it. Your future self, twelve minutes into the problem, will thank you.

3. removeFirst() is O(n). Using a plain array as a queue turns O(n) BFS into O(n²) BFS. Wrap a two-stack queue or use a head pointer.

4. sorted() returns a new array. Calling arr.sorted() with no assignment leaves arr untouched. The in-place version is arr.sort(). Swift usually warns you, but warnings are easy to miss when you're in the zone and 20 minutes behind schedule.

5. Value semantics bite with closures. Capturing an Array or Dictionary in a closure captures a copy. Mutations inside don't propagate back to the caller. Use inout parameters when a function needs to modify a collection and have that change visible outside.


If you want to practice explaining these choices under interview conditions, SpaceComplexity runs voice-based mock interviews where picking Set over Array is only half the work. You also have to defend it out loud in real time.

For a broader look at Swift's idioms and patterns beyond just data structures, check out Swift for Coding Interviews.


Further Reading