3.2.9 · D4Linear Data Structures

Exercises — Priority Queue — concept (heap-based implementation covered later)

3,431 words16 min readBack to topic

Throughout, we use a min-priority queue unless a problem says max. Reminder of the vocabulary (all from the parent):

To picture the state of a queue I will write its contents as a set like {A:5, B:2} meaning "element A has priority 5, element B has priority 2". Order inside the braces does not matter — a priority queue never promises the rest is sorted, only that the top is correct (see the [!mistake] on full-sorting in the parent).

To see the two core operations as pictures before we drill them, here is what an insert and an extract actually look like on the Heap / Binary Tree we build later — insert bubbles up, extract sinks down:

Figure — Priority Queue — concept (heap-based implementation covered later)

Level 1 — Recognition

L1.1 — Which tool?

You are told: "Repeatedly pull out the currently most-urgent job, where new jobs keep arriving." Which data structure fits: Stack (LIFO), Queue (FIFO), or Priority Queue? Give the one-sentence reason.

Recall Solution — L1.1

Priority Queue. A Stack (LIFO) returns the most recently added item; a Queue (FIFO) returns the earliest added item. Neither looks at urgency. The phrase "most-urgent job" means removal order depends on a priority number, not on arrival time — that is exactly the priority-queue contract.

L1.2 — Read the contract

A min-PQ currently holds {tea:7, milk:3, ice:9}. What does peek() return, and does the queue change afterward?

Recall Solution — L1.2

peek() returns milk (priority 3, the smallest). The queue is unchangedpeek only looks. The contents stay {tea:7, milk:3, ice:9}. Only extract-min would remove milk. (If the queue had been empty, peek() would have nothing to return — see the empty-queue [!mistake] at the top.)

L1.3 — Min or Max?

In Huffman Coding you repeatedly merge the two least-frequent symbols. Do you want a min-PQ or a max-PQ, and what is the priority number?

Recall Solution — L1.3

A min-PQ, with frequency as the priority. "Least-frequent" = smallest frequency number = smallest priority = what extract-min hands back. Two extract-min calls give you the two rarest symbols to merge.


Level 2 — Application

L2.1 — Trace a min-PQ

Run these on an initially empty min-PQ, then perform three extract-min calls. List what comes out, in order. insert(A,5), insert(B,1), insert(C,4), insert(D,1), insert(E,9).

Recall Solution — L2.1

After all inserts: {A:5, B:1, C:4, D:1, E:9}.

  • extract-min → smallest priority is 1, held by both B and D. Standard PQs are not stable (parent's third [!mistake]), so a tie may return either. We'll assume insertion-order tie-break gives B(1) first. State: {A:5, C:4, D:1, E:9}.
  • extract-minD(1). State: {A:5, C:4, E:9}.
  • extract-min → smallest is C(4). State: {A:5, E:9}.

Out (in order): B, D, C. Note E(9), inserted last, is nowhere near leaving — priority beats arrival.

L2.2 — Max via min (the negation trick)

You only own a min-PQ but you want the largest price out first. Prices arrive: 40, 10, 70, 25. Show what you insert, then what the first extract-min yields after un-negating.

Recall Solution — L2.2

Insert each price p as -p (parent Example 2): insert , , , . State (by priority): {-70, -40, -25, -10}. extract-min returns the smallest stored number = . Un-negate: . Result: 70, which is indeed the largest original price. Why it works: the most-negative stored value corresponds to the most-positive real value.

L2.3 — Top-2 with a size cap

Stream: 8, 2, 10, 6, 4. Keep a min-PQ of size ≤ 2 (extract-min whenever size hits 3). What two numbers remain at the end?

Recall Solution — L2.3

Mirror the parent's Example 3 technique — the tiny min-PQ throws away the smallest whenever it overflows, so the two largest survive. (We only ever extract-min right after an insert pushed size to 3, so the queue is never empty when we extract — safe by construction.)

  • insert 8 → {8}
  • insert 2 → {2,8}
  • insert 10 → {2,8,10} size 3 → extract-min removes 2{8,10}
  • insert 6 → {6,8,10} → extract-min removes 6{8,10}
  • insert 4 → {4,8,10} → extract-min removes 4{8,10} Remaining: {8, 10} — the top-2 largest. ✅

Level 3 — Analysis

L3.1 — Which naive structure?

A workload does 1 insert but 1,000,000 extract-min calls on a fixed set of items already loaded. Between an unsorted list and a sorted list, which is cheaper overall, and by roughly what factor per extract?

Recall Solution — L3.1

From the parent's cost table:

  • Unsorted list: insert , each extract-min → total scans.
  • Sorted list: one-time sort/insert cost, each extract-min → total operations. Sorted list wins, by roughly a factor of per extract. When extracts vastly outnumber inserts, pay once to keep sorted so every extract is cheap.

L3.2 — Count the work in Dijkstra-style mixing

Suppose you perform inserts and extract-min calls. Write the total cost for unsorted, sorted, and heap, then compute the actual numbers for (use ).

Recall Solution — L3.2

Using the parent's derivation: Plugging , :

  • (both naive options).
  • (heap). Ratio: . The heap is about 341× faster here — and the gap widens with .

The figure below plots both curves against . The orange curve is the naive cost; the teal curve is the heap's . The dashed plum line marks : read up to where each curve crosses it — the orange point sits at 16.8 million, the teal point barely above the axis at 49 thousand. That vertical gap is the 341× speed-up, and notice how it opens ever wider as you slide the plum line rightward.

Figure — Priority Queue — concept (heap-based implementation covered later)

L3.3 — Where the naive tie fails

Explain in one paragraph why the unsorted list's insert does not save the day in a mixed workload, and connect it to why we bother learning Heap later.

Recall Solution — L3.3

The unsorted list is cheap only for insert; every extract-min must scan all elements to find the minimum, costing . In a mixed workload with many extracts, those scans dominate and the cheap insert becomes irrelevant. Each naive structure buys one fast operation by making the other slow — total stays . A Heap refuses that trade: by keeping only a partial order (top guaranteed, rest loosely arranged), both operations become , so the whole workload is .


Level 4 — Synthesis

L4.1 — Stable ties by design

Your PQ must break ties in FIFO order (earlier insert leaves first among equal priorities), but the standard PQ is not stable. Design a priority scheme that forces the behaviour, and re-run L2.1's ties with it.

Recall Solution — L4.1

Use the parent's mnemonic "P for priority, S for sequence": store the pair where seq is a global counter incremented on every insert. Compare pairs lexicographically (dictionary rule above) — first by , then by seq. Re-doing the ties from L2.1: B was inserted (seq 2), D was inserted (seq 4). Both have , so the queue compares vs ; the first entries tie, so we compare seconds: since , B leaves before D — guaranteed, not by luck. Every tie is now deterministic FIFO because seq is unique and increasing.

L4.2 — A two-sided need

A hospital board wants a system that can, on demand, report either the most urgent patient (smallest triage number) or the least urgent patient (largest triage number). You may only use min-PQs. Sketch a design and give the operations.

Recall Solution — L4.2

Maintain two min-PQs over the same patients:

  • PQ_urgent — stores triage p as-is. extract-min gives the most urgent (smallest p).
  • PQ_relaxed — stores triage as -p (negation trick, L2.2). extract-min there gives , i.e. the least urgent patient. insert(patient, p): push (p) to PQ_urgent and (-p) to PQ_relaxed. To report most/least urgent, peek the matching queue (guarding for emptiness first — see the top [!mistake]). When you actually remove a patient you must invalidate them in the other queue — commonly with a "lazy deletion" tombstone, checked on peek. This reuses one primitive — a min-PQ — to serve both extremes, echoing Example 2 of the parent.

L4.3 — Merge two sorted streams

Two already-sorted (ascending) lists arrive: L1 = [2,5,9], L2 = [1,4,4,7]. Use a min-PQ to merge them into one sorted list, and give the exact output order.

Recall Solution — L4.3

Seed the PQ with the front of each list, tagged with which list it came from. Repeatedly extract-min, output it, and push the next element of that same list. (We stop the moment the PQ is empty — the empty-queue guard from the top is the loop's exit condition.)

  • Seed: {L1:2, L2:1}.
  • extract 1 (from L2) → out [1]; push L2's next 4 → {L1:2, L2:4}.
  • extract 2 (L1) → out [1,2]; push L1's next 5 → {L1:5, L2:4}.
  • extract 4 (L2) → out [1,2,4]; push L2's next 4 → {L1:5, L2:4}.
  • extract 4 (L2) → out [1,2,4,4]; push L2's next 7 → {L1:5, L2:7}.
  • extract 5 (L1) → out [1,2,4,4,5]; push L1's next 9 → {L1:9, L2:7}.
  • extract 7 (L2) → out [...,7]; L2 empty → {L1:9}.
  • extract 9 (L1) → out [...,9]; queue empty → stop. Output: [1, 2, 4, 4, 5, 7, 9]. ✅ (This is the heart of k-way merge; with lists you keep fronts.)

Level 5 — Mastery

L5.1 — Prove the lower bound intuition

Argue: any PQ built on a single flat list (sorted or unsorted) must have at least one of {insert, extract-min} cost in the worst case. Then state why a Heap escapes it.

Recall Solution — L5.1

Argument. In a flat list the minimum must live at a known position (say, one end) for extract to be . Keeping it there after an arbitrary insert requires placing the new element in its correct relative slot, which may force shifting up to elements → insert is . Conversely, if insert is (append anywhere), the minimum's position is unknown, so extract must search all elements → extract is . A single ordering promise can't make both cheap: fast extract needs order maintained (slow insert); fast insert abandons order (slow extract). Escape. A Heap is not flat — it's a Binary Tree (a complete one). Its height is only because each level doubles the number of nodes, so nodes fit in about levels (see the tree figure below). Both insert (bubble-up) and extract (sink-down) touch only one root-to-leaf path = each. The tree's branching is exactly what breaks the flat-list dilemma.

Figure — Priority Queue — concept (heap-based implementation covered later)

L5.2 — Cost of building via repeated insert

You build a PQ by inserting items one at a time into a heap, each insert . Give the total build cost as a sum, explain the bounding step, and give the number for (use ) as an upper bound .

Recall Solution — L5.2

Inserting the -th item into a heap of size costs , so the total is Why the bound holds: the sum has exactly terms. Since is an increasing function and every index satisfies , each term is at most the largest term . Replacing all terms by that common largest value can only increase the sum: Hence the build is . For , , so the upper bound is operations. (Aside: a smarter bottom-up heapify achieves , but the repeated-insert bound is what we asked for here.)

L5.3 — Design a scheduler contract

Design the priority scheme for a task scheduler that must obey, in order: (1) higher importance runs first; (2) among equal importance, earlier deadline first; (3) among equal deadline, FIFO. Assume importance (bigger = more important), deadline (smaller = sooner), and a global insert counter seq. Write the tuple you'd store in a min-PQ and verify it orders three sample tasks correctly: T1 (I=5, d=20, seq=1), T2 (I=5, d=10, seq=2), T3 (I=9, d=99, seq=3).

Recall Solution — L5.3

A min-PQ pops the smallest tuple under the lexicographic (dictionary) rule from L4.1, so every field must be arranged so that "should-run-sooner" translates into "numerically smaller." Go rule by rule:

  • Rule 1 — importance: bigger must run first, but a min-PQ prefers smaller numbers. So store : a bigger becomes a more negative (smaller) key, winning. This is the primary key.
  • Rule 2 — deadline: among equal importance, smaller runs first — and min-PQ already prefers smaller, so store as-is. This is the secondary key, only consulted when the values tie.
  • Rule 3 — FIFO: among equal deadline, earlier insert wins; seq grows with time, so smaller seq = earlier, stored as-is. This is the tertiary tie-breaker.

Stored key (compared lexicographically):

Verify with the samples — build each key, then find the lexicographically smallest first:

  • T1 →
  • T2 →
  • T3 →

Compare first entries: T3 has , which is smaller than , so T3 pops first (it is the most important, ). T1 and T2 both start with (importance tie), so we drop to the second entry — the deadline: , so T2 before T1. Run order: T3 → T2 → T1. Every rule fired at the right priority level, and the design generalises: add more tie-break fields simply by appending them to the tuple in order of importance. ✅


Connections

  • Heap — the structure that escapes the flat-list dilemma (L5.1) and gives bubble-up / sink-down.
  • Binary Tree — a heap is a complete one; its height gives the cost.
  • Queue (FIFO) · Stack (LIFO) — contrast the ordering disciplines (L1.1).
  • Dijkstra's Algorithm — the mixed insert/extract workload of L3.2.
  • Huffman Coding — repeated extract-min of L1.3 / L4.3 style merges.
  • Abstract Data Type — the PQ is a contract; the heap is one fulfilment of it.