Chapter 11: Priority Queues and Heaps
Introduction
Learning Outcomes
Explain the core operations of a priority queue and where it is used.
Relate heap order and the complete-tree property to the array representation.
Carry out insertion, removal of the root, and heap construction by hand.
Analyze the cost of each operation and of heap sort.
The Priority Queue ADT
The Priority Queue
- Every item has a priority (its key), and items leave in priority order.
- Priority, not arrival order, decides who goes next: an ambulance overtakes the traffic.
- Typical uses: task scheduling, emergency rooms, event-driven simulation.
- Costs depend on the implementation: with a heap, looking at the top item is O(1) and removing it is O(log n); with an unsorted list even looking is O(n).
The Priority Queue ADT
Basic Concepts
Like Goodrich, we define the ADT as a min-priority queue (removeMin), while the heap code uses a max-heap (insert / removeMax). The choice only flips the direction of the comparisons.
The Priority Queue ADT
Queue or Priority Queue?
Four patients arrive at the emergency room in this order (a smaller number is more urgent):
| Arrival | 1st | 2nd | 3rd | 4th |
|---|---|---|---|---|
| Patient (priority) | Alan (3) | Eve (1) | Carl (2) | Nina (1) |
| Queue (FIFO) | Alan | Eve | Carl | Nina |
| Priority queue | Eve | Nina | Carl | Alan |
The Priority Queue ADT
Applications
The Priority Queue ADT
Core Operations
| Operation | Meaning |
|---|---|
insert(k, v) | Insert: the entry takes its place according to its priority. |
removeMin() | Remove: removes and returns the highest-priority entry. |
min() | Peek: returns the entry without removing it. |
size() | Number of entries. |
isEmpty() | Is the queue empty? |
public interface PriorityQueue<K, V> { void insert(K key, V value); Entry<K, V> min(); // null if empty Entry<K, V> removeMin(); // null if empty int size(); boolean isEmpty(); }
In a max-priority queue the same operations are called max() and removeMax(). The heap code in this chapter uses a max-heap (insert, removeMax).
The Priority Queue ADT
A Sample Sequence of Operations (min-priority)
| Operation | Returns | Queue contents |
|---|---|---|
insert(5, A) | { (5,A) } | |
insert(9, C) | { (5,A), (9,C) } | |
insert(3, B) | { (3,B), (5,A), (9,C) } | |
min() | (3,B) | { (3,B), (5,A), (9,C) } |
removeMin() | (3,B) | { (5,A), (9,C) } |
insert(7, D) | { (5,A), (7,D), (9,C) } | |
removeMin() | (5,A) | { (7,D), (9,C) } |
removeMin() | (7,D) | { (9,C) } |
removeMin() | (9,C) | { } |
removeMin() | null | { } empty queue |
List-Based Implementations
Unsorted List vs. Sorted List
insert: append at the end → O(1)min,removeMin: scan the whole list → O(n)
insert: find the right slot → O(n)min,removeMin: the minimum is always at one end → O(1)
List-Based Implementations · Step through the code
Code Trace: removeMin() on an unsorted array
Unsorted array a = [5, 9, 3, 7, 4]: insertions go at the end, so the minimum could be anywhere. First candidate: minIdx = 0.
k = 1: a[1] = 9 ≥ a[0] = 5 → minIdx stays.
k = 2: a[2] = 3 < a[0] = 5 → new minimum, minIdx = 2.
k = 3: a[3] = 7 ≥ a[2] = 3 → minIdx stays.
k = 4: a[4] = 4 ≥ a[2] = 3 → minIdx stays.
k = 5 = n → scan done: 4 comparisons. Every removeMin scans the whole array → O(n).
min = a[2] = 3 is saved.
To fill the gap without shifting, the last item (4) moves to index 2; n = 4. Order doesn't matter, so this is O(1).
return 3. The remaining array [5, 9, 4, 7] is still unsorted.
// unsorted array: insert(x) { a[n] = x; n++; } O(1) public int removeMin() { // requires n > 0 int minIdx = 0; // index of the minimum for (int k = 1; k < n; k++) if (a[k] < a[minIdx]) minIdx = k; int min = a[minIdx]; a[minIdx] = a[n - 1]; n--; // last item fills the gap return min; }
List-Based Implementations · Step through the code
Code Trace: insert(4) on a sorted array
The array is sorted from largest to smallest, so the highest-priority (smallest) item is always last. k = n − 1 = 4: search for the slot from the back.
a[4] = 2 < 4 → 2 belongs after x, so it shifts.
a[5] = a[4] = 2: shifted one to the right (a[4] is a stale copy for now and will be overwritten).
k = 3: move one to the left.
a[3] = 3 < 4 → 3 belongs after x, so it shifts.
a[4] = a[3] = 3: shifted one to the right (a[3] is a stale copy for now and will be overwritten).
k = 2: move one to the left.
a[2] = 5 ≥ 4 → the loop ends. 4 goes at k + 1 = 3.
a[3] = 4, n = 6. The array is still sorted. In the worst case (x is the largest) every item shifts → O(n).
In return, removeMin() is cheap: the minimum (2) is already last → n = 5, return 2. No shifting → O(1).
// sorted array: largest to smallest, min always at a[n-1] public void insert(int x) { int k = n - 1; while (k >= 0 && a[k] < x) { // items smaller than x a[k + 1] = a[k]; // shift one to the right k--; } a[k + 1] = x; n++; } public int removeMin() { n--; return a[n]; }
List-Based Implementations
Array and Linked-List Representations
- Items live in an array; keeping it sorted means shifting items on every insertion (O(n)).
- If the top-priority item sits at the front, every removal shifts the whole array left: O(n).
- Better: keep the top-priority item at the end → removal is O(1).
- Each item is linked in at the position its priority calls for: finding it is O(n), linking is O(1).
- The top-priority item is at the head, so removal = deleting the head, O(1).
- No shifting, but every node carries an extra reference.
This list runs from smallest to largest (min-priority), so the highest-priority item, 3, is at the head. A new node 6 has been created.
Finding the slot: advance until prev.next.value = 7 > 6 → prev = 5. This scan takes O(n).
First newNode.next = prev.next (6 → 7). Order matters: without this link in place, the path to 7 would be lost.
Then prev.next = newNode (5 → 6). Linking is O(1) with no shifting, and removal always happens at the head: O(1).
List-Based Implementations
A Sorted-Array Priority Queue: Code
class Item<T> { T data; int priority; Item(T d, int p) { data = d; priority = p; } }
// fields of SortedArrayPQ<E> private Item<E>[] items = (Item<E>[]) new Item[10]; private int size = 0; public boolean isEmpty() { return size == 0; }
public void insert(E data, int p) { if (size == items.length) items = Arrays.copyOf(items, 2 * size); int i = size; while (i > 0 && items[i-1].priority < p) { items[i] = items[i - 1]; // shift right i--; } items[i] = new Item<>(data, p); size++; }
public E remove() { if (isEmpty()) return null; E result = items[0].data; for (int i = 1; i < size; i++) items[i - 1] = items[i]; items[--size] = null; return result; }
Items are stored as (data, priority) pairs, with the highest priority first, in items[0]. This simple version ignores the advice on the previous slide: insert is O(n) (shifting right) and so is remove (shifting left). Keeping the items in ascending order would shrink remove to one line (items[--size]) and O(1). The strict < comparison preserves arrival order among equal priorities.
List-Based Implementations · Step through the code
Code Trace: insert(D, 5) with a Tie
After insert(A, 2), insert(B, 5), insert(C, 1) the array is [B:5, A:2, C:1]. Now insert(D, 5): two items will shift.
insert(D, 5): size 3 == 10? No, so no need to grow.
i = size = 3: start at the first free slot and search leftward.
i > 0 and items[2].priority = 1 < 5 → true: the item on the left has lower priority.
items[3] = items[2]: C shifts one to the right; i = 2.
i > 0 and items[1].priority = 2 < 5 → true: the item on the left has lower priority.
items[2] = items[1]: A shifts one to the right; i = 1.
items[0].priority = 5 < 5 → false (on a tie the earlier arrival stays ahead: FIFO), so the loop ends.
items[1] = (D, 5): D fills the gap.
size++ → 4. The array is still sorted from largest to smallest.
public void insert(E data, int p) { if (size == items.length) items = Arrays.copyOf(items, 2 * size); int i = size; while (i > 0 && items[i-1].priority < p) { items[i] = items[i - 1]; // shift right i--; } items[i] = new Item<>(data, p); size++; }
List-Based Implementations
Comparing Costs
| Operation | Unsorted list | Sorted list | Binary heap |
|---|---|---|---|
size, isEmpty | O(1) | O(1) | O(1) |
insert | O(1) | O(n) | O(log n) |
min | O(n) | O(1) | O(1) |
removeMin | O(n) | O(1) | O(log n) |
| Filling with n items | O(n) | O(n log n)* | O(n)** |
* By sorting first. ** With bottom-up heap construction.
Binary Heaps
The Binary Heap
The binary heap is the most common way to implement a priority queue. It is a binary tree that satisfies two conditions at once:
Binary Heaps
Min-Heaps and Max-Heaps
key(parent) ≤ key(child)key(parent) ≥ key(child)- The order holds only between parent and child: in the min-heap, 7 and 3 are siblings, so 7 > 3 is fine.
- Every path from the root down to a leaf is sorted (increasing in a min-heap, decreasing in a max-heap).
Binary Heaps
Complete Binary Trees
last level filled from the left
a gap in the last level
last level not left-aligned
Binary Heaps
Heap or Not?
For each tree, decide: max-heap, min-heap, or not a heap at all? Which property fails?
Show answers
- Max-heap ✓ (complete, and every parent ≥ its children).
- Not a heap: 12 > its parent 8 → heap order fails.
- Min-heap ✓ (complete, and every parent ≤ its children).
- Not a heap: 8 has no children while 9 does → not complete.
Binary Heaps · Step through the code
Code Trace: isHeap([10, 8, 9, 3, 5, 7, 4, 6])
isHeap(a) is called on an 8-item array. Every node except the root (index 0) is compared once with its parent.
i = 1: parent p = (1 − 1) / 2 = 0. a[0] = 10 ≥ a[1] = 8 ✓ → continue.
i = 2: parent p = (2 − 1) / 2 = 0. a[0] = 10 ≥ a[2] = 9 ✓ → continue.
i = 3: parent p = (3 − 1) / 2 = 1. a[1] = 8 ≥ a[3] = 3 ✓ → continue.
i = 4: parent p = (4 − 1) / 2 = 1. a[1] = 8 ≥ a[4] = 5 ✓ → continue.
i = 5: parent p = (5 − 1) / 2 = 2. a[2] = 9 ≥ a[5] = 7 ✓ → continue.
i = 6: parent p = (6 − 1) / 2 = 2. a[2] = 9 ≥ a[6] = 4 ✓ → continue.
i = 7: p = 3. a[3] = 3 < a[7] = 6 ✗ → the child is larger than its parent: return false.
Result: false. The first seven items form a valid heap, but 6 is larger than its parent 3. The worst case is n − 1 comparisons → O(n).
static boolean isHeap(int[] a) { // max-heap? for (int i = 1; i < a.length; i++) { int p = (i - 1) / 2; // parent if (a[p] < a[i]) return false; // out of order } return true; }
Binary Heaps
Array Representation
- Starting at the root, nodes are written into the array level by level, left to right (level order).
- Because the tree is complete, the array has no gaps and needs no pointers (references).
The small numbers next to the nodes are their array indices. In these slides indices start at 0, so the root is heap[0].
Binary Heaps
Parent and Child Indices
- Java's integer division already rounds down:
(i - 1) / 2. A node has no children when 2i + 1 ≥ n. - The 1-based alternative leaves heap[0] empty: children 2k and 2k + 1, parent ⌊k/2⌋. The two are equivalent; pick one and use it consistently.
Insertion and Removal
Insertion: Up-Heap (Sift-Up)
- Put the new item in the first free slot (heap[n]). The tree stays complete.
- Heap order may now be broken: if the new item is larger than its parent (max-heap), swap them.
- Keep moving the item up until it reaches the root or its parent is greater than or equal to it.
swim.Insertion and Removal
Inserting into a Max-Heap: Java Code
public class MaxHeap { private int[] heap = new int[4]; private int n = 0; // number of items public void insert(int x) { if (n == heap.length) resize(2 * heap.length); heap[n] = x; n++; // first free slot swim(n - 1); } private void swim(int i) { // up-heap while (i > 0) { int parent = (i - 1) / 2; if (heap[parent] >= heap[i]) return; swap(i, parent); i = parent; } } private void swap(int a, int b) { int t = heap[a]; heap[a] = heap[b]; heap[b] = t; } } // MaxHeap
Insertion and Removal · Step through the code
Code Trace: insert(10), insert(8)
Initial max-heap: every parent ≥ its children, and the largest (9) is at the root. Capacity is 16, so there is room.
insert(10): 10 goes into the first free slot, heap[7]; n = 8. The tree stays complete, but heap order may break.
i = 7 > 0 → parent = (7 − 1) / 2 = 3. Compare: heap[3] = 2 < heap[7] = 10.
10 > 2 → swap (index 7 ↔ 3); the item moves up one level, i = 3.
i = 3 > 0 → parent = (3 − 1) / 2 = 1. Compare: heap[1] = 3 < heap[3] = 10.
10 > 3 → swap (index 3 ↔ 1); the item moves up one level, i = 1.
i = 1 > 0 → parent = (1 − 1) / 2 = 0. Compare: heap[0] = 9 < heap[1] = 10.
10 > 9 → swap (index 1 ↔ 0); the item moves up one level, i = 0.
i = 0: the item is at the root; the loop test (i > 0) fails and swim ends. 10 is in place.
insert(8): 8 goes into the first free slot, heap[8]; n = 9. The tree stays complete, but heap order may break.
i = 8 > 0 → parent = (8 − 1) / 2 = 3. Compare: heap[3] = 3 < heap[8] = 8.
8 > 3 → swap (index 8 ↔ 3); the item moves up one level, i = 3.
i = 3 > 0 → parent = (3 − 1) / 2 = 1. Compare: heap[1] = 9 ≥ heap[3] = 8.
9 ≥ 8: the parent is already ≥ the item → heap order holds, return. 8 is in place.
public void insert(int x) { if (n == heap.length) resize(2 * heap.length); heap[n] = x; n++; // first free slot swim(n - 1); } private void swim(int i) { // up-heap while (i > 0) { int parent = (i - 1) / 2; if (heap[parent] >= heap[i]) return; swap(i, parent); i = parent; } }
Insertion and Removal · Step through the code
Code Trace: inserting 4, 5, 2, 6 into an empty heap
insert(4): 4 goes into the first free slot, heap[0]; n = 1. The tree stays complete, but heap order may break.
i = 0: the item is at the root; the loop test (i > 0) fails and swim ends. 4 is in place.
insert(5): 5 goes into the first free slot, heap[1]; n = 2. The tree stays complete, but heap order may break.
i = 1 > 0 → parent = (1 − 1) / 2 = 0. Compare: heap[0] = 4 < heap[1] = 5.
5 > 4 → swap (index 1 ↔ 0); the item moves up one level, i = 0.
i = 0: the item is at the root; the loop test (i > 0) fails and swim ends. 5 is in place.
insert(2): 2 goes into the first free slot, heap[2]; n = 3. The tree stays complete, but heap order may break.
i = 2 > 0 → parent = (2 − 1) / 2 = 0. Compare: heap[0] = 5 ≥ heap[2] = 2.
5 ≥ 2: the parent is already ≥ the item → heap order holds, return. 2 is in place.
insert(6): 6 goes into the first free slot, heap[3]; n = 4. The tree stays complete, but heap order may break.
i = 3 > 0 → parent = (3 − 1) / 2 = 1. Compare: heap[1] = 4 < heap[3] = 6.
6 > 4 → swap (index 3 ↔ 1); the item moves up one level, i = 1.
i = 1 > 0 → parent = (1 − 1) / 2 = 0. Compare: heap[0] = 5 < heap[1] = 6.
6 > 5 → swap (index 1 ↔ 0); the item moves up one level, i = 0.
i = 0: the item is at the root; the loop test (i > 0) fails and swim ends. 6 is in place.
public void insert(int x) { if (n == heap.length) resize(2 * heap.length); heap[n] = x; n++; // first free slot swim(n - 1); } private void swim(int i) { // up-heap while (i > 0) { int parent = (i - 1) / 2; if (heap[parent] >= heap[i]) return; swap(i, parent); i = parent; } }
Insertion and Removal · Step through the code
Code Trace (continued): insert(1), insert(3)
Start: heap = [6, 5, 2, 4], n = 4 and capacity 4 → the array is completely full.
insert(1): n == heap.length (4) → the array is full. resize(8) doubles the capacity and copies the items into a new array.
1 goes into the first free slot, heap[4]; n = 5. The tree stays complete, but heap order may break.
i = 4 > 0 → parent = (4 − 1) / 2 = 1. Compare: heap[1] = 5 ≥ heap[4] = 1.
5 ≥ 1: the parent is already ≥ the item → heap order holds, return. 1 is in place.
insert(3): 3 goes into the first free slot, heap[5]; n = 6. The tree stays complete, but heap order may break.
i = 5 > 0 → parent = (5 − 1) / 2 = 2. Compare: heap[2] = 2 < heap[5] = 3.
3 > 2 → swap (index 5 ↔ 2); the item moves up one level, i = 2.
i = 2 > 0 → parent = (2 − 1) / 2 = 0. Compare: heap[0] = 6 ≥ heap[2] = 3.
6 ≥ 3: the parent is already ≥ the item → heap order holds, return. 3 is in place.
public void insert(int x) { if (n == heap.length) resize(2 * heap.length); heap[n] = x; n++; // first free slot swim(n - 1); } private void swim(int i) { // up-heap while (i > 0) { int parent = (i - 1) / 2; if (heap[parent] >= heap[i]) return; swap(i, parent); i = parent; } }
Insertion and Removal · Step through the code
Code Trace: resize(8)
insert(1) is called with n == heap.length = 4 → resize(8). An array's length is fixed, so growing means a new array plus copying.
A new 8-cell array, newArr, is allocated; Java fills an int array with 0s.
k = 0 < n → newArr[0] = heap[0] = 6.
k = 1 < n → newArr[1] = heap[1] = 5.
k = 2 < n → newArr[2] = heap[2] = 2.
k = 3 < n → newArr[3] = heap[3] = 4.
k = 4 = n → the loop ends. n items were copied, so this resize costs O(n).
heap = newArr: the field now refers to the new array, and the old one is left to the garbage collector. Same tree; only the capacity is now 8.
resize returns and insert(1) continues. Because the capacity doubles each time, n insertions copy fewer than 2n items in total → amortized O(1) per insertion.
private void resize(int capacity) { int[] newArr = new int[capacity]; for (int k = 0; k < n; k++) newArr[k] = heap[k]; // copy heap = newArr; // old array becomes garbage }
Insertion and Removal
Removing the Maximum: Down-Heap (Sift-Down)
- Save the item at the root (the largest); it is the return value.
- Move the last item (heap[n − 1]) to the root and decrease n by one.
- Swap the root with its larger child.
- Keep moving the item down until it reaches a leaf or is greater than or equal to both children.
Insertion and Removal
Removing the Maximum: Java Code
public int removeMax() { // requires n > 0 int max = heap[0]; swap(0, n - 1); n--; // last item to the root sink(0); return max; } private void sink(int i) { // down-heap while (2 * i + 1 < n) { // has a left child? int j = 2 * i + 1; // j: larger child if (j + 1 < n && heap[j + 1] > heap[j]) j++; if (heap[i] >= heap[j]) return; swap(i, j); i = j; // down one level } }
if (n == 0).Insertion and Removal · Step through the code
Code Trace: removeMax() and sink
removeMax(): the largest item is always at the root → max = heap[0] = 9.
The root and the last item swap (0 ↔ 6); n = 6, so 9 leaves the heap (faded). The tree is still complete; the new root 4 may break the order.
Left child 1 (3), right child 2 (6) → the larger is 6, j = 2.
4 < 6 → swap (index 0 ↔ 2); the larger child rises, i = 2.
Only a left child: index 5 (5) → j = 5.
4 < 5 → swap (index 2 ↔ 5); the larger child rises, i = 5.
i = 5: 2·5 + 1 = 11 ≥ n = 6 → no children (a leaf), so the loop ends.
sink is done, and the array is a valid max-heap again. return 9. The path spans at most ⌊log₂ n⌋ levels → O(log n).
public int removeMax() { // requires n > 0 int max = heap[0]; swap(0, n - 1); n--; // last item to the root sink(0); return max; } private void sink(int i) { // down-heap while (2 * i + 1 < n) { // has a left child? int j = 2 * i + 1; // j: larger child if (j + 1 < n && heap[j + 1] > heap[j]) j++; if (heap[i] >= heap[j]) return; swap(i, j); i = j; // down one level } }
Insertion and Removal · Step through the code
Code Trace: two removeMax() calls in a row
removeMax() #1: the largest item is always at the root → max = heap[0] = 10.
The root and the last item swap (0 ↔ 8); n = 8, so 10 leaves the heap (faded). The tree is still complete; the new root 3 may break the order.
Left child 1 (9), right child 2 (6) → the larger is 9, j = 1.
3 < 9 → swap (index 0 ↔ 1); the larger child rises, i = 1.
Left child 3 (8), right child 4 (1) → the larger is 8, j = 3.
3 < 8 → swap (index 1 ↔ 3); the larger child rises, i = 3.
Only a left child: index 7 (2) → j = 7.
heap[3] = 3 ≥ heap[7] = 2 → no smaller than either child, so heap order holds: return.
sink is done, and the array is a valid max-heap again. return 10. The path spans at most ⌊log₂ n⌋ levels → O(log n).
removeMax() #2: the largest item is always at the root → max = heap[0] = 9.
The root and the last item swap (0 ↔ 7); n = 7, so 9 leaves the heap (faded). The tree is still complete; the new root 2 may break the order.
Left child 1 (8), right child 2 (6) → the larger is 8, j = 1.
2 < 8 → swap (index 0 ↔ 1); the larger child rises, i = 1.
Left child 3 (3), right child 4 (1) → the larger is 3, j = 3.
2 < 3 → swap (index 1 ↔ 3); the larger child rises, i = 3.
i = 3: 2·3 + 1 = 7 ≥ n = 7 → no children (a leaf), so the loop ends.
sink is done, and the array is a valid max-heap again. return 9. The path spans at most ⌊log₂ n⌋ levels → O(log n).
public int removeMax() { // requires n > 0 int max = heap[0]; swap(0, n - 1); n--; // last item to the root sink(0); return max; } private void sink(int i) { // down-heap while (2 * i + 1 < n) { // has a left child? int j = 2 * i + 1; // j: larger child if (j + 1 < n && heap[j + 1] > heap[j]) j++; if (heap[i] >= heap[j]) return; swap(i, j); i = j; // down one level } }
Insertion and Removal
Min-Heap: insert(1) and removeMin()
Initial min-heap: every parent ≤ its children, and the smallest item (2) is at the root.
insert(1): 1 goes into the first free slot (index 7). Its parent: (7−1)/2 = 3 → 9.
1 < 9 → swap (index 7 ↔ 3).
1 < 5 → swap (index 3 ↔ 1).
1 < 2 → swap (index 1 ↔ 0). At the root → stop.
removeMin(): min = heap[0] = 1 (the root) is the value to return.
Swap the root with the last item (0 ↔ 7); n = 7, and 1 is outside the heap. The new root 9 may break heap order → down-heap.
Children: 2 and 3 → the smaller is 2. 9 > 2 → swap (0 ↔ 1).
Children: 5 and 6 → the smaller is 5. 9 > 5 → swap (1 ↔ 3). Index 3 is a leaf → stop.
Insertion and Removal
Cost of Heap Operations
| Operation | Cost | Why? |
|---|---|---|
size, isEmpty | O(1) | a counter n is kept |
max / min (peek) | O(1) | the item is always at heap[0] |
insert | O(log n)* | Up-heap: one path from a leaf to the root |
removeMax (removeMin) | O(log n)* | Down-heap: one path from the root to a leaf |
| Searching for an arbitrary item | O(n) | A heap is not sorted, so it is no good for searching |
* Amortized, including growing and shrinking the array. With height ⌊log₂ n⌋, a heap of n = 10⁶ items needs at most 19 swaps.
Insertion and Removal
Adaptable Priority Queues
Sometimes an item's priority changes while it waits (a distance shrinks in Dijkstra's algorithm, a patient gets worse), or the item has to be removed from the middle. Goodrich defines two extra operations for this:
heap[e.index]; if the key grew, up-heap; if it shrank, down-heap (max-heap).Insertion and Removal · Step through the code
Code Trace: changeKey(4, 8)
changeKey(4, 8): the key at index 4 is 1. Save the old value: old = 1.
heap[4] = 8. The tree is still complete, but heap order may now be broken around this node.
8 > 1: the key increased, so it can only conflict with its parent → call swim(4).
Parent (4 − 1) / 2 = 1: heap[1] = 3 < 8 → swap needed.
Swap (index 4 ↔ 1); i = 1. The 3 that moved down was already larger than its other child.
Parent 0: heap[0] = 9 ≥ 8 → order holds, return.
changeKey is done: 8 moved up one level. Only the path 4 → 1 → 0 was examined: O(log n).
public void changeKey(int i, int key) { int old = heap[i]; heap[i] = key; if (key > old) swim(i); // increased: move up else sink(i); // decreased: move down } private void swim(int i) { // up-heap while (i > 0) { int parent = (i - 1) / 2; if (heap[parent] >= heap[i]) return; swap(i, parent); i = parent; } }
Insertion and Removal · Step through the code
Code Trace (continued): changeKey(0, 1)
changeKey(0, 1): the key at index 0 is 9. Save the old value: old = 9.
heap[0] = 1. The tree is still complete, but heap order may now be broken around this node.
1 < 9: the key decreased, so it can only conflict with its children → call sink(0).
Children 8 and 6 → the larger is 8, j = 1.
1 < 8 → swap (0 ↔ 1); the larger child moves up, i = 1.
Children 2 and 3 → the larger is 3, j = 4.
1 < 3 → swap (1 ↔ 4); the larger child moves up, i = 4.
i = 4: 2·4 + 1 = 9 ≥ n → a leaf, so the loop ends.
Done: 1 sank two levels and 8 rose to the root. changeKey(0, x) with a small x does exactly what the sink step of removeMax does.
public void changeKey(int i, int key) { int old = heap[i]; heap[i] = key; if (key > old) swim(i); // increased: move up else sink(i); // decreased: move down } private void sink(int i) { // down-heap while (2 * i + 1 < n) { int j = 2 * i + 1; // j: larger child if (j + 1 < n && heap[j + 1] > heap[j]) j++; if (heap[i] >= heap[j]) return; swap(i, j); i = j; } }
Heap Construction and Heap Sort
Building a Heap from n Items
Option 1: n insertions
- Each insertion does an up-heap: O(log n).
- Total O(n log n).
- The only option when items arrive one at a time (online).
Option 2: bottom-up
- If all the items are available up front, take the array as it is.
- Run down-heap on every node, from the last internal node (⌊n/2⌋ − 1) back to the root.
- Total O(n).
for (int k = n / 2 - 1; k >= 0; k--) // from the last internal node to the root sink(k); // sink k; its subtrees are already heaps
Heap Construction and Heap Sort · Step through the code
Code Trace: buildHeap() (k = 4, 3, 2)
An arbitrary array (not a heap). Indices 5…9 are leaves, and each is already a heap on its own. Start at k = n/2 − 1 = 4 (the last internal node).
k = 4 → sink(4). Only a left child: index 9 (7) → j = 9.
heap[4] = 16 ≥ heap[9] = 7 → no smaller than either child, so heap order holds: return.
k = 3 → sink(3). Left child 7 (14), right child 8 (8) → the larger is 14, j = 7.
2 < 14 → swap (index 3 ↔ 7); the larger child rises, i = 7.
i = 7: 2·7 + 1 = 15 ≥ n = 10 → no children (a leaf), so the loop ends.
k = 2 → sink(2). Left child 5 (9), right child 6 (10) → the larger is 10, j = 6.
3 < 10 → swap (index 2 ↔ 6); the larger child rises, i = 6.
i = 6: 2·6 + 1 = 13 ≥ n = 10 → no children (a leaf), so the loop ends.
public void buildHeap() { // arbitrary array for (int k = n / 2 - 1; k >= 0; k--) sink(k); // subtrees are heaps } private void sink(int i) { // down-heap while (2 * i + 1 < n) { // has a left child? int j = 2 * i + 1; // j: larger child if (j + 1 < n && heap[j + 1] > heap[j]) j++; if (heap[i] >= heap[j]) return; swap(i, j); i = j; // down one level } }
Heap Construction and Heap Sort · Step through the code
Code Trace (continued): k = 1, 0
k = 1 → sink(1). Left child 3 (14), right child 4 (16) → the larger is 16, j = 4.
1 < 16 → swap (index 1 ↔ 4); the larger child rises, i = 4.
Only a left child: index 9 (7) → j = 9.
1 < 7 → swap (index 4 ↔ 9); the larger child rises, i = 9.
i = 9: 2·9 + 1 = 19 ≥ n = 10 → no children (a leaf), so the loop ends.
k = 0 → sink(0). Left child 1 (16), right child 2 (10) → the larger is 16, j = 1.
4 < 16 → swap (index 0 ↔ 1); the larger child rises, i = 1.
Left child 3 (14), right child 4 (7) → the larger is 14, j = 3.
4 < 14 → swap (index 1 ↔ 3); the larger child rises, i = 3.
Left child 7 (2), right child 8 (8) → the larger is 8, j = 8.
4 < 8 → swap (index 3 ↔ 8); the larger child rises, i = 8.
i = 8: 2·8 + 1 = 17 ≥ n = 10 → no children (a leaf), so the loop ends.
k = −1 → the loop ends. The array is now a max-heap: every parent ≥ its children, and the largest (16) is at the root. Total work: O(n).
public void buildHeap() { // arbitrary array for (int k = n / 2 - 1; k >= 0; k--) sink(k); // subtrees are heaps } private void sink(int i) { // down-heap while (2 * i + 1 < n) { // has a left child? int j = 2 * i + 1; // j: larger child if (j + 1 < n && heap[j + 1] > heap[j]) j++; if (heap[i] >= heap[j]) return; swap(i, j); i = j; // down one level } }
Heap Construction and Heap Sort
Why Is Bottom-Up Construction O(n)?
| Node height | Nodes | Max swaps | Total |
|---|---|---|---|
| 0 (leaves) | 8 | 0 | 0 |
| 1 | 4 | 1 | 4 |
| 2 | 2 | 2 | 4 |
| 3 (root) | 1 | 3 | 3 |
| n = 15 | 11 |
- Half the nodes are leaves and do no work at all; the few tall nodes are the only ones that do much.
- In general: Σh (n / 2h+1) · h ≤ n → O(n).
- With n insertions, the many deep nodes walk long paths instead: in the same tree the worst case is 0 + 2·1 + 4·2 + 8·3 = 34 swaps.
Heap Construction and Heap Sort
Heap Sort
- The sorted part grows at the end of the array while the heap shrinks at the front; no extra array is needed (in place, O(1) extra memory).
- O(n log n) even in the worst case: no O(n²) worst case as in quick sort.
- Not stable: equal keys may change their relative order.
- A max-heap sorts in ascending order, a min-heap in descending order.
Heap Construction and Heap Sort
Heap Sort: Java Code
public static void heapSort(int[] a) { int n = a.length; // 1) build: from the last internal node to the root for (int k = n / 2 - 1; k >= 0; k--) sink(a, k, n); // 2) sort: move the largest to the end for (int last = n - 1; last > 0; last--) { swap(a, 0, last); sink(a, 0, last); // heap size = last } } static void sink(int[] a, int i, int n) { while (2 * i + 1 < n) { int j = 2 * i + 1; if (j + 1 < n && a[j + 1] > a[j]) j++; if (a[i] >= a[j]) return; swap(a, i, j); i = j; } }
Heap Construction and Heap Sort · Step through the code
Code Trace: heapSort([6, 5, 3, 4, 1, 2])
Input a = [6, 5, 3, 4, 1, 2]. The whole array is the heap region; the sorted part on the right is still empty.
Phase 1: sink is called for k = 2, 1, 0. This array is already a max-heap, so each call returns after its first comparison; no swaps.
last = 5: the root (the largest, 6) swaps with a[5] = 2 → 6 is in its final place; heap size 5.
sink(a, 0, 5): at i = 0, the larger child a[1] = 5 > 2 → swap (0 ↔ 1).
sink(a, 0, 5): at i = 1, the larger child a[3] = 4 > 2 → swap (1 ↔ 3). Index 3 is a leaf → sink ends.
last = 4: the root (the largest, 5) swaps with a[4] = 1 → 5 is in its final place; heap size 4.
sink(a, 0, 4): at i = 0, the larger child a[1] = 4 > 1 → swap (0 ↔ 1).
sink(a, 0, 4): at i = 1, the larger child a[3] = 2 > 1 → swap (1 ↔ 3). Index 3 is a leaf → sink ends.
last = 3: the root (the largest, 4) swaps with a[3] = 1 → 4 is in its final place; heap size 3.
sink(a, 0, 3): at i = 0, the larger child a[2] = 3 > 1 → swap (0 ↔ 2). Index 2 is a leaf → sink ends.
last = 2: the root (the largest, 3) swaps with a[2] = 1 → 3 is in its final place; heap size 2.
sink(a, 0, 2): at i = 0, the larger child a[1] = 2 > 1 → swap (0 ↔ 1). Index 1 is a leaf → sink ends.
last = 1: the root (the largest, 2) swaps with a[1] = 1 → 2 is in its final place; heap size 1.
sink(a, 0, 1): heap size 1, the root has no children → nothing to do.
last = 0 → the loop ends. The array is in ascending order: [1, 2, 3, 4, 5, 6]. No extra array was used (in place); total O(n log n).
public static void heapSort(int[] a) { int n = a.length; // 1) build: from the last internal node to the root for (int k = n / 2 - 1; k >= 0; k--) sink(a, k, n); // 2) sort: move the largest to the end for (int last = n - 1; last > 0; last--) { swap(a, 0, last); sink(a, 0, last); // heap size = last } }
Heap Construction and Heap Sort
Sorting with a Priority Queue
The general idea: insert every item into a priority queue, then call removeMin n times. Which sorting algorithm we get depends on the implementation:
| Implementation | n × insert | n × removeMin | Resulting algorithm |
|---|---|---|---|
| Unsorted list | O(n) | O(n²) | Selection sort, O(n²) |
| Sorted list | O(n²) | O(n) | Insertion sort, O(n²) |
| Binary heap | O(n log n) | O(n log n) | Heap sort, O(n log n) |
Heap Construction and Heap Sort
Kinds of Heaps: d-ary, Binomial, Fibonacci
- d-ary heap: each node has d children; parent (i − 1) / d. Height logd n, so insertion gets faster, but removal compares d children per level.
- Binomial / Fibonacci heaps: a forest of trees; merging two queues and decreasing a key are fast (Dijkstra, Prim).
| Min-heap type | insert | removeMin | decrease key | merge |
|---|---|---|---|---|
| Binary heap | O(log n) | O(log n) | O(log n) | O(n) |
| d-ary heap | O(logd n) | O(d logd n) | O(logd n) | O(n) |
| Binomial heap | O(log n) | O(log n) | O(log n) | O(log n) |
| Fibonacci heap | O(1) | O(log n)* | O(1)* | O(1) |
* amortized. In practice the binary heap is often the fastest, thanks to its simplicity and cache-friendly array layout.
Java and Applications
Java's Built-In PriorityQueue
PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(5); pq.offer(1); pq.offer(8); pq.peek(); // 1 (min-heap, O(1)) pq.poll(); // 1 (O(log n)) // reverse comparator for a max-heap PriorityQueue<Integer> maxPq = new PriorityQueue<>(Comparator.reverseOrder()); // patients: lower priority number goes first PriorityQueue<Patient> er = new PriorityQueue<>( Comparator.comparingInt(p -> p.priority));
- Internally it is an array-based binary min-heap.
offer/pollare O(log n);peekandsizeare O(1);remove(obj)andcontainsare O(n).- Not thread-safe; for multithreaded code, use
PriorityBlockingQueue. - No FIFO guarantee among equal priorities.
Java and Applications · Step through the code
Code Trace: what PriorityQueue does inside
An empty PriorityQueue. Internally it keeps a binary min-heap in an array (queue), with the smallest item at the root.
offer(5): the heap was empty, so 5 becomes the root (index 0).
offer(1): added at index 1; its parent 5 > 1 → min-heap order is broken.
siftUp (up-heap): 1 moves up to the root → queue = [1, 5]. Cost: O(log n).
offer(8): added at index 2; parent 1 ≤ 8 → no up-heap needed.
peek(): returns the root, 1, but does not remove it → O(1).
poll(): takes the root, 1; the last item, 8, moves to the root; size = 2.
siftDown (down-heap): 8 swaps with its smaller child → queue = [5, 8]. 1 is printed.
poll(): takes the root, 5; the last item, 8, moves to the root; size = 1.
One item is left, so no siftDown is needed. 5 is printed.
size() = 1 (only 8 is left) → O(1). Console output: 1 (peek), 1, 5 (poll), 1 (size).
PriorityQueue<Integer> pq =
new PriorityQueue<>(); // min-heap
pq.offer(5);
pq.offer(1);
pq.offer(8);
System.out.println(pq.peek()); // look
System.out.println(pq.poll()); // remove
System.out.println(pq.poll());
System.out.println(pq.size());
Java and Applications · Step through the code
Application: kthLargest([5, 1, 9, 3, 7, 6], 3)
k = 3: we keep a min-heap of at most k items. Its root is the smallest of the k largest items seen so far.
x = 5: offer(5) → added at the end, up-heap. size = 1.
size = 1 ≤ k = 3 → nothing to drop yet.
x = 1: offer(1) → added at the end, up-heap (it swam up: 1 swap). size = 2.
size = 2 ≤ k = 3 → nothing to drop yet.
x = 9: offer(9) → added at the end, up-heap. size = 3.
size = 3 ≤ k = 3 → nothing to drop yet.
x = 3: offer(3) → added at the end, up-heap (it swam up: 1 swap). size = 4.
size = 4 > k → poll(): the smallest, 1, is dropped (it cannot be among the k largest). Last item to the root, down-heap.
x = 7: offer(7) → added at the end, up-heap. size = 4.
size = 4 > k → poll(): the smallest, 3, is dropped (it cannot be among the k largest). Last item to the root, down-heap.
x = 6: offer(6) → added at the end, up-heap (it swam up: 1 swap). size = 4.
size = 4 > k → poll(): the smallest, 5, is dropped (it cannot be among the k largest). Last item to the root, down-heap.
Loop done: pq holds the three largest items {6, 7, 9}. peek() = 6 → the 3rd largest. Total O(n log k), memory O(k).
static int kthLargest(int[] a, int k) { PriorityQueue<Integer> pq = new PriorityQueue<>(); for (int x : a) { pq.offer(x); // O(log k) if (pq.size() > k) pq.poll(); // drop the smallest } return pq.peek(); // smallest of the k largest }
Java and Applications
Application: A Priority Scheduler
Simulator: Priority Scheduler Simulator (three levels, a time quantum, optional aging).
Summary
Summary
| Operation | Heap | How? |
|---|---|---|
| peek (max/min) | O(1) | heap[0] |
| insert | O(log n) | put at heap[n], up-heap |
| removeMax / removeMin | O(log n) | move the last item to the root, down-heap |
| building a heap | O(n) | down-heap from ⌊n/2⌋ − 1 down to 0 |
| heap sort | O(n log n) | build + move the root to the end n − 1 times |
Summary
Interactive Simulators
Summary
Common Mistakes
PriorityQueue, println(pq) → [1, 3, 2].j + 1 < n check and you read an item outside the heap (one already removed).contains and remove(obj) are O(n). Fast access needs a locator or a map.PriorityQueue is a min-heap by default; for a max-heap, use Comparator.reverseOrder().Summary
Sample Problems
- Insert 5, 12, 8, 3, 15, 10 in this order into an empty max-heap. What is the final array?
- Apply
removeMax()once to the heap from problem 1. What is returned, and what is the new array? - Turn the array {3, 1, 6, 5, 2, 4} into a max-heap bottom-up.
- In a 0-based array, at which indices are the parent and the children of the node at index 13?
- Is {10, 7, 9, 8, 2, 5} a max-heap? If not, why not?
- What does it cost to find the largest item in a min-heap?
Show answers
- {15, 12, 10, 3, 5, 8}
- Returns 15; the new array is {12, 8, 10, 3, 5}.
- {6, 5, 4, 1, 2, 3}
- Parent ⌊(13 − 1)/2⌋ = 6; children 27 and 28.
- No: the 7 at index 1 has a child 8 (index 3) that is larger.
- O(n): the largest item is one of the leaves, and all of the roughly n/2 leaves must be checked.
Summary
Exercises
- Apply heap sort to [4, 10, 3, 5, 1]. What is the array after each step?
- What is the height of a binary heap with 100 items? At most how many swaps can one insertion make?
- In a max-heap with distinct keys, at which indices can the 2nd and 3rd largest items be?
- Rewrite
isHeapfor a min-heap. What does it return for an empty array and for a one-item array? - How would you merge k sorted lists with N items in total using a min-heap? What does it cost?
- In the max-heap [9, 7, 8, 3, 5, 6, 4], what is the array after
changeKey(5, 10)?
Answers and hints
- Build: [10, 5, 3, 4, 1]; then (sorted to the right of |): [5, 4, 3, 1 | 10] → [4, 1, 3 | 5, 10] → [3, 1 | 4, 5, 10] → [1 | 3, 4, 5, 10].
- ⌊log₂ 100⌋ = 6; at most 6 swaps (leaf → root).
- The 2nd largest is a child of the root: index 1 or 2. The 3rd largest: any of indices 1–6.
if (a[p] > a[i]) return false;The loop never runs →truein both cases.- Put the head of each list into the heap; remove the minimum and insert the next item from its list. The heap holds ≤ k items → O(N log k).
- 6 → 10, then up-heap: 10 swaps first with 8, then with 9 → [10, 7, 9, 3, 5, 8, 4].
Summary
Further Reading
- Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
- Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, 4th ed., 2022.
- Oracle, Java SE 17
PriorityQueueAPI documentation.