</> MAANG.io
coding interview · 301

Advanced

Advanced coding interview preparation covering complex algorithms, system design, and optimization techniques for senior-level positions.

0/95 solved 0% complete

Ranking Systems

What is this?

Both problems ask for "the current best" while the scores keep changing underneath. A heap answers "the best" perfectly and cannot update a key at all — so the resolution is counter-intuitive and the same in both: do not fix the heap. Push a new entry, leave the old one rotting, and discard stale entries when they surface at the top.

That works only because there is a second structure — a plain dictionary — that is always right, and every pop is checked against it.

flowchart TD A["score changes after insertion"] --> B["a heap cannot update a key"] B --> C["dict = the source of truth"] B --> D["heap = ordering only"] D --> E["edit: push a NEW entry"] E --> F["pop: does it match the dict?"] F -->|"no"| G["stale — discard, pop again"] F -->|"yes"| H["this is the real answer"]

💡 Fun fact: lazy deletion is not an interview shortcut — it is how Dijkstra's algorithm is written in practice. The textbook version calls for a decrease-key operation that Python's heapq does not have and that most real heaps make awkward. So every production implementation pushes the improved distance as a new entry and skips any popped entry whose distance no longer matches the current best. Same structure, same validity check, same reason. Recognising that these three problems are one pattern is worth more than any of them individually.

🔓 The 2 problems in this chapter are free. Sign in with Google or Microsoft to start solving.


The one-line idea: keep one structure that is always correct and one that is merely ordered. Never repair the ordered one — validate against the correct one at the moment you read.


1. The two structures

self.info = {}                              # food/task id → the CURRENT value. authoritative.
self.heap = defaultdict(list)               # cuisine → [(-rating, name), …]. ordering only.

def change(self, key, new_value):
    self.info[key] = (new_value, group)     # O(1) — the truth is updated here
    heappush(self.heap[group], (-new_value, key))     # O(log n) — a second, newer entry

change never searches the heap and never removes anything from it. The heap may end up holding several entries per key, all but one obsolete, and that is by design — the cost is bounded by the number of operations, not by the number of live items.

2. The validity check

def best(self, group):
    h = self.heap[group]
    while h:
        neg_rating, key = h[0]
        if self.info[key][0] == -neg_rating:          # the dict agrees → genuine
            return key
        heappop(h)                                     # stale → discard and look again

Two details decide whether this is correct. Peek before popping: the valid top must stay in the heap for the next call. And the comparison must be against the authority, not against a remembered value — a key whose rating changed and changed back is still valid, and a check based on "has this been edited" would wrongly reject it.

For Task Manager, remove deletes the key from the dict, so the validity check must also handle a key that is absent entirely — self.info.get(key) returning None is a stale entry, not a crash.

3. Tie-breaks ride in the key

Neither problem breaks ties by insertion order:

  • Food ratings want the lexicographically smallest name, which (-rating, name) gives directly, since Python compares tuples left to right.
  • Task Manager wants the largest task id, which needs (-priority, -taskId) — negating both components so a min-heap yields the maximum of each.

Getting the tie-break into the tuple is what keeps best free of comparison logic. A tie-break handled after the pop is a tie-break that is wrong on the boundary.


4. A 30-second worked example (stale entries)

Tasks [[1,101,10], [2,102,20], [3,103,15]], then edit(102, 8).

dict:  101→(10,u1)   102→(8,u2)   103→(15,u3)          ← authoritative
heap:  (-20,-102)  (-15,-103)  (-10,-101)  (-8,-102)   ← two entries for 102

execTop():
  peek (-20,-102)   dict says 102 has priority 8, not 20   → stale, pop
  peek (-15,-103)   dict says 103 has priority 15          → match, return user 3

The stale (-20, -102) sits at the very top of the heap and is exactly the wrong answer — it is the entry an implementation without the validity check would return. Notice too that it is only discarded when it surfaces: had execTop never been called, the cleanup would never have happened, and nothing would have been wrong.


5. Where you'll actually meet this

  • Schedulers. Cluster and OS schedulers pop the highest-priority job while priorities are renice'd and jobs cancelled underneath.
  • Job queues. Celery, Sidekiq and every delayed-job system allow reprioritising and cancelling before execution.
  • Shortest paths. Dijkstra and A* with lazy deletion instead of decrease-key is the standard implementation.
  • Event simulation. Timer wheels validate a popped event against the live timer table before firing it.
  • Leaderboards. Live rankings where scores stream in continuously and only the top is ever read.

6. Problems in this chapter

▶ Design a Food Rating System

Highest-rated food per cuisine, ties by lexicographically smallest name, with ratings changing over time. Per-cuisine heap plus an authoritative dictionary.
Pattern: heap + lazy deletion. Target: O(log n) per operation.

▶ Design Task Manager

Add, edit, remove and execute the highest-priority task, ties by largest task id. A dictionary is the truth; the heap only orders, and stale entries are skipped on pop.
Pattern: heap + lazy deletion with removal. Target: O(log n) amortised per operation.


7. Common pitfalls 🚫

  • Trying to update inside the heap. There is no such operation; that is the whole reason for this design.
  • Popping instead of peeking when the top is valid, which loses it for the next call.
  • Validating against a remembered value rather than the dictionary — a value that changed and changed back is still valid.
  • Not handling a deleted key in the validity check; an absent entry is stale, not an error.
  • Breaking ties after the pop instead of encoding them in the heap key.
  • Forgetting to negate both components for a max-heap on two fields.
  • Claiming O(log n) without qualification. It is amortised — one call can discard many stale entries, but each was pushed once.

8. Key takeaways

  1. One structure is the truth; the other is only an order. Never confuse their roles.
  2. Lazy deletion beats repairing the heap — and it is what real schedulers do.
  3. Validate at read time, against the authority, every time.
  4. Peek, then pop only if stale. A valid top must survive the call.
  5. Tie-breaks belong in the heap key, negated as needed for a min-heap.
  6. Say "amortised". The bound is real but it is not per-operation worst case.
  7. Why interviewers like it: the naive design is obvious and impossible, so the conversation goes straight to the trade-off that makes it work.

Order: Design a Food Rating System → Design Task Manager.

Design Task Manager

Core idea: Keep two structures that play different roles. A dict taskId → (priority, userId) is the source of truthedit and remove just touch one dict entry in O(1). A max-heap of (priority, taskId) only gives you ordering, and you never try to delete or update inside it. Instead you use lazy deletion: edit pushes a new heap entry, remove only updates the dict, and execTop pops from the heap and skips any entry that no longer matches the dict — reconciling stale entries one at a time, exactly when you need the top.


Problem, rephrased

Forget the textbook phrasing. Here's the scenario:

You're running the dispatcher for a shared compute cluster. Every user can submit jobs, and each job carries a priority. At any moment an idle worker asks "what's the single most important job right now?" — and you must hand it the highest-priority job, break ties by the largest job id, mark it as running (removed), and tell the dispatcher which user it belonged to. Meanwhile users keep submitting new jobs, bumping the priority of jobs already queued, and cancelling jobs that no longer matter.

Implement a TaskManager supporting:

  • TaskManager(tasks) — initialize from a list of [userId, taskId, priority] triples.
  • add(userId, taskId, priority) — add a new task (taskId is guaranteed not to exist yet).
  • edit(taskId, newPriority) — change an existing task's priority.
  • remove(taskId) — delete an existing task.
  • execTop() — find the task with the highest priority (ties broken by highest taskId), remove it, and return its userId. If no tasks remain, return -1.

A single user may own many tasks. Let n be the number of live tasks and m the total number of operations.

Operation sequence (tasks=[[1,101,10],[2,102,20],[3,103,15]]) Returns Why
add(4,104,5) task 104 (prio 5) added for user 4
edit(102,8) task 102's priority drops 20 → 8
execTop() 3 highest is 103 (prio 15) → user 3, removed
remove(101) task 101 deleted
add(5,105,15) task 105 (prio 15) added for user 5
execTop() 5 top is 105 (prio 15); ties with nothing → user 5, removed

That last execTop is the whole problem in miniature: after the edit, the heap still holds a stale (20, 102) entry. We must not return it — the dict says 102 is now priority 8.


Sign in to continue reading

The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.

Sign in to read the full lesson

Sign in to MAANG.io

Use your Google or Microsoft account — no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.