Apple Interview Prep

Interview Profile

  • Format: 6–8 rounds — multiple coding rounds + system design (on-device / distributed) + behavioral
  • Coding style: Methodical; prefers clean, maintainable solutions over one-liner cleverness; expect to walk through your reasoning at every step
  • Senior bar: OOP design patterns, memory efficiency (on-device ML constraints), correctness over micro-optimization; Apple will ask you to redesign your solution for memory pressure
  • What Apple cares about: Privacy-preserving design (data stays on device), on-device computation (CoreML, Core Data, HealthKit), memory efficiency (tight heap budgets on iPhone), and Swift/Objective-C runtime correctness — even in a Java interview, they’ll ask how memory ownership and ARC relate to your algorithm’s space usage

Pattern Frequency Breakdown

PatternFrequency %Notes
Trees / Graphs30%Filesystem trees, DOM (WebKit), Core Data object graphs
Strings / Arrays25%Siri NLP, keyboard autocorrect, text rendering
Dynamic Programming20%Autocorrect edit distance, Siri intent parsing
Design / OOP15%Iterator patterns, cache design, lazy evaluation
Heap10%HealthKit telemetry aggregation, scheduling

Problems From This Repo

ProblemWhy Apple Asks This
Binary Tree Level Order Traversal (LC 102)Filesystem BFS — traversing directory trees for Spotlight indexing, finding files at a given depth
Lowest Common Ancestor (LC 236)App hierarchy navigation — finding the common UIViewController ancestor for a modal presentation decision
Number of Islands (LC 200)Region detection in Maps — identifying contiguous land regions, connected indoor floor plan areas
Implement Trie (LC 208)Siri autocomplete — prefix matching over the user’s contact list and app vocabulary
Copy List with Random Pointer (LC 138)Deep copy and memory model — duplicating NSManagedObject graphs with arbitrary relationships; ARC retain cycles
Edit Distance (LC 72)Keyboard autocorrect — minimum edit distance from a typed word to the nearest dictionary entry
Word Break (LC 139)Siri intent segmentation — splitting a speech recognition hypothesis into recognized intent tokens
Largest Rectangle in Histogram (LC 84)UI layout engine — computing the largest possible frame that fits within a set of constrained view heights
Design HashMap (LC 706)OOP design fundamentals — open addressing vs. chaining; how Apple’s NSDictionary handles collision resolution
Non-overlapping Intervals (LC 435)Calendar / event scheduling — removing the fewest events to produce a non-conflicting calendar

New Problems (Apple-Specific)


Problem 1: Flatten Nested List Iterator (LC 341) — Medium

Link: LeetCode 341

Quick Summary: Given a nested list of integers, implement an iterator that flattens it. hasNext() returns true if there are still integers; next() returns the next integer.

Industry Context: Swift’s LazySequence protocol and Objective-C’s NSEnumerator are built on exactly this pattern. Apple expects a discussion of lazy evaluation — don’t pre-flatten the entire list into an array at construction time (eager), because the input may be infinite or too large to materialize. The generator/coroutine mental model is the right framing: yield values on demand. Apple interviewers will explicitly ask “what if the nested list is too large to fit in memory?” — that’s the pivot from the eager approach to the stack-based lazy approach.


Approach 1: Eager Flattening at Construction Time

// Time: O(n) construction  Space: O(n) — pre-materializes everything
public class NestedIteratorEager implements Iterator<Integer> {
    private List<Integer> flat = new ArrayList<>();
    private int index = 0;
 
    public NestedIteratorEager(List<NestedInteger> nestedList) {
        flatten(nestedList);
    }
 
    private void flatten(List<NestedInteger> list) {
        for (NestedInteger ni : list) {
            if (ni.isInteger()) {
                flat.add(ni.getInteger());
            } else {
                flatten(ni.getList());  // recursive DFS
            }
        }
    }
 
    @Override
    public Integer next() { return flat.get(index++); }
 
    @Override
    public boolean hasNext() { return index < flat.size(); }
}
  • Time: O(n) at construction, O(1) per next() / hasNext()
  • Space: O(n) — entire flattened output stored in memory at once
  • Tradeoff: Simple and correct, but materializes the full list eagerly. Fails if the nested structure is a lazily-generated stream, is infinite, or exceeds available heap. Apple will dock points for this approach without acknowledging the memory concern.

Approach 2: Stack-Based Lazy Iterator (optimal)

// Time: O(1) amortized per next()/hasNext()  Space: O(d) — d = nesting depth
public class NestedIterator implements Iterator<Integer> {
    // Stack holds iterators at each nesting level — top is the "current" level
    private Deque<Iterator<NestedInteger>> stack = new ArrayDeque<>();
 
    public NestedIterator(List<NestedInteger> nestedList) {
        stack.push(nestedList.iterator());
    }
 
    @Override
    public boolean hasNext() {
        // Advance the stack until the top iterator has an integer ready
        while (!stack.isEmpty()) {
            if (!stack.peek().hasNext()) {
                stack.pop();            // current level exhausted — go up
            } else {
                NestedInteger top = stack.peek().next();
                if (top.isInteger()) {
                    // Put it back by re-prepending — trick: use a single-element deque or...
                    // Better: peek-ahead using a stored "pending" field
                    // See refined version below
                    return true;
                } else {
                    stack.push(top.getList().iterator());  // descend into nested list
                }
            }
        }
        return false;
    }
 
    @Override
    public Integer next() {
        // hasNext() leaves the next integer on top via peeked field
        return stack.peek().next().getInteger();  // see refined version
    }
}
 
// Refined version: store the peeked integer to avoid consuming it in hasNext()
public class NestedIteratorRefined implements Iterator<Integer> {
    private Deque<Iterator<NestedInteger>> stack = new ArrayDeque<>();
    private Integer peeked = null;  // the next integer to return, if already resolved
 
    public NestedIteratorRefined(List<NestedInteger> nestedList) {
        stack.push(nestedList.iterator());
    }
 
    @Override
    public boolean hasNext() {
        if (peeked != null) return true;
        while (!stack.isEmpty()) {
            if (!stack.peek().hasNext()) {
                stack.pop();
                continue;
            }
            NestedInteger ni = stack.peek().next();
            if (ni.isInteger()) {
                peeked = ni.getInteger();   // store and stop
                return true;
            } else {
                stack.push(ni.getList().iterator());  // descend
            }
        }
        return false;
    }
 
    @Override
    public Integer next() {
        if (!hasNext()) throw new NoSuchElementException();
        Integer val = peeked;
        peeked = null;
        return val;
    }
}
  • Time: O(1) amortized — each NestedInteger is touched at most twice (once in hasNext(), once consumed)
  • Space: O(d) — where d is the maximum nesting depth; the stack holds one iterator per nesting level
  • Key Insight: The stack stores iterators (cursors into each level’s list), not the list contents themselves. When the top iterator is exhausted, pop back to the parent level. When a nested list is encountered, push its iterator to descend. The peeked field is critical: hasNext() must not consume an element (side-effect-free contract), so store the resolved integer and return it again in next(). This is the iterator/generator coroutine pattern — lazy pull-based evaluation. Apple’s follow-up: “how would you implement this with Java’s Stream.flatMap()? How does its internal spliterator work?” — the answer is the same stack-based lazy approach.

Problem 2: Convert Binary Search Tree to Sorted Doubly Linked List (LC 426) — Medium

Link: LeetCode 426

Quick Summary: Convert a BST to a sorted circular doubly linked list in-place. Left pointers become prev, right pointers become next. Return the head of the list (smallest element).

Industry Context: Core Data and SQLite use B-tree pages internally; when a B-tree page is converted to a sorted linked list for sequential scan or merge operations, this in-order in-place conversion is the exact operation performed. Apple’s SQLite-backed Core Data stack does this during compaction and migration. Also relevant to how NSFetchedResultsController builds sorted arrays from a BST-backed object store.


// Time: O(n)  Space: O(n) — extracts values, then rebuilds as DLL
public Node treeToDoublyListExtraSpace(Node root) {
    if (root == null) return null;
    List<Node> nodes = new ArrayList<>();
    inorder(root, nodes);  // collect in sorted order
 
    for (int i = 0; i < nodes.size(); i++) {
        nodes.get(i).left  = (i > 0) ? nodes.get(i - 1) : nodes.get(nodes.size() - 1);
        nodes.get(i).right = (i < nodes.size() - 1) ? nodes.get(i + 1) : nodes.get(0);
    }
    return nodes.get(0);
}
 
private void inorder(Node node, List<Node> nodes) {
    if (node == null) return;
    inorder(node.left, nodes);
    nodes.add(node);
    inorder(node.right, nodes);
}
  • Time: O(n)
  • Space: O(n) — the intermediate node list
  • Tradeoff: Correct but uses O(n) extra space for the list. Apple will ask “can you do this in O(1) extra space?” — that’s the pivot to the in-place approach.

Approach 2: In-Order DFS In-Place with prev Pointer (optimal)

// Time: O(n)  Space: O(h) — only recursion stack; no auxiliary data structures
private Node prev = null;   // previously visited node during in-order traversal
private Node head = null;   // head of the resulting doubly linked list
 
public Node treeToDoublyList(Node root) {
    if (root == null) return null;
    prev = null;
    head = null;
    inorderLink(root);
    // Close the circular link: head.left = last node, last node.right = head
    head.left = prev;
    prev.right = head;
    return head;
}
 
private void inorderLink(Node node) {
    if (node == null) return;
    inorderLink(node.left);   // process left subtree first (smaller values)
 
    // Link current node to previous
    if (prev == null) {
        head = node;          // first visited (smallest) node becomes head
    } else {
        prev.right = node;    // prev's next = current
        node.left  = prev;    // current's prev = prev
    }
    prev = node;              // advance prev pointer
 
    inorderLink(node.right);  // process right subtree (larger values)
}
  • Time: O(n) — every node visited once
  • Space: O(h) — recursion stack only; no auxiliary list
  • Key Insight: In-order traversal visits BST nodes in ascending order. Maintain a prev pointer that trails the current node by one step — at each node, stitch prev.right = current and current.left = prev. The head is set once when prev == null (the first, smallest node). After the traversal completes, prev points to the last (largest) node — close the circular link manually: head.left = prev; prev.right = head. The elegance is that no new nodes are created and no array is needed: the BST pointer fields are reused as DLL prev/next pointers in-place.

Problem 3: Range Sum Query — Mutable (LC 307) — Medium

Link: LeetCode 307

Quick Summary: Given an integer array, support two operations efficiently: update(i, val) — set nums[i] = val; sumRange(l, r) — return the sum of elements from index l to r inclusive.

Industry Context: Apple Maps and HealthKit telemetry — sensor data arrives as point updates (a GPS coordinate’s accuracy changes, a heart rate sample is revised), and the system needs to answer range queries (total steps between timestamps, elevation gain over a route segment) efficiently. A plain array gives O(1) update but O(n) query; a prefix sum array gives O(1) query but O(n) update. The Fenwick Tree (Binary Indexed Tree) achieves O(log n) for both — the right tradeoff for high-frequency sensor streams.


Approach 1: Prefix Sum Array — O(1) query but O(n) update

// Query: O(1)  Update: O(n)  Space: O(n)
public class NumArrayPrefixSum {
    private int[] prefix;
    private int[] nums;
 
    public NumArrayPrefixSum(int[] nums) {
        this.nums = nums.clone();
        this.prefix = new int[nums.length + 1];
        for (int i = 0; i < nums.length; i++) {
            prefix[i + 1] = prefix[i] + nums[i];
        }
    }
 
    public void update(int i, int val) {
        // Must rebuild entire prefix array from index i — O(n)
        int delta = val - nums[i];
        nums[i] = val;
        for (int j = i + 1; j <= nums.length; j++) {
            prefix[j] += delta;
        }
    }
 
    public int sumRange(int l, int r) {
        return prefix[r + 1] - prefix[l];   // O(1)
    }
}
  • Time: O(n) update, O(1) query
  • Space: O(n)
  • Tradeoff: Optimal if updates are rare and queries dominate. Fails under high-frequency mutation (HealthKit continuous sensor ingestion). This is the baseline to present before introducing the Fenwick Tree.

Approach 2: Fenwick Tree / Binary Indexed Tree (optimal for mixed update/query)

// Update: O(log n)  Query: O(log n)  Space: O(n)
public class NumArray {
    private int[] bit;   // Binary Indexed Tree (1-indexed internally)
    private int[] nums;
    private int n;
 
    public NumArray(int[] nums) {
        this.n = nums.length;
        this.nums = nums.clone();
        this.bit = new int[n + 1];
        // Build BIT by treating each element as a point update
        for (int i = 0; i < n; i++) {
            addToTree(i + 1, nums[i]);
        }
    }
 
    // Add delta to position i (1-indexed) and propagate to responsible ancestors
    private void addToTree(int i, int delta) {
        for (; i <= n; i += i & (-i)) {   // i & (-i) = lowest set bit of i
            bit[i] += delta;
        }
    }
 
    public void update(int i, int val) {
        int delta = val - nums[i];
        nums[i] = val;
        addToTree(i + 1, delta);   // convert to 1-indexed
    }
 
    // Prefix sum [1..i] (1-indexed)
    private int prefixSum(int i) {
        int sum = 0;
        for (; i > 0; i -= i & (-i)) {   // walk up by clearing lowest set bit
            sum += bit[i];
        }
        return sum;
    }
 
    public int sumRange(int l, int r) {
        return prefixSum(r + 1) - prefixSum(l);   // convert to 1-indexed
    }
}
  • Time: O(log n) for both update and sumRange
  • Space: O(n) — the BIT array
  • Key Insight: A Fenwick Tree stores partial sums in a tree structure encoded in a flat array. Each index i in the BIT is responsible for a range of length equal to i & (-i) (the lowest set bit of i). To update index i, walk up by adding the lowest set bit: i += i & (-i). To query a prefix sum [1..i], walk up by removing the lowest set bit: i -= i & (-i). The magic of i & (-i) is that it isolates exactly the bit that controls the responsibility range — two’s complement makes (-i) flip all bits above the lowest set bit, so i & (-i) extracts it cleanly. When to use BIT vs Segment Tree: BIT is simpler to implement and has better cache performance for 1D range sum queries; Segment Tree is more general (range min/max/GCD, lazy propagation for range updates). For Apple interviews, state this tradeoff explicitly.

Problem 4: Lowest Common Ancestor of Binary Tree III (LC 1650) — Medium

Link: LeetCode 1650

Quick Summary: Each node has a parent pointer in addition to left and right. Find the lowest common ancestor of two nodes p and q. You do not have access to the root.

Industry Context: WebKit’s DOM tree — every DOM node has a parentNode pointer. When two event listeners at different nodes need to find their shared bubbling ancestor (e.g., to coordinate a CSS animation or stop event propagation), this exact algorithm is used. Also: UIKit’s UIView hierarchy — given two views deep in the hierarchy, find the common ancestor view for adding a shared overlay.


Approach 1: HashSet of Ancestors

// Time: O(h)  Space: O(h) — h = height from node to root
public Node lowestCommonAncestorWithSet(Node p, Node q) {
    Set<Node> visitedAncestors = new HashSet<>();
    // Walk p up to root, recording all ancestors
    for (Node cur = p; cur != null; cur = cur.parent) {
        visitedAncestors.add(cur);
    }
    // Walk q up to root; first match is the LCA
    for (Node cur = q; cur != null; cur = cur.parent) {
        if (visitedAncestors.contains(cur)) return cur;
    }
    return null;  // no common ancestor (disconnected trees — shouldn't happen per constraints)
}
  • Time: O(h) — each walk is at most h steps
  • Space: O(h) — ancestor set for p’s path

Approach 2: Two-Pointer Path Equalization (optimal — O(1) space)

// Time: O(h)  Space: O(1)
public Node lowestCommonAncestor(Node p, Node q) {
    Node a = p, b = q;
    // Each pointer walks up its own path to root, then switches to the other node's start.
    // After at most (depth_p + depth_q) steps, they will meet at the LCA.
    while (a != b) {
        a = (a == null) ? q : a.parent;
        b = (b == null) ? p : b.parent;
    }
    return a;
}
  • Time: O(h) — at most depth(p) + depth(q) steps
  • Space: O(1)
  • Key Insight: This is the linked list cycle intersection trick (LC 160) applied to tree parent pointers. If p is at depth dp and q is at depth dq, pointer a travels dp steps to the root, then dq steps from q’s start — total dp + dq. Pointer b travels dq steps to the root, then dp steps from p’s start — also dp + dq. They are now at the same depth and will meet at the LCA on the next steps in lock-step. The null-switch (a = (a == null) ? q : a.parent) is the pivot that resets each pointer to the other’s starting position when it falls off the top. No height computation, no set, no extra allocation.

Problem 5: LFU Cache (LC 460) — Hard

Link: LeetCode 460

Quick Summary: Design a data structure that implements a Least Frequently Used (LFU) cache with get(key) and put(key, value). Both operations must run in O(1). On capacity overflow, evict the key with the lowest frequency; if tied, evict the least recently used among those.

Industry Context: Apple’s CoreML on-device model cache — when a device runs multiple ML models (Siri speech model, face recognition, text classification), memory is severely constrained. LFU is more appropriate than LRU here because frequently-used models (Siri’s always-on wake-word model) should survive eviction even if they weren’t used in the last few seconds. A straight LRU would incorrectly evict the wake-word model during a period of silence. Apple’s tighter memory budget (256MB–8GB heap vs. server GBs) makes frequency-based eviction a first-class design concern.


Approach 1: LRU as a baseline for contrast

// LRU: O(1) get/put using LinkedHashMap — presented for comparison only
public class LRUCache extends LinkedHashMap<Integer, Integer> {
    private final int capacity;
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);  // accessOrder = true
        this.capacity = capacity;
    }
    public int get(int key) { return getOrDefault(key, -1); }
    public void put(int key, int value) { super.put(key, value); }
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}
  • Time: O(1) amortized
  • Space: O(capacity)
  • Tradeoff: LRU is easy with LinkedHashMap(accessOrder=true). But for CoreML, a model used 1000 times yesterday and once this minute should not be evicted before a model used only 2 times ever. LRU would evict the frequent model; LFU keeps it.

Approach 2: LFU with Three HashMaps — O(1) All Operations (optimal)

// Time: O(1) for get and put  Space: O(capacity)
public class LFUCache {
    private final int capacity;
    private int minFreq;                              // current minimum frequency in the cache
 
    private final Map<Integer, Integer> keyToVal;     // key → value
    private final Map<Integer, Integer> keyToFreq;    // key → frequency
    // freq → LinkedHashSet of keys at that frequency (LinkedHashSet preserves insertion order
    //         so the "head" is the least recently used among keys with the same frequency)
    private final Map<Integer, LinkedHashSet<Integer>> freqToKeys;
 
    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.minFreq  = 0;
        this.keyToVal  = new HashMap<>();
        this.keyToFreq = new HashMap<>();
        this.freqToKeys = new HashMap<>();
    }
 
    public int get(int key) {
        if (!keyToVal.containsKey(key)) return -1;
        incrementFreq(key);
        return keyToVal.get(key);
    }
 
    public void put(int key, int value) {
        if (capacity <= 0) return;
        if (keyToVal.containsKey(key)) {
            keyToVal.put(key, value);
            incrementFreq(key);
            return;
        }
        // New key — evict if at capacity
        if (keyToVal.size() >= capacity) {
            evictLFU();
        }
        // Insert new key with frequency 1
        keyToVal.put(key, value);
        keyToFreq.put(key, 1);
        freqToKeys.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key);
        minFreq = 1;   // a new key always starts at frequency 1
    }
 
    // Increment the frequency of an existing key
    private void incrementFreq(int key) {
        int freq = keyToFreq.get(key);
        keyToFreq.put(key, freq + 1);
 
        // Move key from freq bucket to freq+1 bucket
        freqToKeys.get(freq).remove(key);
        if (freqToKeys.get(freq).isEmpty()) {
            freqToKeys.remove(freq);
            if (minFreq == freq) minFreq++;  // if we just emptied the min bucket, advance minFreq
        }
        freqToKeys.computeIfAbsent(freq + 1, k -> new LinkedHashSet<>()).add(key);
    }
 
    // Evict the least recently used key among minimum-frequency keys
    private void evictLFU() {
        LinkedHashSet<Integer> minFreqKeys = freqToKeys.get(minFreq);
        int evictKey = minFreqKeys.iterator().next();  // first = least recently used at minFreq
        minFreqKeys.remove(evictKey);
        if (minFreqKeys.isEmpty()) freqToKeys.remove(minFreq);
        keyToVal.remove(evictKey);
        keyToFreq.remove(evictKey);
    }
}
  • Time: O(1) for both get and put

  • Space: O(capacity) — three maps, each bounded by capacity

  • Key Insight: Three maps working in concert:

    1. keyToVal — the actual cache data
    2. keyToFreq — each key’s current access count
    3. freqToKeys — a bucket per frequency, each bucket is a LinkedHashSet (ordered by insertion time to resolve LRU tie-breaking within the same frequency)

    The minFreq variable is the critical O(1) shortcut: instead of scanning all frequency buckets to find the minimum, we maintain it as a global invariant. It resets to 1 on every put of a new key (a new key always starts at frequency 1, which is always ≤ any existing frequency). It increments by 1 when incrementFreq empties the current minFreq bucket — because frequencies only increase, the new minimum must be exactly minFreq + 1.

    LFU vs LRU decision framework: LRU is correct when recency is the dominant predictor of future access (web page cache, session data). LFU is correct when frequency is the dominant predictor and temporal locality is weak (ML model cache, OS page replacement for long-running servers). Apple’s interview will ask this directly.


System Design Connections

Algorithm PatternApple Product Mapping
Trie (prefix tree)Siri autocomplete — prefix matching over contact names, app vocabulary, and learned phrases
LFU CacheCoreML model cache — frequency-based eviction under tight on-device memory constraints
Fenwick Tree / BITHealthKit telemetry — efficient range sum queries over mutable sensor time-series (steps, heart rate)
Iterator / Lazy SequenceSwift LazySequence / NSEnumerator — pull-based evaluation of nested collections without materializing
BST ↔ DLL ConversionCore Data / SQLite B-tree — sorted page linearization for sequential scan and migration
Two Pointers on Parent ChainWebKit DOM — shared ancestor lookup for event bubbling and CSS cascade resolution
Edit Distance (DP 2D)Keyboard autocorrect — nearest dictionary word under Levenshtein distance
Word Break (DP 1D)Siri intent segmentation — tokenizing speech recognition hypotheses into intent slots
Interval Scheduling (Greedy)Calendar.app — removing minimum events to produce a conflict-free schedule
Connected ComponentsMaps — identifying contiguous geographic regions, indoor floor plan area detection

2-Week Sprint Plan

Week 1: Trees / Strings / OOP Design

DayFocusProblems
MonTree BFS + parent pointersLC 102 (re-solve cold); LC 1650 (new)
TueBST + in-order patternsLC 236 (re-solve cold); LC 426 (new)
WedTrie + string patternsLC 208 (re-solve cold); design the autocomplete extension
ThuIterator / OOP designLC 341 (new); walk through Swift LazySequence analogy
FriHashMap / deep copyLC 138 (re-solve cold); LC 706 (re-solve cold)
SatMock sessionTwo problems back-to-back, 35 min each
SunOOP reviewIdentify design patterns used this week (Iterator, Flyweight, Proxy)

Week 2: DP / Data Structures / System Design Integration

DayFocusProblems
MonDP 2DLC 72 (re-solve cold); connect to autocorrect pipeline
TueDP 1DLC 139 (re-solve cold); connect to Siri intent parsing
WedRange query structuresLC 307 (new); BIT vs Segment Tree comparison
ThuCache designLC 460 (new — LFU); compare LFU vs LRU decision framework
FriGraphs + GreedyLC 200 (re-solve); LC 435 (re-solve); Maps + Calendar context
SatFull mock loop2 coding + 1 system design (design Siri autocomplete with a Trie)
SunSystem design integrationWalk one end-to-end Apple system using patterns from this sheet

Senior-Level Talking Points

Privacy-Preserving Design

Apple will ask how your algorithm handles private user data. For every problem, be ready to add:

  • “If this data is user-PII (contacts, health data, messages), the index or cache should live entirely on-device — no server-side component.”
  • “Differential privacy: if we need aggregate statistics, we’d add calibrated Laplace noise to the Fenwick Tree range sum before returning it.”
  • “On-device ML (Core ML) means the model weights and inference never leave the device — the LFU cache for model weights is a local-only data structure.”

On-Device Computation and Memory Efficiency

Apple explicitly tests memory awareness:

  • Eager vs. lazy: Always flag when a solution materializes data unnecessarily (LC 341 eager approach). Ask: “what’s the heap budget?” before choosing eager.
  • BIT vs. Segment Tree: BIT uses exactly O(n) space with no overhead; Segment Tree uses O(4n). On an iPhone with 4GB total RAM and 200MB app budget, this matters.
  • LFU vs. LRU: For CoreML, LFU is correct because the most frequently used models (wake-word, Face ID) must survive short periods of disuse. Explain this without being asked.
  • Stack overflow risk: Recursive DFS on a deep BST (Core Data object graph can have thousands of nodes) — always mention iterative alternatives for production-grade code.

Swift / Objective-C Memory Model Context

Even in a Java interview, Apple may ask:

  • ARC and retain cycles: LC 138 (copy list with random pointer) maps directly to breaking retain cycles when deep-copying an NSManagedObject graph. In Swift, a weak var breaks the cycle the same way your HashMap maps old nodes to new nodes.
  • Value types vs. reference types: Java’s int[] is a reference type; Swift Array<Int> is a value type (copy-on-write). For LC 307, if the array were a Swift value type being passed across threads, you’d need explicit synchronization — in Java, use AtomicIntegerArray for thread-safe BIT updates.
  • Memory ownership: The LFU cache’s eviction policy is a manual memory management policy — conceptually equivalent to ARC’s reference counting, but policy-driven rather than count-driven.

Behavioral Signals Apple Looks For

  • Methodical reasoning: Apple values deliberate explanation over speed. Walk through every design decision out loud.
  • Correctness first: Get a correct solution before optimizing. Apple will penalize a clever-but-wrong solution more than a verbose-but-correct one.
  • Ownership of the design: “I’d choose this approach in production because…” — Apple wants engineers who own their decisions, not just implement specs.
  • Privacy and ethics unprompted: Mention data privacy implications for any problem involving user data. This signals cultural alignment, not just technical skill.
  • Cross-language thinking: If you write Java, be ready to discuss how the same algorithm would differ in Swift — value semantics, protocol-oriented design, and memory ownership are fair game.