Design
# System & Data Structure Design Design problems in DSA interviews test your ability to translate requirements into a functional, efficient, and maintainable class structure. Unlike standard algorithmic problems, the focus here is on **State Management** and **API Design**. ### Core Principles 1. **Encapsulation:** Keep data private and expose functionality through well-defined methods. 2. **Trade-offs:** Every design choice has a cost. Is it better to have $O(1)$ read and $O(N)$ write, or vice versa? 3. **State Consistency:** Ensure that your internal data structures (e.g., a Map and a List) stay in sync after every operation. ### Common Design Patterns #### 1. HashMap + Doubly Linked List (DLL) The "Gold Standard" for $O(1)$ caching (LRU/LFU). ```text [Head] <-> [Node A] <-> [Node B] <-> [Node C] <-> [Tail] ^ ^ ^ ^ ^ (MRU) (Data) (Data) (Data) (LRU) ``` - **HashMap:** Provides $O(1)$ lookups for keys to their corresponding nodes. - **DLL:** Provides $O(1)$ addition/removal of nodes at both ends, maintaining the order of access. #### 2. Amortized Analysis (Rebalancing) Commonly used in **Queue using Stacks** or **Dynamic Arrays**. - Instead of doing heavy work on every call, we batch it. Pushing to a stack is $O(1)$, and "flipping" elements to another stack happens only when necessary, averaging $O(1)$ per operation. #### 3. Ring Buffers (Circular Arrays) Used for fixed-size memory management (e.g., **Circular Queue**, **Hit Counter**). ```text [0] [1] [2] [3] [4] [5] ^ ^ ^ Head (Data) Tail (Pops) (Next Push) ``` - Use `(index + 1) % capacity` to wrap around the array. #### 4. Concurrency & Thread Safety For "Hard" design problems (e.g., **Bounded Blocking Queue**). - Use **Mutexes** (Locks) to prevent data races. - Use **Condition Variables** (`wait`/`notify`) to manage producer-consumer logic efficiently without busy-waiting. ### How to Approach a Design Problem 1. **Identify the API:** What methods do you need to implement? (`get`, `put`, `push`, etc.) 2. **Define the State:** What variables represent the current state? (Size, Capacity, Pointers). 3. **Choose the Data Structures:** Select the combination that minimizes time complexity for the most frequent operations. 4. **Dry Run:** Trace the state changes through a sequence of operations based on your chosen structure.
LRU Cache
Design a data structure that follows the constraints of a Least Recently Used (LRU) cache.
Requirements
- get(key): Return the value of the key if it exists, otherwise return -1.
- put(key, value): Update or insert the value. If the cache reaches capacity, evict the least recently used item before inserting a new one.
Goal
All operations must run in average time complexity.
Examples
Level I: Brute Force (List of Pairs)
Intuition
Maintain a simple list or array of [key, value] pairs. For every get or put operation, we iterate through the list to find if the key exists. This established a baseline for why we need more optimized structures.
Detailed Dry Run
Input: put(1,1), put(2,2), get(1)
| Step | Operation | List State (Old to New) | Result |
|---|---|---|---|
| 1 | put(1,1) | [[1,1]] | null |
| 2 | put(2,2) | [[1,1], [2,2]] | null |
| 3 | get(1) | [[2,2], [1,1]] | 1 |
Level II: Intermediate (Built-in Ordered Map)
Intuition
Most modern languages provide data structures that maintain insertion order. We can leverage these (e.g., LinkedHashMap in Java, OrderedDict in Python, or the standard Map in JS which preserves key insertion order) to implement LRU with minimal code.
Detailed Dry Run
Input: put(1,1), put(2,2), get(1)
| Step | Operation | Map State (Keys) | LRU Logic |
|---|---|---|---|
| 1 | put(1,1) | {1} | Added 1 |
| 2 | put(2,2) | {1, 2} | Added 2 |
| 3 | get(1) | {2, 1} | Re-inserted 1 to end |
Level III: Optimal (HashMap + Deque)
Intuition
To achieve true without relying on language-specific magic, we implement our own Doubly Linked List (DLL) and a HashMap. The DLL allows removal and addition at both ends, while the HashMap provides access to any node given its key.
Detailed Dry Run
Input: put(1,1), put(2,2), get(1)
| Step | Operation | DLL Structure (Head <-> Tail) | Map State |
|---|---|---|---|
| 1 | put(1,1) | H <-> [1:1] <-> T | {1: node1} |
| 2 | put(2,2) | H <-> [2:2] <-> [1:1] <-> T | {1: n1, 2: n2} |
| 3 | get(1) | H <-> [1:1] <-> [2:2] <-> T | {1: n1, 2: n2} |
Design Tic-Tac-Toe
Design a Tic-Tac-Toe game that is played on an grid. You may assume the following rules:
- A move is guaranteed to be valid and is placed on an empty block.
- Once a winning condition is reached, no more moves will be allowed.
- A player who succeeds in placing of their marks in a horizontal, vertical, or diagonal row wins the game.
Goal
move(row, col, player) must run in time complexity.
Examples
Level I: Brute-Force Grid Scan
Intuition
Maintain the actual grid. For every move, scan the entire row, column, and two diagonals to check if all elements belong to the current player.
Detailed Dry Run
Input: n=3, move(0,0,1)
| Step | Move | Grid State | Result |
|---|---|---|---|
| 1 | (0,0,1) | [[1,0,0],...] | 0 (No win yet) |
Level II: Optimized Grid Scan
Intuition
Instead of re-scanning everything, we only check the specific row, column, and diagonals that the current move affected. This reduces the work from to .
Detailed Dry Run
Input: n=3, move(1,1,1)
| Scan | Elements | Result |
|---|---|---|
| Row 1 | [0,1,0] | No Win |
| Col 1 | [0,1,0] | No Win |
| Diag 1 | [1,1,0] | No Win |
| Diag 2 | [0,1,0] | No Win |
Level III: Counter Arrays (Optimal)
Intuition
To achieve time, we stop storing the grid entirely. Instead, we maintain count arrays for rows and columns, and two variables for the diagonals. For player 1, we add 1; for player 2, we subtract 1. If any count's absolute value reaches , we have a winner.
Detailed Dry Run
Input: n=3, move(0,0,1)
| Step | Move | Rows Count | Cols Count | Diag | Anti-Diag | Result |
|---|---|---|---|---|---|---|
| 1 | (0,0,1) | [1,0,0] | [1,0,0] | 1 | 0 | 0 |
| 2 | (1,1,1) | [1,1,0] | [1,1,0] | 2 | 0 | 0 |
| 3 | (2,2,1) | [1,1,1] | [1,1,1] | 3 | 0 | 1 (Win!) |
Implement Stack using Queues
Implement a last-in-first-out (LIFO) stack using one or more queues. The implemented stack should support all the functions of a normal stack (push, top, pop, and empty).
Examples
Level I: Two Queues (Push O(1))
Intuition
Maintain two queues. push is to q1. For pop, we move all elements except the last one from q1 to q2, then the last one is the stack's top.
Detailed Dry Run
Input: push(1), push(2), pop()\n| Step | Op | Q1 | Q2 | Result |\n| :--- | :--- | :--- | :--- | :--- |\n| 1 | push(1) | [1] | [] | null |\n| 2 | push(2) | [1, 2] | [] | null |\n| 3 | pop() | [] | [1] | 2 |
Level II: Two Queues (Pop/Top O(1))
Intuition
To make pop and top , we do the heavy lifting during push. When pushing x, we add it to q2, then move everything from q1 to q2, then swap names. This keeps q1 in LIFO order.
Detailed Dry Run
Push(1), Push(2)
| Op | Q1 | Q2 | Action |
|---|---|---|---|
push(1) | [1] | [] | Simple add |
push(2) | [] | [2, 1] | Add 2 to Q2, then move 1 from Q1 to Q2 |
pop() | [1] | [] | pop from Q1 |
Level III: One Queue (Optimal)
Intuition
The optimal implementation uses only one queue. For push, we add the element and rotate the queue times so the newly added element becomes the front of the queue.
Detailed Dry Run
Queue: [1, 2] -> push(3) -> [1, 2, 3] -> Rotate 1: [2, 3, 1] -> Rotate 2: [3, 1, 2]
| Step | Queue | Action |
|---|---|---|
| 1 | [1,2] | Initial |
| 2 | [1,2,3] | Add 3 to back |
| 3 | [2,3,1] | Poll 1, Add to back |
| 4 | [3,1,2] | Poll 2, Add to back |
| Result: 3 is at front (LIFO) |
Implement Queue using Stacks
Implement a first-in-first-out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (push, peek, pop, and empty).
Examples
Level I: Two Stacks (Push O(N))
Intuition
When pushing an element, move all elements from the primary stack to a temporary stack, push the new element, then move everything back.
Detailed Dry Run
Push(1), Push(2) -> S1: [2, 1] (top is 1)
Level II: Two Stacks (Pop O(N))
Intuition
To make push , just push to s1. For pop and peek, if s1 has elements, move everything to s2, remove/view the top, then move everything back to s1 to maintain order. This establishes a baseline for amortized optimization.
Detailed Dry Run
Push(1), Push(2), Pop()
| Op | S1 | S2 | Action |
|---|---|---|---|
push(1) | [1] | [] | |
push(2) | [1, 2] | [] | |
pop() | [] | [1, 2] | Move S1 to S2, pop 1, move back |
| Result: 1 |
Level III: Two Stacks (Amortized O(1))
Intuition
Use inStack for push and outStack for pop. Transfer only when outStack is empty. This way, each element is moved at most twice (once into inStack, once into outStack), leading to amortized time.
Detailed Dry Run
Push(1), Push(2) -> In: [1, 2]. Pop() -> Move to Out: [2, 1]. Pop 1.
| Op | In | Out | Action |
|---|---|---|---|
push(1) | [1] | [] | |
push(2) | [1, 2] | [] | |
pop() | [] | [2] | Move S1 to S2, return 1 |
| Result: 1 |
Design Hit Counter
Design a hit counter which counts the number of hits received in the past 5 minutes (i.e., the past 300 seconds).
Examples
Level I: Queue of Timestamps
Intuition
Record every hit as a timestamp in a queue. For getHits, remove all timestamps from the front that are older than 300 seconds.
Detailed Dry Run
Queue: [1, 2, 3]. getHits(305) -> 1 is older than (305-300=5), so remove 1.
Level II: Map of Count by Second
Intuition
Instead of storing every hit, we store the total hit count for each unique second using a HashMap<Timestamp, Count>. This is more space-efficient if there are many hits at the same time.
Detailed Dry Run
hit(1), hit(1), hit(2) -> {1: 2, 2: 1}. getHits(301) -> Sum counts for keys > 1.
Level III: Scalable Circular Array (Optimal)
Intuition
To handle large-scale systems with millions of hits, we use a fixed-size array of 300 buckets (one for each second). Each bucket stores the timestamp and hit count.
Detailed Dry Run
Timestamp 301 maps to bucket 301%300 = 1. If old timestamp in bucket 1 was 1, we reset count. Else increment.
Design Circular Queue
Design your implementation of the circular queue. The circular queue is a linear data structure in which the operations are performed based on FIFO (First In First Out) principle and the last position is connected back to the first position to make a circle.
Level I: Array with Front/Rear Pointers
Intuition
Use a fixed-size array and two pointers, head and tail. Use modulo arithmetic to wrap around the array. We use a size variable to easily distinguish between empty and full states.
Detailed Dry Run
Enqueue(1), Enqueue(2), Dequeue()...
| Op | Array | Head | Tail | Size |
|---|---|---|---|---|
init(3) | [0,0,0] | 0 | -1 | 0 |
enq(1) | [1,0,0] | 0 | 0 | 1 |
enq(2) | [1,2,0] | 0 | 1 | 2 |
deq() | [1,2,0] | 1 | 1 | 1 |
Level II: Linked List Implementation
Intuition
Instead of an array, use a Doubly Linked List. This avoids modulo arithmetic but uses more memory for pointers. To make it 'circular' in memory, we can limit the number of nodes to k.
Detailed Dry Run
Head -> [1] <-> [2] <-> [3] <- Tail Enqueue(4) if size < k: Add new node after tail, move tail.
Min Stack
Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.
Level I: Brute Force (Scan on getMin)
Intuition
Maintain a simple stack of elements. Every time getMin() is called, iterate through the entire stack to find the minimum element.
Detailed Dry Run
Push(5), Push(3), getMin() -> Scan [5, 3] -> Min is 3.
Level II: Two Stacks (Standard Optimality)
Intuition
Maintain a second stack to store the minimum value encountered so far for each element in the main stack. This ensures getMin() is .
Detailed Dry Run
Push(5), Push(3)
| Stack | MinStack | Action |
| :--- | :--- | :--- | :--- |
| [5] | [5] | |
| [5, 3] | [5, 3] | 3 < 5, so push 3 |
| pop() | [5] | both pop |
Level III: Optimized Space (Single Stack with Min Delta)
Intuition
Store only the difference between the current value and the minimum value. This allows us to reconstruct the previous minimum when the current minimum is popped.
Detailed Dry Run
Push(x): store (x - min). If x < min, update min = x.
Design Add and Search Words
Design a data structure that supports adding new words and finding if a string matches any previously added string. The search string can contain letters or dots . where a dot can match any letter.
Complexity
addWord: where is word length.search: in worst case due to wildcards, but typically fast with Trie pruning.
Examples
Level I: Set of Words
Intuition
Store all added words in a HashSet. For search, if it contains no dots, do an lookup. If it contains dots, iterate through all words in the set () and check if they match the pattern.
Level II: HashMap of Lengths
Intuition
Group words by their length in a HashMap<Integer, Set<String>>. When searching, we only check words of the exact same length as the input, significantly reduced the comparisons.
Detailed Dry Run
Add('bad', 'dad', 'pad'). Map: {3: ['bad', 'dad', 'pad']}. Search('.ad') checks only length 3 set.
Level III: Trie (Prefix Tree) with DFS
Intuition
Use a Trie to store words. For exact matches, search is . For dots, use DFS to explore all possible children at that level.
Design Underground System
An underground railway system is keeping track of customer travel times between different stations. They are using this data to calculate the average time it takes to travel from one station to another.
Requirement
checkIn(id, stationName, t)checkOut(id, stationName, t)getAverageTime(startStation, endStation)
Examples
Level I: Single HashMap with List (Brute Force)
Intuition
Store every check-in and check-out event in a single list or map. When asked for the average, iterate through ALL logged events to find matches. This is slow but memory-efficient for simple logs.
Detailed Dry Run
Logs: [(id:45, in:'a', out:'b', time:7), (id:46, in:'c', out:'b', time:10)]. Average('a','b') -> find first entry -> 7.
Level II: String Concatenation Keys
Intuition
Use simple string concatenation start+"->"+end as keys in a single HashMap. Store lists of travel durations and compute averages on the fly.
Level III: Two HashMaps
Intuition
Use checkInMap to store id -> (station, time). Use routeMap to store (start, end) -> (totalTime, count). Checkout triggers a look-up in checkInMap, calculates the duration, and updates routeMap.
Design Snake Game
Design a Snake game that is played on a device with screen size . The snake is initially positioned at the top left corner with a length of 1 unit.
Logic
- Move the head.
- Check boundaries and self-collision.
- If head hits food, increase length (don't remove tail).
- Otherwise, move forward (remove tail).
Examples
Level II: Deque only (Body only scan)
Intuition
Maintain the body in a Deque. For collision check, iterate through the entire deque (). This avoids the secondary HashSet but is slower for long snakes.
Level III: Deque + HashSet
Intuition
Use a Deque to store the coordinates of the snake's body (head at front, tail at back). Use a HashSet of encoded coordinates (r * width + c) for self-collision check.
LFU Cache
Design a data structure that follows the constraints of a Least Frequently Used (LFU) cache.
Requirement
- get(key): Return value if exists, update frequency.
- put(key, value): Insert/update value. If at capacity, evict the least frequently used item. If there's a tie, evict the least recently used.
Examples
Level I: Brute Force Scan
Intuition
Store entries in a List or Map. For every put at capacity, iterate through the entire collection to find the item with the minimum frequency and the oldest access time. This is per operation but extremely simple.
Detailed Dry Run
Cache: {(A, f:2, t:1), (B, f:1, t:2)}. Put(C) evicts B because its frequency (1) is lower than A (2).
Level II: Priority Queue (O(log N))
Intuition
Use a PriorityQueue to store entries sorted by frequency, and then by access time (tie-breaker). While push and pop are , it is simpler to implement than the DLL version.
Level III: Map Frequency to Doubly Linked List
Intuition
Maintain a minFreq variable. Use one map for key -> node and another for freq -> DLL of nodes. When a key is accessed, move it from count DLL to count+1 DLL. If count DLL becomes empty and count == minFreq, increment minFreq. Eviction happens at freqMap[minFreq].tail.prev.
Insert Delete GetRandom O(1) - Duplicates allowed
Design a data structure that supports insert, remove, and getRandom in time, where duplicates are allowed.
Strategy
Combine a List of values (for random access) with a HashMap<Value, Set<Indices>> (for removal).
Examples
Level I: Simple List with Linear Scan
Intuition
Maintain a simple ArrayList to store values. For insert, append to the end. For remove, find the first occurrence using a linear scan and remove it. For getRandom, pick a random index in .
Detailed Dry Run
List: [1, 1, 2]. Remove(1) -> Find index 0 -> List: [1, 2]. getRandom() picks 1 or 2 with equal prob.
Level II: Map of Count + Randomized Index
Intuition
Maintain a Map<Value, Frequency> and a List<Value>. When removing, find the first occurrence in the List and replace it with the last element. This is for remove but for others.
Level III: HashMap of Sets + Array
Intuition
When removing an element, swap its last index from the Set with the last element in the Array. This allows deletion from an array by overwriting the target with the last item and popping.
All O(1) Data Structure
Design a data structure that supports the following operations in time:
Requirement
inc(key): Increments the count ofkeyby one.dec(key): Decrements the count ofkeyby one.getMaxKey(): Returns one of the keys with the maximum count.getMinKey(): Returns one of the keys with the minimum count.
Examples
Level I: Brute Force Map Scan
Intuition
Store keys and counts in a single HashMap<String, Integer>. For getMaxKey and getMinKey, iterate through the entire map to find the max/min. This is for updates but for queries.
Detailed Dry Run
Map: {'A': 2, 'B': 1}. getMaxKey() -> iterate -> find 'A'.
Level II: Map of Counts + Set of Keys (Lazy Removal)
Intuition
Use Map<String, Integer> keyCount and Map<Integer, Set<String>> countKeys. When incrementing, move the key from its current set to the set for count + 1. This is but slightly slower than the DLL version due to hash operations.
Level III: HashMap + Doubly Linked List of Sets
Intuition
Use a HashMap to store key -> count. Use a DLL where each node represents a specific count and contains a Set of all keys with that count. This allows movement between nodes when incrementing/decrementing.
Design Twitter
Design a simplified version of Twitter where users can post tweets, follow/unfollow others, and see the 10 most recent tweets in their news feed.
Core Operations
postTweet(userId, tweetId)getNewsFeed(userId)follow(followerId, followeeId)unfollow(followerId, followeeId)
Scaling Hint
Pull strategy: When a user requests their feed, merge the recent tweets from themselves and everyone they follow using a Max-Heap.
Examples
Level I: Map of Lists (Brute Force Sort)
Intuition
Store each user's tweets in a Map<UserId, List<TweetId>>. For getNewsFeed, collect all tweets from the user and their followees into a single list, sort them by timestamp, and return the top 10.
Detailed Dry Run
User 1 follows 2. User 1 tweets: [T1]. User 2 tweets: [T2]. Feed: Sort([T1, T2]) -> [T2, T1].
Level II: Global List + Filtering
Intuition
Maintain a single global list of all tweets. When getNewsFeed(userId) is called, iterate backwards through the list and pick the first 10 tweets that were posted by the user or their followees.
Level III: Map + Heap (Pull-based Feed)
Intuition
Keep a User class containing a Set of followees and a LinkedList of tweets. For getNewsFeed, push the first tweet of each user in the followee list into a Max-Heap (sorted by timestamp), and then standard merge-k-sorted-lists logic.
Design Phone Directory
Design a Phone Directory that supports get, check, and release operations for a fixed set of numbers.
Requirement
get(): Returns an available number or -1.check(num): Returns true if a number is available.release(num): Makes a number available again.
Examples
Level I: Boolean Array (Simple Scan)
Intuition
Use a boolean[] of size maxNumbers to track which numbers are used. For get, scan the array from the beginning to find the first false index. This is for get and for others.
Detailed Dry Run
Array: [F, F, F]. Get() -> finds index 0, sets to T. Array: [T, F, F].
Level II: BitSet (Space Optimized)
Intuition
Use a BitSet to track availability. Finding the next available number is in the bitset, but space is reduced dramatically.
Level III: Queue + HashSet
Intuition
Use a Queue to store available numbers (for get) and a HashSet to store current available numbers (for check). When release is called, add back to both if it wasn't already available.
Data Stream as Disjoint Intervals
Given a data stream input of non-negative integers , summarize the numbers seen so far as a list of disjoint intervals.
Requirement
addNum(val): Add integer to the stream.getIntervals(): Return the summary as a list of intervals.
Examples
Level I: List of Numbers + Rebuild on Query
Intuition
Store all individual numbers in a Set to handle duplicates. For getIntervals, convert the set to a sorted list and iterate through it to form intervals whenever numbers are not consecutive.
Detailed Dry Run
Input: 1, 3, 2. Set: {1, 2, 3}. Sorted: [1, 2, 3]. Intervals: [[1, 3]].
Level II: Sorted List (Manual Merging)
Intuition
Maintain a list of disjoint intervals sorted by start time. For addNum, find the insertion spot and check if it can merge with neighbor intervals. This is due to list shifting.
Level III: Balanced BST / TreeMap
Intuition
Use a TreeMap to store intervals as start -> end. When a new number x is added, find the interval that ends just before it and the one that starts just after it. Merge them if possible.
Range Sum Query 2D - Mutable
Given a 2D matrix, find the sum of the elements inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2). The matrix is mutable, meaning its values can be updated.
Requirement
NumMatrix(matrix): Initializes the data structure.update(row, col, val): Updates the value at(row, col)toval.sumRegion(row1, col1, row2, col2): Returns the sum of elements within the specified rectangle.
Examples
Level I: Row-wise Prefix Sums
Intuition
For each row, maintain a 1D prefix sum array. For sumRegion, iterate through each row in the range and compute the sum of that row's segment in using the prefix sums. This makes update and query .
Detailed Dry Run
Matrix: [[1, 2], [3, 4]]. Row1 Prefix: [1, 3]. Row2 Prefix: [3, 7]. SumRegion(0,0,1,1) = (3-0) + (7-0) = 10.
Level II: 2D Binary Indexed Tree (BIT)
Intuition
Extend the 1D BIT to 2D. Each node in the BIT stores a prefix sum. update and query both take time.
Level III: 2D Segment Tree (Quadtree based)
Intuition
Build a segment tree where each node represents a rectangular region. update and query take . Generally more powerful but harder to implement than BIT.
Snapshot Array
Implement a SnapshotArray that supports the following operations:
Requirement
SnapshotArray(length): Initializes an array-like data structure with the given length, where each element is initially 0.set(index, val): Sets the element at the givenindexto beval.snap(): Takes a snapshot of the array and returns thesnap_id.get(index, snap_id): Returns the value at the givenindex, corresponding to the givensnap_id.
Examples
Level I: Full Array Copy on Snap
Intuition
Every time snap() is called, we create a full copy of the current array and store it in a List<int[]>. This is for set and get, but for snap.
Detailed Dry Run
Array: [0, 0]. Set(0, 5). Snap() -> Store [5, 0]. Set(0, 6). Snap() -> Store [6, 0]. Get(0, 0) -> return 5.
Level II: Versioned Map (HashMap per index)
Intuition
Use Map<Integer, Integer>[] arr. Each index has a map mapping snap_id -> value. This is simpler but takes more space for small changes.
Level III: List of Lists (Binary Search Optimization)
Intuition
Each index stores a list of (snap_id, value) pairs. When get(index, snap_id) is called, perform a binary search (lower bound) on the list to find the value at or before that snap_id.
Design Bounded Blocking Queue
Implement a thread-safe bounded blocking queue. Submitting a task to a full queue should block the calling thread until a slot becomes available. Fetching a task from an empty queue should block the calling thread until a task becomes available.
Examples
Level I: Synchronized Methods with wait/notify
Intuition
Use synchronized on enqueue and dequeue. Inside each, use a while loop to wait() if the condition (full or empty) isn't met, and notifyAll() after making a change. This is the classic Java threading building block.
Detailed Dry Run
Thread 1: Enqueue(1). Thread 2: Enqueue(2). Thread 3: Enqueue(3) -> waits (full). Thread 4: Dequeue() -> returns 1, signals threads.
Level II: Semaphores
Intuition
Use Semaphore fill = new Semaphore(0) and Semaphore drain = new Semaphore(cap). enqueue calls drain.acquire() and fill.release(). dequeue does the reverse.
Level III: Condition Variables (Producer-Consumer)
Intuition
Use a ReentrantLock with two Condition variables (e.g., notEmpty, notFull). Threads wait on these conditions when the queue is empty or at capacity, and signal each other when appropriate.
Design In-Memory File System
Design a data structure that represents an in-memory file system. Support ls, mkdir, addContentToFile, and readContentFromFile.
Examples
Level I: Map of Full Paths
Intuition
Maintain a Map<String, String> where the key is the full path and the value is the content. For ls, iterate through keys to find children. This is simple but slow for deep trees.
Level III: Trie-based Tree Structure
Intuition
Model the file system as a tree (Trie), where each node is either a directory or a file. Directories contain a Map<String, Node> of children. This allows for efficient path traversal.
Design Browser History
You have a browser of one tab where you start on a homepage and you can visit another url, get back in the history number of steps or move forward in the history number of steps.
Level I: Two Stacks
Intuition
Maintain history and forward stacks. When visiting, clear forward stack.
Detailed Dry Run
Visit(A), Visit(B), Back(1) -> Back is at A.
Level III: Single Array with Pointer (Optimal)
Intuition
Use a single dynamic array (list) and an index pointer. Overwrite forward history on visit and simply move the index on back / forward.
Detailed Dry Run
Index starts at 0. Visit moves it to 1. Back(1) moves it to 0.
Design Food Rating System
Design a food rating system that can update ratings and return the highest-rated food for a specific cuisine.
Level III: Multi-Index with Sorted Sets (Optimal)
Intuition
Use two HashMaps: one to map food -> (cuisine, rating) and another to map cuisine -> SortedSet of (rating, food). SortedSet handles lexicographical tie-breaks.
Detailed Dry Run
Cuisine 'Japanese' has {Sushi: 5, Ramen: 4}. Update Ramen to 6 -> Highest Rated is now Ramen.
Design Movie Rental System
Design a movie rental system that supports searching for available movies, renting, dropping off, and reporting the cheapest rented movies.
Level III: Multi-Index Sorted Sets (Optimal)
Intuition
Use multiple data structures to track state: available[movie] -> SortedSet(price, shop), rented -> SortedSet(price, shop, movie), and priceMap[shop, movie] -> price.
Detailed Dry Run
Rent movie 1 at shop 2. Move (price, shop, movie) from available to rented set.
Found an issue or have a suggestion?
Help us improve! Report bugs or suggest new features on our Telegram group.