Heaps & Priority Queues

A priority queue selects an extremal-priority item; a heap is one efficient representation. This chapter derives binary-heap indexing, swim and sink, linear-time heap construction, heapsort, d-ary and binomial variants, stable tie-breaking, handles, and concurrency constraints.

A priority queue stores items and repeatedly returns one with extremal priority. Unlike a sorted sequence, it need not expose all items in order; unlike FIFO, insertion time does not choose removal order. A heap is a representation that keeps just enough partial order to make the extremum cheap while allowing updates without shifting a sorted array.

Binary min-heap represented as a complete tree and array A complete min-heap has priority 1 at the root, children 3 and 5, and lower nodes 7, 9, 8, and 6. The same nodes occupy zero-based array indices zero through six. Formulas map parent and children. Arrows show insertion swimming upward and root replacement sinking downward. Complete shape + parent-before-child order 1357986 swim ↑sink ↓ Zero-based mapping parent(i) = ⌊(i − 1) / 2⌋ left(i) = 2i + 1 right(i) = 2i + 2 The same complete tree occupies one array prefix 1357986 i=0i=1i=2i=3i=4i=5i=6 Heap order is partial: this array is not a sorted sequence.
Completeness maps the tree into one array prefix; swim and sink repair only an ancestor or descendant path.

Specify the priority-queue contract first #

A minimum-priority queue commonly offers:

  • insert(item, priority);
  • min() without removal;
  • extract_min();
  • optionally decrease_key(handle,new_priority), arbitrary erase, and meld.

State whether equal priorities are stable, whether priorities can change, whether duplicates are allowed, and what handles survive mutation. std::priority_queue is a max-priority adaptor by default, exposes no arbitrary element handle, and does not promise stable ordering among equivalent elements. Those omissions simplify its implementation.

If equal-priority jobs must preserve arrival order, compare composite keys (priority, sequence_number). Stability is then explicit and testable. Beware counter wrap and distributed producers: the tie-breaker must remain unique and ordered within the promised scope.

A binary heap combines two invariants #

A min-heap has:

  1. shape: a complete binary tree—all levels full except possibly the last, filled left to right;
  2. order: every node's priority is no greater than its children's.

The order invariant implies the root is globally minimal: every root-to-node path is nondecreasing. It does not imply that siblings or subtrees are mutually sorted. Printing the array is therefore not a sorted traversal.

Completeness removes pointers. In a zero-based array, for index ii:

parent(i)=⌊i−12⌋(i>0),parent(i)=\left\lfloor\frac{i-1}{2}\right\rfloor\quad(i>0),

left(i)=2i+1,right(i)=2i+2.left(i)=2i+1,\qquad right(i)=2i+2.

The height is ⌊log⁡2n⌋\lfloor\log_2 n\rfloor, so repairing one root-to-leaf or leaf-to-root path takes O(log⁡n)O(\log n) comparisons/swaps.

Insert swims; extraction sinks #

Insertion places the new item in the next array slot, preserving completeness, then swims it upward while it precedes its parent. Extract-min saves the root, moves the last item to the root, shrinks the array, then sinks that item by swapping with the smaller child until order is restored.

min_heap_repairs.cppcpp
template<class T, class Less>
void swim(std::vector<T>& a, std::size_t i, Less less) {
    while (i > 0) {
        std::size_t p = (i - 1) / 2;
        if (!less(a[i], a[p])) break;
        std::swap(a[i], a[p]);
        i = p;
    }
}

template<class T, class Less>
void sink(std::vector<T>& a, std::size_t i, Less less) {
    for (;;) {
        std::size_t left = 2 * i + 1;
        if (left >= a.size()) break;
        std::size_t child = left;
        if (left + 1 < a.size() && less(a[left + 1], a[left])) ++child;
        if (!less(a[child], a[i])) break;
        std::swap(a[i], a[child]);
        i = child;
    }
}

Choosing the better child before swapping is essential. Swapping with an arbitrary violating child can leave the parent larger than the other child.

Operation Binary heap Sorted array Unsorted array
find minimum O(1)O(1) O(1)O(1) O(n)O(n)
insert O(log⁡n)O(\log n) O(n)O(n) shift O(1)O(1)
extract minimum O(log⁡n)O(\log n) O(n)O(n) shift unless removing convenient end O(n)O(n) search
arbitrary ordered iteration not provided O(n)O(n) requires sorting

Bottom-up heap construction is linear #

Inserting nn elements one at a time gives an O(nlog⁡n)O(n\log n) upper bound. Floyd/Williams bottom-up construction starts at the last internal node and sinks each internal node. Although one sink can travel O(log⁡n)O(\log n), most nodes lie near leaves and travel little.

At most about n/2h+1n/2^{h+1} nodes have height hh, so total sink distance is bounded by

∑h≥0n2h+1h=O(n).\sum_{h\ge 0}\frac{n}{2^{h+1}}h=O(n).

This is a recurring analysis lesson: multiplying “number of calls” by the worst cost of any call can be a loose bound when calls start at structurally different heights.

Heapsort reuses the representation in place #

For ascending heapsort, build a max-heap, swap the root with the last unsorted position, shrink the heap boundary, and sink the new root. Each extraction fixes one final array position. The algorithm takes O(nlog⁡n)O(n\log n) worst-case time and O(1)O(1) auxiliary storage in its array form, but it is generally unstable. Its access pattern is also less cache-friendly than highly engineered quicksort variants despite the strong worst-case bound.

More operations require more structure #

Handles and mutable priority #

decrease_key must locate the item before swimming it. Searching a plain heap array is O(n)O(n), erasing the advantage. A mutable heap therefore returns handles and maintains a position map, updating that map on every swap. Define handle invalidation, generation checks, and whether priorities may both increase and decrease. A stale raw index can silently mutate the wrong item after swaps or slot reuse.

Lazy duplicate insertion is another design: insert the new (priority,id) and ignore stale records when extracted by comparing against authoritative state. This avoids mutable handles but can increase memory and extraction work. It is a different, explicit contract—not an invisible fallback.

d-ary heaps #

A dd-ary heap shortens height to Θ(log⁡dn)\Theta(\log_d n) but a sink examines up to dd children per level. Larger dd can reduce random-access depth and help decrease-key-heavy workloads, while increasing comparisons during extraction. Choose from operation mix and measured layout, not asymptotics alone.

Meldable heaps #

A binary heap stored in one array does not meld two heaps efficiently: concatenation followed by heapify is linear in their combined size. Binomial queues represent a heap as a forest with at most one binomial tree of each rank. Meld behaves like binary addition: equal-rank trees link and carry to the next rank, giving logarithmic meld and extremum operations under the classical structure. Fibonacci and pairing heaps offer other tradeoffs, especially for theoretical or decrease-key-heavy workloads, but bring pointer, constant-factor, and implementation complexity.

Priority queues do not solve scheduling policy by themselves #

Priority scheduling raises system questions:

  • Can a continuous stream of high priorities starve low priorities?
  • Does priority age or inherit across dependencies?
  • Are priorities trusted user input?
  • Is the comparison total and deterministic?
  • Can a cancelled job remain as a lazy stale heap entry indefinitely?
  • Does extraction need a deadline-aware wait or batch semantics?

Multiple threads also need a compound synchronization protocol. A mutex around one heap gives simple linearizability but can bottleneck. Sharded or relaxed priority queues trade exact global-min extraction for throughput; that semantic weakening must be approved by the algorithm using it.

Review and failure diagnostics #

  1. Is the root meant to be minimum or maximum, and is the comparator direction consistent?
  2. Are both completeness and heap order preserved?
  3. Does sink select the better child before swapping?
  4. Is zero-based or one-based index arithmetic used consistently, with overflow considered for 2*i+1?
  5. Is build claimed as O(n)O(n) because it is bottom-up, or is it actually repeated insertion?
  6. Are ties stable by contract or intentionally unspecified?
  7. Does decrease-key have a validated handle/position map?
  8. Are stale entries bounded and recognized against authoritative state?
  9. Does the client need sorted iteration, predecessor queries, or meld—operations a basic heap does not promise?
  10. Can scheduling priorities starve work or be adversarially manipulated?

Exercises #

  1. Show the array after inserting priorities 7, 3, 5, 1 into a min-heap.
  2. Why does a heap with root 1 and children 4 and 2 remain valid even though its breadth-first array is not sorted?
  3. Explain what must happen to a position map whenever two heap elements swap.
Solutions
  1. One valid sequence of states is [7], [3,7], [3,7,5], then [1,3,5,7] after swimming 1 past 7 and 3.
  2. Heap order constrains each parent-child edge only: 1 precedes both 4 and 2. It does not constrain 4 relative to 2.
  3. The map entry for each swapped item's identity must be updated to its new array index as part of the same invariant-preserving operation.

Sources

  1. bookAlgorithms in C++, Third Edition, Parts 1–4: Fundamentals, Data Structures, Sorting, Searchingnotes
  2. paperAlgorithm 232 — Heapsort (Williams, 1964)
  3. paperA Data Structure for Manipulating Priority Queues (Vuillemin, 1978)
  4. docsC++ Working Draft N5050 — priority_queue

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close