37

Design Data Structures

Clarify the API, fix each method's cost, write the invariant — then snap HashMaps, arrays, stacks, TreeMaps and tries together so every call hits its target.

advanced~80 min4 simulations5 problems

▶ guided lesson⚡ 1-minute summary↻ 13 recall cards

before this: Java OOP · Hashing · Linked Lists · Stacks · Binary Search · BST · Tries

01 · why it exists

Why this exists, and the weight it carries

since1974

Design from the API (abstract data types)

Barbara Liskov (MIT) & Stephen Zilles (IBM)

In 1974 Barbara Liskov and Stephen Zilles published "Programming with Abstract Data Types" at an ACM SIGPLAN symposium. Their idea: describe a data type by the operations its users may call, and keep its representation hidden inside. Liskov's 2008 Turing Award citation honours her contributions to programming language and system design, "especially related to data abstraction, fault tolerance, and distributed computing". Every Java interface you use — List, Map, Deque — is this idea: one API, several representations behind it, each with its own costs.

the problem it solvedCode that reaches into a structure's fields everywhere can't change the structure without breaking. Fixing the operations first lets the inside change freely.

its jobs today

  • Java's Collections Framework: Map is the API; HashMap and TreeMap are two representations with different costs per method.
  • Every "Design X" interview question: the class and its methods are fixed, and you choose what's inside so each method meets its cost.
  • Service APIs: callers depend only on the methods, so the storage behind them can change — a list today, an index tomorrow.

the weight it carries

Illustrative: LeetCode allows 2·10⁵ calls on a RandomizedSet — say 10⁵ inserts, then 10⁵ removes. If each remove scans and shifts a list of up to 10⁵ values, that is up to 10⁵ × 10⁵ = 10¹⁰ steps — far too slow. With every method O(1) on average it is 2·10⁵ calls × a few operations ≈ 10⁶ steps.

without it

Without fixing the API and the per-method costs first, you polish one method and discover at the end that another one is O(n) — the most common way design rounds go wrong.

when · why · how

when
Any question that says "design" or "implement the class" with several methods.
why
The slowest method decides the total: n calls × the worst per-call cost. Choosing a structure per method keeps every call inside its budget.
how
Write the class skeleton with each method's target cost as a comment, then the fields, then the invariant — and only then the method bodies.

02 · plain english

The idea in 30 seconds

A design question hands you an API (a class and its methods) and a cost for each method. Your job: choose the fields so that every method meets its cost.

You rarely invent anything. You compose pieces you already know: a HashMap to find, an array to pick by index, a stack for newest first, a TreeMap for nearest ≤ x, a trie for paths.

The glue is an invariant: one sentence about the fields that is true after every call. A method may break it for a moment, but restores it before it returns.

picture this

A database table with indexes. The table holds the rows, and you add one index for each query that must be fast; every insert or delete updates the table and all its indexes together. A design answer is the same: one store, one helper structure per fast method, all kept in sync.

03 · remember

What you must know

  • The routine: state the API → write each method's target cost → pick one structure per method's question → write the invariant over the fields → code method by method → dry-run a script of calls.
  • One question, one structure: find by key → HashMap · pick by position or at random → ArrayList · newest first → stack · oldest first → queue · smallest or largest → heap · nearest ≤ x, sorted order → TreeMap or a sorted list + binary search · by path or prefix → trie.
  • Swap with the last: an array can drop any slot in O(1) when order doesn't matter. Copy the last element into the hole, update its index in the map, remove the end.
  • Group by a changing count: keep value → count plus count → stack of values. A count moves by exactly 1 per call, so the top group is tracked with one integer, never searched.
  • Append-only history is sorted for free: when timestamps only grow, appending keeps each key's list sorted, and "value at time t" is a binary search for the floor.
  • Paths are tries of names: split /a/b/c on / and walk a tree of nodes, each mapping a name to a child. TreeMap children hand ls its names already sorted.
  • Count, don't copy: store item → how many copies and loop over a small candidate set (one column of points) instead of every stored item.
  • Say each method's cost precisely: HashMap is O(1) average, ArrayList.add is O(1) amortized, TreeMap is O(log n) worst case.
  • LRU Cache, Min Stack, Design Twitter, Implement Trie, Design Circular Queue and Design HashSet live in their own chapters. Practise them here as design rounds (see Also practise this here).

04 · see it

Play with it in 3D

Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.

sim

Insert Delete GetRandom O(1): swap with the last

Top: the ArrayList. Bottom: the HashMap from value to index. A remove copies the last value into the hole, fixes its index and drops the end — nothing shifts.

loading simulation…
sim

Maximum Frequency Stack: one stack per count

A value pushed f times sits once in each of groups 1…f. pop takes the top of the highest group: the most frequent value, and among ties the newest.

loading simulation…
sim

Time Based Key-Value Store: binary search for the floor

Each key's times grow, so its list is sorted for free. get(key, t) narrows lo..hi to the last time ≤ t.

loading simulation…
sim

In-memory file system: walk a tree of names

Folders are boxes, files are spheres. Every call splits its path on "/" and takes one TreeMap step per name; missing folders are created on the way.

loading simulation…

05 · cost

Time & space, explained

Which piece answers which question fast (n stored items)
find by keyadd / removepick at randomnearest ≤ xwhy
HashMap / HashSetO(1) avgO(1) avgO(n)O(n)Hashing jumps straight to one bucket, but a hash table keeps no order and no index — random picks and floor lookups must walk it.
ArrayList (any order)O(n)add O(1) amortized · remove O(n), or O(1) by swapping with the lastO(1)O(n)Index access is one address calculation; finding a value is a scan. remove(i) shifts the tail unless you fill the hole with the last element.
Sorted ArrayList + binary searchO(log n)O(n) to keep it sorted · O(1) amortized if keys only growO(1)O(log n)Binary search halves the range each step; inserting in the middle shifts the tail — unless every new key is the largest, as with timestamps.
TreeMapO(log n)O(log n)O(n)O(log n)A red-black tree stays balanced, so every lookup, insert and floor/ceiling query walks one root-to-leaf path.
ArrayDeque as a stackO(n)O(1) at the top—O(n)Only the top is reachable in O(1); anything else is a walk.
This chapter's designs: the slow method vs the designed one
First ideaDesignedwhy
RandomizedSet.removeO(n) — find, then shift (ArrayList only)O(1) averageThe map gives the index; the last element fills the hole, so nothing shifts.
RandomizedSet.getRandomO(n) — walk a HashSetO(1)values.get(random.nextInt(size)) is one index access.
FreqStack.popO(n) scan · O(log n) heapO(1) averageThe answer is always the top of group maxFreq, and maxFreq moves by one.
TimeMap.get (v versions of the key)O(v) — scan backO(log v)The history is sorted by time, so the floor is a binary search.
FileSystem.ls (P stored paths, d names in the path, k listed)O(P · L) — scan every path stringO(d · log c + k)The tree walks d levels, each one TreeMap lookup among c siblings, then copies k sorted names.
DetectSquares.count (n points added)O(n²) — scan pairsO(c), c ≤ 1,001Only points in the query's column can be a corner, and y ≤ 1000 limits that column to 1,001 different values.

06 · java

Raw vs Collections Framework

Delete from the middle in O(1): fill the hole with the last element

javaint[] + size: copy the last live element into the holejava.util.*
/** Removes data[i] from the live part data[0..size-1] in O(1). Order is not kept. Returns the new size. */static int removeAt(int[] data, int size, int i) {    data[i] = data[size - 1]; // the last live element fills the hole    return size - 1;          // and the old last slot is simply outside the live part now}
  • list.remove(i) shifts every element after i: O(n). list.remove(list.size() - 1) shifts nothing: O(1).
  • The price is the order: the last element jumps into the middle. Use it for sets and pools, not for playlists or queues.
  • The moved element's index changed, so update the map. Remove the deleted value from the map last — then deleting the last element works too.

Value at time t: the floor of t in a sorted history

javabinary search: last index with times[i] ≤ tjava.util.*
/** Index of the last time ≤ t in a sorted array, or -1 if every time is later. O(log n). */static int floorIndex(int[] times, int t) {    int lo = 0, hi = times.length - 1, found = -1;    while (lo <= hi) {        int mid = (lo + hi) >>> 1;        if (times[mid] <= t) {            found = mid;  // fits; look right for a later one            lo = mid + 1;        } else {            hi = mid - 1; // too late; look left        }    }    return found;}
  • Keep a found variable: record mid when it fits, then keep going right. You never have to reason about where lo stops.
  • floorEntry(t) returns the entry with the largest key ≤ t, or null. floorKey(t) returns just the key.
  • Prefer the list when writes arrive in time order (append is O(1) amortized and memory is compact); use TreeMap when they don't.
  • (lo + hi) >>> 1 never overflows; (lo + hi) / 2 can for indices near 2³¹.

Group values by a count that changes

javaList<Deque<Integer>> indexed by the countjava.util.*
/** levels.get(f) = values that reached count f, newest last. Counts are 1, 2, 3…, so a list index works. */static void addToLevel(List<Deque<Integer>> levels, int f, int value) {    while (levels.size() <= f) levels.add(new ArrayDeque<>()); // grow up to level f    levels.get(f).push(value);}
  • Counts are 1, 2, 3… with no gaps, so a list indexed by the count works as well as a map.
  • computeIfAbsent(f, k -> new ArrayDeque<>()) returns the existing stack or creates it — no null checks.
  • Use ArrayDeque for the stacks: push, pop and peek work at the front in O(1). The legacy Stack class is synchronized and slower.

Splitting a path into names

javaby hand: scan for '/'java.util.*
/** "/home/priya/code" → [home, priya, code], reading the characters by hand. */static List<String> namesByHand(String path) {    List<String> names = new ArrayList<>();    int start = 1; // skip the leading '/'    for (int i = 1; i <= path.length(); i++) {        if (i == path.length() || path.charAt(i) == '/') {            if (i > start) names.add(path.substring(start, i));            start = i + 1;        }    }    return names;}
  • "/a/b".split("/") is ["", "a", "b"] and "/".split("/") is an empty array. Always skip empty names.
  • split takes a regular expression. "/" is safe, but a dot is special: split on "\\.", not ".".
  • Scanning by hand avoids building an array; in an interview, split is clearer.
Java API you will actually use
calldoescost
map.get(k) / map.containsKey(k)Find a key's value (null if absent) / test presenceO(1) average
map.merge(k, 1, Integer::sum)Add 1 to k's count (creating it at 1) and return the new countO(1) average
map.computeIfAbsent(k, x -> new ArrayList<>())Return k's collection, creating it on first useO(1) average
list.set(i, x) / list.get(i)Overwrite / read slot iO(1)
list.remove(list.size() - 1)Remove the last slot; nothing shiftsO(1)
list.remove(i) / list.remove(Integer.valueOf(x))Remove by index / by value; shifts everything after itO(n)
random.nextInt(n)A uniformly random int in [0, n)O(1)
treeMap.floorEntry(t) / floorKey(t)Entry / key with the largest key ≤ t, or nullO(log n)
treeMap.keySet()The keys in sorted order (a view; copy it before returning)O(k) to iterate k keys
Collections.binarySearch(list, key)Index of key, or −(insertion point) − 1 if absent; needs a sorted RandomAccess listO(log n)
deque.push(x) / deque.pop() / deque.peek()Stack operations on an ArrayDeque (front = top)O(1)
path.split("/")The parts between slashes; a leading "/" gives an empty first partO(length)
sb.append(s)Grow a StringBuilder (a file's content) without copying it every timeO(|s|) amortized

Insert, delete and pick at random, all O(1)

→ ArrayList + HashMap value → index (swap-with-last delete)

The list gives random access by index; the map finds the slot to delete without a scan.

Pop the most frequent value, ties to the newest

→ HashMap value → count + HashMap count → ArrayDeque + int maxFreq

Counts move by ±1, so the top group is always maxFreq; no heap needed.

Value at time t, writes in time order

→ HashMap key → two ArrayLists (times, values) + binary search

Appending keeps the list sorted: set O(1) amortized, get O(log v), compact memory.

Value at time t, writes in any order (or deletes)

→ HashMap key → TreeMap<Integer, String> with floorEntry

The tree keeps order on every insert in O(log v).

Paths, folders, nested names

→ Trie node with TreeMap<String, Node> children

One lookup per level; TreeMap returns ls already sorted. (HashMap + sort if ls is rare.)

Many identical items, count-based answers

→ HashMap<K, Integer> with merge (a multiset)

k copies become one entry and a factor of k; loops run over distinct items only.

O(1) get and put with least-recently-used eviction

→ HashMap + doubly linked list (or LinkedHashMap in access order)

The map finds the node; the list reorders and evicts in O(1). See LRU Cache.

A stack that reports its minimum in O(1)

→ ArrayDeque of {value, min so far}

Each entry remembers the minimum below it, so pop needs no recomputation. See Min Stack.

07 · recognise

Spot the pattern

"insert, delete and getRandom, all in O(1)"

HashMap value → index + ArrayList; delete by swapping with the last element

"pop the most frequent; on a tie, the most recent"

count map + map from count to a stack, plus maxFreq

"value at timestamp t", "latest version ≤ t", "snapshot"

Per-key history sorted by time + floor search (binary search or TreeMap.floorEntry)

"mkdir", "ls", paths like /a/b/c, nested names

Trie of names: each node maps a name to a child; TreeMap children for sorted output

"count the squares/rectangles formed with stored points", "duplicates count separately"

Count identical points; loop over the few points that can be a partner (same column or row)

"O(1) get and put with eviction of the least recently used"

HashMap + doubly linked list (LRU Cache, linked-lists chapter)

"Design X" / "implement the class" with several methods and per-call targets

The routine: API → cost per method → one structure per question → invariant → script test

08 · traps & tricks

Mistakes to avoid, tricks to use

traps

  • Swap-with-last: forgetting indexOf.put(last, i) — the moved element keeps a stale index, and a later remove deletes the wrong slot.
  • Swap-with-last: calling indexOf.remove(val) before indexOf.put(last, i). When val is the last element, the put brings it back.
  • list.remove(val) on a List<Integer> with an int removes by index. Use the index from your map, or list.remove(Integer.valueOf(val)) when you really mean a value.
  • Comparing two Integers from a map with ==: equal values above 127 are usually different objects, so == says false. Unbox first (int x = map.get(k)) or use .equals.
  • Frequency stack: forgetting to decrease the popped value's count, or scanning for a new maximum when maxFreq - 1 is always right.
  • Time map: searching for the first time ≥ t (ceiling) instead of the last time ≤ t (floor), or returning null instead of "".
  • File system: not skipping the empty strings from split("/"), and overwriting a file's content instead of appending.
  • Returning an internal list or keySet() from a method lets the caller change your state. Return a copy: new ArrayList<>(…).

tricks

  • Open by writing the API: class name, each method's signature and its target cost. Then ask: value ranges? duplicates? how many calls? do timestamps only grow? thread safety?
  • Write the invariant as a comment above your fields before any method body, and check every method against it.
  • Name the brute force first ("one list makes remove O(n)"), then say which piece fixes which method. That shows the interviewer your reasoning, not just the answer.
  • After coding, dry-run a short script that hits the edges: an empty structure, removing the last element, duplicates, a tie.
  • Say average for HashMap and amortized for ArrayList.add — precise words build trust.
  • Prefer two simple structures kept in sync over one clever structure. Clear code is part of the grade — bas itna yaad rakho: one question, one structure, one invariant.
  • LeetCode gives design tests as two arrays — method names and arguments. Read them as a script and replay it by hand.

09 · why & how

Questions you might be too shy to ask

How do I know which structures to combine?

List the methods and the question each one must answer fast: find by key, pick by index, newest, smallest, nearest ≤ x, by path. Each question has a standard piece (see the table). Take those pieces, then write down what must stay in sync between them — that is your invariant, and it tells you what every update must touch.

Is HashMap really O(1)?

On average, with a decent hash, yes: each call looks at one bucket. In the worst case many keys share a bucket. Since Java 8 a bucket that grows large is turned into a balanced tree (JEP 180), so even that case is O(log n) for keys that are Comparable. In an interview, say "O(1) average".

What does "O(1) amortized" mean for ArrayList.add?

Most adds just write into a free slot: O(1). Once in a while the backing array is full, so Java copies everything into a bigger one: that one add is O(n). (OpenJDK grows it to about 1.5× — an implementation detail; the spec only promises amortized O(1).) The array grows by a fixed factor, so copies are rare, and spread over many adds the cost per add is still O(1). Amortized = "cheap on average over a long run of calls", with a guarantee, not luck.

LeetCode gives me a class with no main. Where do my fields go, and who calls the methods?

Declare the fields inside the class, outside every method, and set them up where they're declared or in the constructor. For each test case, the judge does new RandomizedSet() once, then calls your methods in the order of the test script. Every call sees the same fields, so whatever one call stores, the next call can read. That shared state is the whole design.

The fields are private final, but I keep changing the map. How?

final only locks the reference: the field always points to the same HashMap object. The contents of that map can still change freely with put and remove. So final stops one bug (accidentally writing freq = new HashMap<>() somewhere) and costs nothing.

Why does getRandom need an array? Can't a HashSet do it?

A set has no index. To return its k-th element you must walk k entries: O(n). An array answers "element at index r" directly, so values.get(random.nextInt(values.size())) is a fair pick in O(1).

Swap-with-last changes the order. Is that allowed?

Only when order doesn't matter, as in a set or a pool of servers. If order matters (a playlist, a queue), you need a linked list or a balanced tree instead, and you pay for it elsewhere.

TreeMap or binary search on a list for the time map?

If timestamps only grow (LC 981 promises it), appending keeps a list sorted: set is O(1) amortized and get is O(log n), with less memory. If timestamps can arrive out of order, or old versions get deleted, use a TreeMap: O(log n) for both.

In Maximum Frequency Stack one value sits in many groups at once. Isn't that wasting memory?

No. Each push adds exactly one entry (to group f) and each pop removes exactly one. So the groups together hold exactly as many entries as the stack has elements: O(n) memory, same as a plain stack. A value pushed 3 times sits once in groups 1, 2 and 3: three entries for three copies.

For LRU Cache, why a doubly linked list? Why not a singly linked one?

On get, the map hands you the node, and you must unlink it from the middle of the list. Unlinking needs the node before it. A doubly linked node has prev, so that's O(1). A singly linked list would have to walk from the head to find the previous node: O(n), and the O(1) promise is gone.

Can I just use LinkedHashMap for LRU Cache in an interview?

Mention it, it shows you know Java: new LinkedHashMap<>(capacity, 0.75f, true) keeps entries in access order, and overriding removeEldestEntry to return size() > capacity evicts the oldest. But ask first. Most interviewers then say "now build it yourself", because the HashMap + doubly linked list is what they are testing.

What if the interviewer asks for thread safety?

Say how you would do it: the simplest is synchronized on every public method, because your methods change two structures that must stay consistent together. A ConcurrentHashMap alone doesn't keep a map and a list in sync. Don't add locks unless asked.

Where does LFU Cache (LC 460) fit?

It combines this chapter's ideas: a count per key, a group per count (each group in least-recently-used order) and the minimum count. Build it after Maximum Frequency Stack here and LRU Cache in the linked-lists chapter.

My design works but the code is long. Is that bad?

Not if each method is short and does one thing. Pull a repeated walk or lookup into one private helper (like walk(path) in the file system), name fields by their meaning (indexOf, maxFreq), and the length stops mattering.

10 · practice

Interview problems

Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.

  1. #380
    Insert Delete GetRandom O(1)must-do
    MediumBloombergUberLinkedInByteDance / TikTokApple+1
  2. #981
    Time Based Key-Value Storemust-do
    MediumNetflixAppleUberAmazonMicrosoft+1
  3. #2013
    Detect Squares
    MediumGoogleAmazon
  4. #895
    Maximum Frequency Stackmust-do
    HardAmazonMicrosoftGoogle
  5. #588
    Design In-Memory File SystemLeetCode Premium
    HardUberAmazonMicrosoftGoogle

Also practise this here

Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.

  1. #146
    LRU Cache
    Mediumin Linked Lists

    The most-asked design round. get and put must be O(1), so a HashMap finds the node and a doubly linked list keeps recency order — two structures, one invariant.

  2. #155
    Min Stack
    Mediumin Stacks

    Augment instead of search: each entry stores the minimum below it, so getMin never scans — the same move as keeping maxFreq here.

  3. #355
    Design Twitter
    Mediumin Heaps

    Compose per-user pieces (a follow set, a tweet list) and merge them on read with a heap: a design that moves the expensive work to the query.

  4. #208
    Implement Trie (Prefix Tree)
    Mediumin Tries

    The node behind this chapter's file system: a children map plus an end flag. Here the edges are whole names instead of letters.

  5. #622
    Design Circular Queue
    Mediumin Queues

    Fixed memory, O(1) per call: an array with head + size, where every other index is derived. The invariant does the work.

  6. #705
    Design HashSet
    Easyin Java OOP

    Build the hash table itself (buckets + chaining). Knowing why it is O(1) on average is what lets you promise O(1) in every design here.

  7. #528
    Random Pick with Weight
    Mediumin Math & Numbers

    getRandom's weighted cousin: prefix sums and a binary search turn a random ticket into an index.

  8. #729
    My Calendar I
    Mediumin BST

    TreeMap floor and ceiling inside a design class — the time map's lookup, used to reject overlapping bookings.

11 · revise

The whole chapter on one screen

10 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.

  1. what it is

    A design question = an API + a cost per method. Pick one structure per method's question, glue them with an invariant, keep them in sync on every call.

  2. use it when

    "Design X" or "implement the class" with methods like insert, get, pop, ls — and targets like O(1) or O(log n) per call.

  3. template

    Routine: API → cost per method → structures → invariant as a comment → code each method → dry-run a script of calls.

  4. template

    O(1) delete: values.set(i, last); indexOf.put(last, i); values.remove(size - 1); indexOf.remove(val);

  5. cost

    RandomizedSet O(1) avg · FreqStack O(1) avg · TimeMap set O(1) amortized, get O(log v) · file system O(depth · log c) · DetectSquares O(one column).

  6. trap

    Swap-with-last: update the moved element's index, and remove val from the map last — or deleting the last element breaks.

  7. trap

    list.remove(5) on a List<Integer> removes index 5, not the value 5. And "/a".split("/") starts with an empty string.

  8. pro tip

    Most frequent, newest wins → count → stack groups + a maxFreq that moves by ±1. Value at time t → floor search in a time-sorted history.

  9. pro tip

    Paths → a trie of names (TreeMap children give sorted ls). Shapes from stored points → count copies, loop over one column.

  10. pro tip

    Say average for HashMap and amortized for ArrayList.add. Then dry-run: empty, delete the last element, duplicates, a tie.

12 · recall

Flip, remember, repeat

card 1 / 13
recall

What are the steps of the design-round routine?

tap or Space to flip ↻

Question 1: What are the steps of the design-round routine?

13 · check yourself

Quick quiz

  1. values = [10, 40, 30, 20] and indexOf maps each value to its index. You remove 40 by swapping with the last. What is values now, and what is indexOf.get(20)?

  2. In remove(val), why call indexOf.remove(val) after indexOf.put(last, i)?

  3. Why can't a HashSet alone give getRandom in O(1)?

  4. FreqStack after push(4), push(6), push(6), push(4). What does pop() return?

  5. In FreqStack.pop, group[maxFreq] becomes empty. What should happen to maxFreq?

  6. TimeMap: set(k, "a", 2), set(k, "b", 5), set(k, "c", 9). What does get(k, 8) return?

  7. Why can LC 981 keep each key's history in a plain ArrayList and still binary-search it?

  8. What does `"/".split("/")` return in Java?

  9. DetectSquares holds (3,10), (11,2), (3,2) and a second (11,2). What is count([11,10])?

  10. In a design round, what should you write before any method body?