iosNudge

Collections

Chapter 2 of 6 2 min read

Array

var nums = [5, 2, 8]
nums.append(1)                 // [5, 2, 8, 1]      O(1) on average
nums.insert(9, at: 0)          // [9, 5, 2, 8, 1]   O(n): everything shifts
let last = nums.removeLast()   // 1                 O(1), crashes if empty
let maybe = nums.popLast()     // Optional(8)       O(1), nil if empty
let first = nums.removeFirst() // 9                 O(n): everything shifts
nums.swapAt(0, 1)              // [2, 5]
print(nums, last, maybe!, first)

Ways to create an array:

let zeros = Array(repeating: 0, count: 5)          // [0, 0, 0, 0, 0]
let oneToFive = Array(1...5)                       // [1, 2, 3, 4, 5]
let evens = Array(stride(from: 0, to: 10, by: 2))  // [0, 2, 4, 6, 8]
print(zeros, oneToFive, evens)

Searching:

let a = [3, 1, 4, 1, 5]
print(a.contains(4))                  // true, O(n)
print(a.firstIndex(of: 1)!)           // 1
print(a.lastIndex(of: 1)!)            // 3
print(a.firstIndex { $0 > 3 }!)       // 2
print(a.min()!, a.max()!)             // 1 5
print(a.count(where: { $0 == 1 }))    // 2
print(a.allSatisfy { $0 > 0 })        // true

The slice trap

A slice keeps the indices of the original array. It does not start at 0.

let arr = [10, 20, 30, 40]
let slice = arr[1...2]         // [20, 30]
print(slice.startIndex)        // 1, not 0
// slice[0]                    // crash: index out of range
let copy = Array(slice)        // [20, 30], starts at 0 again
print(copy[0])                 // 20

2D arrays

let rows = 3
let cols = 4
var grid = Array(repeating: Array(repeating: 0, count: cols), count: rows)
grid[1][2] = 7

for r in 0..<rows {
    for c in 0..<cols where grid[r][c] == 7 {
        print("found 7 at", r, c)   // found 7 at 1 2
    }
}

Arrays are values in Swift. So the rows above are independent copies. (In Python, [[0] * n] * m shares one row. Swift does not have this bug.)

Grid neighbors, a pattern you will use in many problems:

func neighbors(_ r: Int, _ c: Int, rows: Int, cols: Int) -> [(Int, Int)] {
    let directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]   // right, down, left, up
    var result: [(Int, Int)] = []
    for (dr, dc) in directions {
        let nr = r + dr
        let nc = c + dc
        if nr >= 0 && nr < rows && nc >= 0 && nc < cols {
            result.append((nr, nc))
        }
    }
    return result
}

print(neighbors(0, 0, rows: 3, cols: 3))   // [(0, 1), (1, 0)]

Dictionary

var counts: [String: Int] = [:]
for word in ["a", "b", "a"] {
    counts[word, default: 0] += 1      // no need to check if the key exists
}
print(counts["a"]!)                    // 2
print(counts["z"] as Any)              // nil
counts["b"] = nil                      // removes "b"

if let value = counts["a"] {
    print(value)                       // 2
}

The order of a dictionary is random, and it can change between runs. Sort when order matters:

let scores = ["bo": 3, "al": 5, "cy": 1]
for (name, score) in scores.sorted(by: { $0.key < $1.key }) {
    print(name, score)                 // al 5, bo 3, cy 1
}
let best = scores.max { $0.value < $1.value }!.key
print(best)                            // al

Useful initializers:

let words = ["eat", "tea", "tan", "ate", "nat"]
let groups = Dictionary(grouping: words) { String($0.sorted()) }
// ["aet": ["eat", "tea", "ate"], "ant": ["tan", "nat"]]

let pairs = Dictionary(uniqueKeysWithValues: [("a", 1), ("b", 2)])   // crashes on duplicate keys
let merged = Dictionary([("a", 1), ("a", 5)], uniquingKeysWith: +)  // ["a": 6]
let doubled = pairs.mapValues { $0 * 2 }                            // ["a": 2, "b": 4]
print(groups.count, merged, doubled.count)

Set

var seen: Set<Int> = []
let result = seen.insert(3)
print(result.inserted)            // true
print(seen.insert(3).inserted)    // false: it was already there
print(seen.contains(3))           // true, O(1) on average
seen.remove(3)

let a: Set = [1, 2, 3]
let b: Set = [2, 3, 4]
print(a.union(b).sorted())                 // [1, 2, 3, 4]
print(a.intersection(b).sorted())          // [2, 3]
print(a.subtracting(b).sorted())           // [1]
print(a.symmetricDifference(b).sorted())   // [1, 4]
print(a.isSubset(of: b))                   // false

insert returns a tuple. Checking .inserted lets you "add and check" in one step.

Tuples

Tuples group a few values without a new type. They are great for returning two values.

func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
    guard let first = nums.first else { return nil }
    return nums.reduce((first, first)) { (min($0.0, $1), max($0.1, $1)) }
}

if let range = minMax([4, 9, 1]) {
    print(range.min, range.max)        // 1 9
}
let (quotient, remainder) = (17 / 5, 17 % 5)   // (3, 2)
print(quotient, remainder)

Tuples are not Hashable. You cannot put them in a Set or use them as dictionary keys.

// var visited = Set<(Int, Int)>()     // error: (Int, Int) is not Hashable

struct Cell: Hashable {
    let r: Int
    let c: Int
}
var visited: Set<Cell> = [Cell(r: 0, c: 0)]
visited.insert(Cell(r: 1, c: 2))

// Or turn the pair into one Int: key = r * cols + c
let cols = 10
let key = 2 * cols + 3                 // row 2, col 3 -> 23
print(visited.count, key)

Tuples can be compared with < (up to 6 items). The first item decides, then the next:

let items = [(2, "b"), (1, "z"), (2, "a")]
let ordered = items.sorted(by: <)
print(ordered)                         // [(1, "z"), (2, "a"), (2, "b")]

Sorting with closures

var scores = [3, 1, 2]
scores.sort()                          // in place: [1, 2, 3]
let descending = scores.sorted(by: >)  // a new array: [3, 2, 1]
print(scores, descending)

struct Person {
    let name: String
    let age: Int
}

let people = [
    Person(name: "Ana", age: 30),
    Person(name: "Bo", age: 25),
    Person(name: "Cy", age: 30),
]

// Age high to low; for the same age, name A to Z
let sortedPeople = people.sorted {
    if $0.age != $1.age { return $0.age > $1.age }
    return $0.name < $1.name
}
print(sortedPeople.map(\.name))        // ["Ana", "Cy", "Bo"]

let intervals = [[5, 6], [1, 3], [2, 4]]
print(intervals.sorted { $0[0] < $1[0] })   // [[1, 3], [2, 4], [5, 6]]

Sorting is O(n log n). If the order of equal items matters, add a tie-breaker, like the name above.

map, filter, reduce

let nums = [1, 2, 3, 4]
print(nums.map { $0 * $0 })                 // [1, 4, 9, 16]
print(nums.filter { $0 % 2 == 0 })          // [2, 4]
print(nums.reduce(0, +))                    // 10
print(["1", "x", "3"].compactMap { Int($0) })   // [1, 3]: nil values are dropped
print([[1, 2], [3]].flatMap { $0 })         // [1, 2, 3]

Use reduce(into:) to build a collection. It changes one value in place. Plain reduce would copy the dictionary on every step.

let parity = [1, 2, 3, 4, 5].reduce(into: [String: Int]()) { counts, n in
    counts[n % 2 == 0 ? "even" : "odd", default: 0] += 1
}
print(parity["odd"]!, parity["even"]!)      // 3 2

zip and enumerated

zip walks two sequences together. It stops at the shorter one.

let names = ["a", "b", "c"]
let ages = [1, 2]
for (name, age) in zip(names, ages) {
    print(name, age)                        // a 1, then b 2
}

let values = [1, 3, 6, 10]
let gaps = zip(values, values.dropFirst()).map { $1 - $0 }
print(gaps)                                 // [2, 3, 4]: difference between neighbors

enumerated gives you a counter and the value:

for (index, value) in ["x", "y"].enumerated() {
    print(index, value)                     // 0 x, then 1 y
}

The counter always starts at 0. On a slice, it is not the real index. Use indices or zip(slice.indices, slice) there.

Loops and ranges

for i in stride(from: 10, through: 0, by: -3) { print(i, terminator: " ") }   // 10 7 4 1
print()
for i in (0..<4).reversed() { print(i, terminator: " ") }                       // 3 2 1 0
print()

let n = 0
for i in 0..<n { print(i) }       // fine: runs zero times
// for i in 0...n - 1 { }         // crash when n == 0: the range would be 0...-1

Prefer 0..<n over 0...n-1. The closed range crashes when n is 0.

Numbers

print(Int.max)                   // 9223372036854775807
// Int.max + 1                   // crash: Swift traps on overflow
print(Int.max &+ 1 == Int.min)   // true: &+ wraps around on purpose
print(-7 / 2)                    // -3: rounds toward zero
print(-7 % 3)                    // -1: the sign follows the left side
print(((-7 % 3) + 3) % 3)        // 2: a modulo that is never negative
print(abs(-5), min(3, 7), max(3, 7))   // 5 3 7
print(Double(7) / 2)             // 3.5

Big-O cheat sheet

OperationArraySetDictionary
Read by index or keyO(1)-O(1) average
containsO(n)O(1) averageO(1) average (keys)
AddO(1) at the end, O(n) at the frontO(1) averageO(1) average
RemoveO(1) at the end, O(n) at the frontO(1) averageO(1) average
SortO(n log n)--

Rule of thumb: if you call contains or firstIndex inside a loop, you probably want a Set or a Dictionary instead.

Interview tips

  • Say the cost of what you call: "contains on an array is O(n), so I'll use a Set."
  • Count with dict[key, default: 0] += 1. It is short and needs no optional unwrapping.
  • Dictionary and set order is random. Sort before you print or return ordered results.
  • first, last, min() and max() return optionals. Think about the empty input before you write !.
  • For a "visited" set of grid cells, use a small Hashable struct or r * cols + c, not a tuple.
  • If you know the final size, reserve it once:
var squares: [Int] = []
squares.reserveCapacity(1000)   // one memory allocation instead of many
for i in 0..<1000 {
    squares.append(i * i)
}
print(squares.count)            // 1000