Choose an answer, then open Show answer below to check it.
Show answer
✓Answer: B b
Correct. After a is read, b is the least recently used.
Why not the others?
✕A a. Reading a makes it the most recently used.
✕C c. The new item is the most recent.
✕D Nothing, because the cache grows. Capacity is 2, so one item must go.
30-second answer
Combine a dictionary from key to node with a doubly linked list ordered by recency, so lookups, inserts and evictions are O(1). On a hit, move the node to the front; on insert past capacity, remove the tail. Protect the state with a Mutex (iOS 18) or an actor, depending on whether callers are synchronous or async.
Practise saying it · 60 s. No recording, just a timer
Explain your LRU design.
Say your answer out loud, then check which of these points you covered:
Dictionary + linked list
O(1) operations
Reads mutate
Mutex or actor
Deep dive
Make back pointers weak (or manage them carefully) to avoid reference cycles.
An actor version is simpler, but every call becomes async.
NSCache isn't LRU and evicts unpredictably, but handles memory pressure.
Add a cost limit if values vary in size.
Mutex-backed LRU cache Type-checked · Swift 6.4
import Synchronization
final class LRUCache<Key: Hashable & Sendable, Value: Sendable>: Sendable {
private final class Node {
let key: Key
var value: Value
weak var prev: Node?
var next: Node?
init(key: Key, value: Value) { self.key = key; self.value = value }
}
private struct State {
var map: [Key: Node] = [:]
var head: Node? // most recently used
var tail: Node? // least recently used
mutating func remove(_ node: Node) {
if let prev = node.prev { prev.next = node.next } else { head = node.next }
if let next = node.next { next.prev = node.prev } else { tail = node.prev }
node.prev = nil; node.next = nil
}
mutating func pushFront(_ node: Node) {
node.next = head
head?.prev = node
head = node
if tail == nil { tail = node }
}
}
private let state = Mutex(State())
let capacity: Int
init(capacity: Int) {
precondition(capacity > 0)
self.capacity = capacity
}
func value(for key: Key) -> Value? {
state.withLock { s in
guard let node = s.map[key] else { return nil }
s.remove(node); s.pushFront(node)
return node.value
}
}
func insert(_ value: Value, for key: Key) {
state.withLock { s in
if let node = s.map[key] {
node.value = value
s.remove(node); s.pushFront(node)
return
}
let node = Node(key: key, value: value)
s.map[key] = node
s.pushFront(node)
if s.map.count > capacity, let lru = s.tail {
s.remove(lru)
s.map[lru.key] = nil
}
}
}
}
Common mistakes
O(n) recency updates with an array.
Locking only writes, not reads that reorder.
Senior take & follow-ups
Seniors give O(1) operations, explain why reads mutate, and pick the right lock.
Follow-up questions
Think of your answer first, then open the hint.
Why must reads take the lock?
Show hint
A read moves the node, which mutates the list.
Mutex or actor?
Show hint
Mutex for synchronous callers, actor for async ones.
Made by the author: practise the data structures and algorithms that sit next to iOS questions in most interview loops, with step-by-step visualizations and runnable code.