Implement rotation, LRU cache, streaming median, cycle detection
Company: Apple
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Onsite
Quick Answer: This question evaluates understanding of array/matrix manipulation, cache and data structure design, streaming algorithms for median maintenance, and graph theory for cycle detection, with emphasis on algorithmic complexity analysis and handling of edge cases.
Rotate Image 90 Degrees Clockwise (In Place)
Constraints
- matrix is square: len(matrix) == len(matrix[i]) for all i (n x n).
- 0 <= n <= 100.
- Elements are arbitrary 32-bit integers (may be negative).
- Must mutate the input in place using O(1) extra space.
Examples
Input: ([[1,2,3],[4,5,6],[7,8,9]],)
Expected Output: [[7,4,1],[8,5,2],[9,6,3]]
Explanation: Standard 3x3 rotation: the first column (1,4,7) becomes the first row reversed (7,4,1).
Input: ([[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]],)
Expected Output: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
Explanation: 4x4 case exercises two concentric rings (outer and inner).
Input: ([[1]],)
Expected Output: [[1]]
Explanation: A 1x1 matrix has no layers to rotate, so it is unchanged.
Input: ([[1,2],[3,4]],)
Expected Output: [[3,1],[4,2]]
Explanation: Smallest non-trivial even case: a single four-cell cycle.
Input: ([],)
Expected Output: []
Explanation: Empty matrix: n=0, no work, returns empty.
Hints
- Process the matrix in concentric layers/rings from outside to inside; there are n//2 layers.
- For each layer, rotate groups of four cells at once using one temporary variable: top -> right -> bottom -> left.
- The four indices that cycle for offset (i, j) are (i, j), (n-1-j, i), (n-1-i, n-1-j), and (j, n-1-i).
- An alternative O(1) approach is transpose-then-reverse-each-row, but the four-way layer swap avoids the second pass.
LRU Cache (get / put with Eviction)
Constraints
- 1 <= capacity <= 10^4.
- Keys and values fit in 32-bit integers.
- get on an absent key returns -1.
- put on an existing key updates the value and refreshes recency.
- 0 <= number of operations <= 10^5.
Examples
Input: (2, [['put',1,1],['put',2,2],['get',1],['put',3,3],['get',2],['put',4,4],['get',1],['get',3],['get',4]])
Expected Output: [1, -1, -1, 3, 4]
Explanation: Capacity 2. get(1)=1 (refreshes 1); put(3) evicts 2; get(2)=-1; put(4) evicts 1; get(1)=-1; get(3)=3; get(4)=4.
Input: (1, [['put',2,1],['get',2],['put',3,2],['get',2],['get',3]])
Expected Output: [1, -1, 2]
Explanation: Capacity 1: put(3) evicts key 2, so get(2)=-1 and get(3)=2.
Input: (2, [['get',1]])
Expected Output: [-1]
Explanation: Reading from an empty cache returns -1.
Input: (2, [['put',1,1],['put',1,10],['get',1]])
Expected Output: [10]
Explanation: put on an existing key updates the stored value to 10.
Input: (3, [['put',1,1],['put',2,2],['put',3,3],['get',1],['put',4,4],['get',2],['get',1],['get',3],['get',4]])
Expected Output: [1, -1, 1, 3, 4]
Explanation: get(1) makes 1 most-recent, so the next insert (4) evicts the LRU key 2; get(2)=-1, get(1)=1 survives.
Hints
- Combine a hash map (key -> node) with a doubly linked list ordering nodes from most- to least-recently used.
- On get/put of an existing key, move its node to the most-recently-used end.
- When size exceeds capacity after an insert, remove the node at the least-recently-used end and delete it from the map.
- Python's OrderedDict gives O(1) move_to_end and popitem(last=False), making it a clean stand-in for the map + linked list.
- For thread safety, protect the map+list mutations with a single lock; for TTL, store an expiry per entry and treat expired entries as misses.
Streaming Median (add / median)
Constraints
- Numbers are integers (may be negative); medians can be non-integer (.5) for even counts.
- median() on an empty stream returns None.
- Insertion must be O(log n); median retrieval must be O(1).
- 0 <= number of operations <= 10^5.
Examples
Input: ([['add',1],['add',2],['median'],['add',3],['median']],)
Expected Output: [1.5, 2.0]
Explanation: After 1,2 the median is (1+2)/2 = 1.5; after adding 3 the median is the middle value 2.0.
Input: ([['add',5],['median'],['add',15],['median'],['add',1],['median'],['add',3],['median']],)
Expected Output: [5.0, 10.0, 5.0, 4.0]
Explanation: Running medians of 5; 5,15; 5,15,1; 5,15,1,3 — i.e. 5.0, 10.0, 5.0, then (3+5)/2 = 4.0.
Input: ([['median']],)
Expected Output: [None]
Explanation: Querying the median before any add returns None.
Input: ([['add',-1],['add',-2],['add',-3],['median']],)
Expected Output: [-2.0]
Explanation: Negatives are handled normally; the middle of -1,-2,-3 is -2.0.
Input: ([['add',2],['add',2],['add',2],['add',2],['median']],)
Expected Output: [2.0]
Explanation: Duplicates: median of four 2's is (2+2)/2 = 2.0.
Hints
- Keep two heaps: a max-heap for the lower half and a min-heap for the upper half.
- In Python, simulate a max-heap by pushing negated values into a min-heap (heapq).
- After each add, rebalance so |len(lo) - len(hi)| <= 1, keeping the extra element in lo.
- If counts are equal the median is the average of the two heap tops; otherwise it is the top of the larger heap.
- Both heap tops are O(1), so median() is O(1); only add() pays the O(log n) heap cost.
Detect Cycle in a Directed Graph
Constraints
- 0 <= n <= 10^4 nodes, labeled 0..n-1.
- Edges are directed; [u, v] means u -> v.
- A self-loop [u, u] counts as a cycle.
- Multiple disconnected components must all be checked.
- Graph may be empty (n = 0 or no edges).
Examples
Input: (4, [[0,1],[1,2],[2,3],[3,1]])
Expected Output: True
Explanation: 1 -> 2 -> 3 -> 1 forms a cycle; DFS hits the GRAY node 1 via the back edge 3 -> 1.
Input: (4, [[0,1],[1,2],[2,3]])
Expected Output: False
Explanation: A simple path with no back edges is acyclic.
Input: (1, [[0,0]])
Expected Output: True
Explanation: A self-loop is a cycle.
Input: (3, [])
Expected Output: False
Explanation: No edges means no cycle.
Input: (2, [[0,1],[1,0]])
Expected Output: True
Explanation: Two nodes pointing at each other form a 2-cycle.
Input: (0, [])
Expected Output: False
Explanation: An empty graph has no cycle.
Hints
- Run DFS from every unvisited node to cover disconnected components.
- Use three colors: WHITE (unseen), GRAY (on the current recursion stack), BLACK (done).
- Finding an edge to a GRAY node is a back edge -> a cycle exists.
- Color a node BLACK only after all its out-edges are explored; reaching a BLACK node is not a cycle.
- For undirected graphs, instead check for a visited neighbor that is not the immediate parent.