# 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.
All operations must run in average time complexity.
Examples
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 |
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 |
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:
move(row, col, player) must run in time complexity.
Examples
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) |
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 |
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
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 |
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 |
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
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)
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 |
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
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.
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.
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.
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 |
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.
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.
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 |
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.
addWord: where is word length.search: in worst case due to wildcards, but typically fast with Trie pruning.Examples
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.
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.
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.
checkIn(id, stationName, t)checkOut(id, stationName, t)getAverageTime(startStation, endStation)Examples
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.
Intuition
Use simple string concatenation start+"->"+end as keys in a single HashMap. Store lists of travel durations and compute averages on the fly.
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.
Examples
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.
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.
Examples
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).
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.
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.
Combine a List of values (for random access) with a HashMap<Value, Set<Indices>> (for removal).
Examples
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.
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.
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:
inc(key): Increments the count of key by one.dec(key): Decrements the count of key by one.getMaxKey(): Returns one of the keys with the maximum count.getMinKey(): Returns one of the keys with the minimum count.Examples
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'.
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.
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.
postTweet(userId, tweetId)getNewsFeed(userId)follow(followerId, followeeId)unfollow(followerId, followeeId)Pull strategy: When a user requests their feed, merge the recent tweets from themselves and everyone they follow using a Max-Heap.
Examples
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].
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.
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.
get(): Returns an available number or -1.check(num): Returns true if a number is available.release(num): Makes a number available again.Examples
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].
Intuition
Use a BitSet to track availability. Finding the next available number is in the bitset, but space is reduced dramatically.
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.
addNum(val): Add integer to the stream.getIntervals(): Return the summary as a list of intervals.Examples
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]].
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.
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.
NumMatrix(matrix): Initializes the data structure.update(row, col, val): Updates the value at (row, col) to val.sumRegion(row1, col1, row2, col2): Returns the sum of elements within the specified rectangle.Examples
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.
Intuition
Extend the 1D BIT to 2D. Each node in the BIT stores a prefix sum. update and query both take time.
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:
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 given index to be val.snap(): Takes a snapshot of the array and returns the snap_id.get(index, snap_id): Returns the value at the given index, corresponding to the given snap_id.Examples
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.
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.
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
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.
Intuition
Use Semaphore fill = new Semaphore(0) and Semaphore drain = new Semaphore(cap). enqueue calls drain.acquire() and fill.release(). dequeue does the reverse.
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
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.
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.
Intuition
Maintain history and forward stacks. When visiting, clear forward stack.
Detailed Dry Run
Visit(A), Visit(B), Back(1) -> Back is at A.
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.
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.
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.
Help us improve! Report bugs or suggest new features on our Telegram group.