Chapter 11: Priority Queues and Heaps

Data Structures
The priority queue ADTList-based implementationsBinary heaps Array representationUp-heap / down-heapBuilding a heapHeap sortPriority scheduler
Sercan KÜLCÜ · Giresun University · Computer Engineering

Introduction

Learning Outcomes

The ADT

Explain the core operations of a priority queue and where it is used.

Binary heaps

Relate heap order and the complete-tree property to the array representation.

Operations

Carry out insertion, removal of the root, and heap construction by hand.

Cost analysis

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).
112
Not first come, first served: the highest priority goes first.

The Priority Queue ADT

Basic Concepts

Entry = (key, value)The key carries the priority; the value carries the actual data.(1, "heart attack") · (4, "sprain")
PriorityKeys must be comparable (<, ≤). Usually a smaller key means higher priority (a min-priority queue).
Highest priorityThe item at the "front": the smallest key in a min-queue, the largest in a max-queue.
Equal priorityItems with equal keys are not guaranteed to leave in FIFO order; if that matters, add an arrival sequence number to the key.

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):

Arrival1st2nd3rd4th
Patient (priority)Alan (3)Eve (1)Carl (2)Nina (1)
Queue (FIFO)AlanEveCarlNina
Priority queueEveNinaCarlAlan
QueueLeaving order = arrival order: first come, first served.
Priority queueLeaving order = priority order. Eve and Nina tie at 1; here we kept their arrival order.

The Priority Queue ADT

Applications

Operating SystemsProcess scheduling: a higher-priority process gets the CPU first.interrupt handler > UI > backup
Graph AlgorithmsDijkstra's shortest paths and A* search take the next closest vertex from a priority queue.
Emergency ManagementPatient er and incident handling: the most urgent case is handled first.
Data CompressionHuffman coding merges the two lowest-frequency nodes at every step.

The Priority Queue ADT

Core Operations

OperationMeaning
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)

OperationReturnsQueue 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

Unsorted list
5 9 3 7 4 min needs a full scan ← insert
  • insert: append at the end → O(1)
  • min, removeMin: scan the whole list → O(n)
Sorted list
3 4 5 7 9 min find a slot for 4, shift the rest
  • insert: find the right slot → O(n)
  • min, removeMin: the minimum is always at one end → O(1)
One is fast at inserting, the other at removing. The binary heap balances the two (both O(log n)).

List-Based Implementations · Step through the code

Code Trace: removeMin() on an unsorted array

removeMin()n = 5051923374456▲ minIdx

Unsorted array a = [5, 9, 3, 7, 4]: insertions go at the end, so the minimum could be anywhere. First candidate: minIdx = 0.

removeMin()n = 5051923374456▲ minIdx▲ k

k = 1: a[1] = 9 ≥ a[0] = 5 → minIdx stays.

removeMin()n = 5051923374456▲ k, minIdx

k = 2: a[2] = 3 < a[0] = 5 → new minimum, minIdx = 2.

removeMin()n = 5051923374456▲ minIdx▲ k

k = 3: a[3] = 7 ≥ a[2] = 3 → minIdx stays.

removeMin()n = 5051923374456▲ minIdx▲ k

k = 4: a[4] = 4 ≥ a[2] = 3 → minIdx stays.

removeMin()n = 5051923374456▲ minIdx

k = 5 = n → scan done: 4 comparisons. Every removeMin scans the whole array → O(n).

removeMin()n = 5051923374456▲ minIdx

min = a[2] = 3 is saved.

removeMin()n = 4051924374456▲ minIdx

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).

removeMin()n = 4051924374456

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

insert(4)n = 5091725334256▲ k

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.

insert(4)n = 5091725334256▲ k

a[4] = 2 < 4 → 2 belongs after x, so it shifts.

insert(4)n = 50917253342526▲ k

a[5] = a[4] = 2: shifted one to the right (a[4] is a stale copy for now and will be overwritten).

insert(4)n = 50917253342526▲ k

k = 3: move one to the left.

insert(4)n = 50917253342526▲ k

a[3] = 3 < 4 → 3 belongs after x, so it shifts.

insert(4)n = 50917253343526▲ k

a[4] = a[3] = 3: shifted one to the right (a[3] is a stale copy for now and will be overwritten).

insert(4)n = 50917253343526▲ k

k = 2: move one to the left.

insert(4)n = 50917253343526▲ k

a[2] = 5 ≥ 4 → the loop ends. 4 goes at k + 1 = 3.

insert(4)n = 60917253443526

a[3] = 4, n = 6. The array is still sorted. In the worst case (x is the largest) every item shifts → O(n).

removeMin()n = 50917253443526▲ returned

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

Array representation
  • 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).
Linked-list representation
  • 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.
head35796newNodenull▲ prev

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.

head35796newNodenull▲ prev

Finding the slot: advance until prev.next.value = 7 > 6 → prev = 5. This scan takes O(n).

head35796newNodenull▲ prev

First newNode.next = prev.next (6 → 7). Order matters: without this link in place, the path to 7 would be lost.

head35796newNodenull▲ prev

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

0B:51A:22C:1345…

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.

0B:51A:22C:1345…new itemD:5

insert(D, 5): size 3 == 10? No, so no need to grow.

0B:51A:22C:1345…▲inew itemD:5

i = size = 3: start at the first free slot and search leftward.

0B:51A:22C:1345…▲inew itemD:5

i > 0 and items[2].priority = 1 < 5 → true: the item on the left has lower priority.

0B:51A:22C:13C:145…▲inew itemD:5

items[3] = items[2]: C shifts one to the right; i = 2.

0B:51A:22C:13C:145…▲inew itemD:5

i > 0 and items[1].priority = 2 < 5 → true: the item on the left has lower priority.

0B:51A:22A:23C:145…▲inew itemD:5

items[2] = items[1]: A shifts one to the right; i = 1.

0B:51A:22A:23C:145…▲inew itemD:5

items[0].priority = 5 < 5 → false (on a tie the earlier arrival stays ahead: FIFO), so the loop ends.

0B:51D:52A:23C:145…▲i

items[1] = (D, 5): D fills the gap.

0B:51D:52A:23C:145…

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

OperationUnsorted listSorted listBinary heap
size, isEmptyO(1)O(1)O(1)
insertO(1)O(n)O(log n)
minO(n)O(1)O(1)
removeMinO(n)O(1)O(log n)
Filling with n itemsO(n)O(n log n)*O(n)**

* By sorting first.   ** With bottom-up heap construction.

n insertions + n removals: O(n²) with either list, O(n log n) with a heap. For n = 10⁶ that is a factor of about 50,000.

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:

907182335465next insert6level 0level 1level 2← root = largest
Siblings are unordered: swapping 7 and 8 still gives a heap.
1. Heap-order propertyEvery node comes before its children: in a max-heap, parent ≥ child. The highest-priority item is at the root.
2. Complete binary tree propertyEvery level except the last is full, and the last fills from left to right → height ⌊log₂ n⌋, and no gaps in the array.
Insertion and removal work along a single path from the root to a leaf → O(log n). A heap is not a binary search tree.

Binary Heaps

Min-Heaps and Max-Heaps

0739856
Min-heap: smallest key at the root
key(parent) ≤ key(child)
9362154
Max-heap: largest key at the root
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

12345
Complete ✓
last level filled from the left
12346
Not complete ✗
a gap in the last level
12367
Not complete ✗
last level not left-aligned
HeightLevel d holds at most 2d nodes, so a complete binary tree with n nodes has height h = ⌊log₂ n⌋.n = 7 → h = 2 · n = 1000 → h = 9
Why it mattersA new item always goes into "the leftmost free spot on the last level", and in the array that is exactly index n.

Binary Heaps

Heap or Not?

1089357
(a)
10893127
(b)
13254
(c)
108976
(d)

For each tree, decide: max-heap, min-heap, or not a heap at all? Which property fails?

Show answers
  1. Max-heap ✓ (complete, and every parent ≥ its children).
  2. Not a heap: 12 > its parent 8 → heap order fails.
  3. Min-heap ✓ (complete, and every parent ≤ its children).
  4. 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])

10081923354754667isHeap(a)n = 801018293345576476

isHeap(a) is called on an 8-item array. Every node except the root (index 0) is compared once with its parent.

100p81i923354754667isHeap(a)n = 801018293345576476▲ i▲ p

i = 1: parent p = (1 − 1) / 2 = 0. a[0] = 10 ≥ a[1] = 8 ✓ → continue.

100p8192i3354754667isHeap(a)n = 801018293345576476▲ i▲ p

i = 2: parent p = (2 − 1) / 2 = 0. a[0] = 10 ≥ a[2] = 9 ✓ → continue.

10081p9233i54754667isHeap(a)n = 801018293345576476▲ i▲ p

i = 3: parent p = (3 − 1) / 2 = 1. a[1] = 8 ≥ a[3] = 3 ✓ → continue.

10081p923354i754667isHeap(a)n = 801018293345576476▲ i▲ p

i = 4: parent p = (4 − 1) / 2 = 1. a[1] = 8 ≥ a[4] = 5 ✓ → continue.

1008192p335475i4667isHeap(a)n = 801018293345576476▲ i▲ p

i = 5: parent p = (5 − 1) / 2 = 2. a[2] = 9 ≥ a[5] = 7 ✓ → continue.

1008192p33547546i67isHeap(a)n = 801018293345576476▲ i▲ p

i = 6: parent p = (6 − 1) / 2 = 2. a[2] = 9 ≥ a[6] = 4 ✓ → continue.

100819233p54754667iisHeap(a)false01018293345576476▲ i▲ p

i = 7: p = 3. a[3] = 3 < a[7] = 6 ✗ → the child is larger than its parent: return false.

10081923354754667isHeap(a)false01018293345576476

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).
90316223145546heap[]90316223145546

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

90316223145546heap[]90316223145546
Childrenleft(i) = 2i + 1 · right(i) = 2i + 2i = 1 → 3 and 4 · i = 2 → 5 and 6
Parentparent(i) = ⌊(i − 1) / 2⌋i = 6 → 2 · i = 5 → 2 · i = 4 → 1
  • 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)

  1. Put the new item in the first free slot (heap[n]). The tree stays complete.
  2. Heap order may now be broken: if the new item is larger than its parent (max-heap), swap them.
  3. Keep moving the item up until it reaches the root or its parent is greater than or equal to it.
12907182335465106heap[6] = 10 added → 2 swaps up to the root
CostAt most h = ⌊log₂ n⌋ swaps → O(log n).
Why it worksThe parent that moves down in a swap was already larger than its other child.
Namesup-heap, bubble-up, sift-up, swim; the code in this chapter calls 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
resizeWhen the array fills up, its capacity doubles (amortized O(1)).
swapExchanges heap[a] and heap[b].
StoppingAt the root (i = 0) or once parent ≥ item.

Insertion and Removal · Step through the code

Code Trace: insert(10), insert(8)

90316223145546startn = 7 · heap.length = 160913263241556478

Initial max-heap: every parent ≥ its children, and the largest (9) is at the root. Capacity is 16, so there is room.

90316223145546107newinsert(10)n = 8 · heap.length = 16091326324155647108▲ new

insert(10): 10 goes into the first free slot, heap[7]; n = 8. The tree stays complete, but heap order may break.

90316223parent145546107iinsert(10)n = 8 · heap.length = 16091326324155647108▲ i▲ parent

i = 7 > 0 → parent = (7 − 1) / 2 = 3. Compare: heap[3] = 2 < heap[7] = 10.

903162103i14554627insert(10)n = 8 · heap.length = 16091326310415564728▲ i

10 > 2 → swap (index 7 ↔ 3); the item moves up one level, i = 3.

9031parent62103i14554627insert(10)n = 8 · heap.length = 16091326310415564728▲ i▲ parent

i = 3 > 0 → parent = (3 − 1) / 2 = 1. Compare: heap[1] = 3 < heap[3] = 10.

90101i623314554627insert(10)n = 8 · heap.length = 16091102633415564728▲ i

10 > 3 → swap (index 3 ↔ 1); the item moves up one level, i = 1.

90parent101i623314554627insert(10)n = 8 · heap.length = 16091102633415564728▲ i▲ parent

i = 1 > 0 → parent = (1 − 1) / 2 = 0. Compare: heap[0] = 9 < heap[1] = 10.

100i91623314554627insert(10)n = 8 · heap.length = 16010192633415564728▲ i

10 > 9 → swap (index 1 ↔ 0); the item moves up one level, i = 0.

100i91623314554627insert(10)n = 8 · heap.length = 16010192633415564728▲ i

i = 0: the item is at the root; the loop test (i > 0) fails and swim ends. 10 is in place.

1009162331455462788newinsert(8)n = 9 · heap.length = 160101926334155647288▲ new

insert(8): 8 goes into the first free slot, heap[8]; n = 9. The tree stays complete, but heap order may break.

100916233parent1455462788iinsert(8)n = 9 · heap.length = 160101926334155647288▲ i▲ parent

i = 8 > 0 → parent = (8 − 1) / 2 = 3. Compare: heap[3] = 3 < heap[8] = 8.

100916283i1455462738insert(8)n = 9 · heap.length = 160101926384155647283▲ i

8 > 3 → swap (index 8 ↔ 3); the item moves up one level, i = 3.

10091parent6283i1455462738insert(8)n = 9 · heap.length = 160101926384155647283▲ i▲ parent

i = 3 > 0 → parent = (3 − 1) / 2 = 1. Compare: heap[1] = 9 ≥ heap[3] = 8.

10091parent6283i1455462738insert(8)n = 9 · heap.length = 160101926384155647283▲ i▲ parent

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

40newinsert(4)n = 1 · heap.length = 40412345▲ new

insert(4): 4 goes into the first free slot, heap[0]; n = 1. The tree stays complete, but heap order may break.

40iinsert(4)n = 1 · heap.length = 40412345▲ i

i = 0: the item is at the root; the loop test (i > 0) fails and swim ends. 4 is in place.

4051newinsert(5)n = 2 · heap.length = 404152345▲ new

insert(5): 5 goes into the first free slot, heap[1]; n = 2. The tree stays complete, but heap order may break.

40parent51iinsert(5)n = 2 · heap.length = 404152345▲ i▲ parent

i = 1 > 0 → parent = (1 − 1) / 2 = 0. Compare: heap[0] = 4 < heap[1] = 5.

50i41insert(5)n = 2 · heap.length = 405142345▲ i

5 > 4 → swap (index 1 ↔ 0); the item moves up one level, i = 0.

50i41insert(5)n = 2 · heap.length = 405142345▲ i

i = 0: the item is at the root; the loop test (i > 0) fails and swim ends. 5 is in place.

504122newinsert(2)n = 3 · heap.length = 4051422345▲ new

insert(2): 2 goes into the first free slot, heap[2]; n = 3. The tree stays complete, but heap order may break.

50parent4122iinsert(2)n = 3 · heap.length = 4051422345▲ i▲ parent

i = 2 > 0 → parent = (2 − 1) / 2 = 0. Compare: heap[0] = 5 ≥ heap[2] = 2.

50parent4122iinsert(2)n = 3 · heap.length = 4051422345▲ i▲ parent

5 ≥ 2: the parent is already ≥ the item → heap order holds, return. 2 is in place.

50412263newinsert(6)n = 4 · heap.length = 40514223645▲ new

insert(6): 6 goes into the first free slot, heap[3]; n = 4. The tree stays complete, but heap order may break.

5041parent2263iinsert(6)n = 4 · heap.length = 40514223645▲ i▲ parent

i = 3 > 0 → parent = (3 − 1) / 2 = 1. Compare: heap[1] = 4 < heap[3] = 6.

5061i2243insert(6)n = 4 · heap.length = 40516223445▲ i

6 > 4 → swap (index 3 ↔ 1); the item moves up one level, i = 1.

50parent61i2243insert(6)n = 4 · heap.length = 40516223445▲ i▲ parent

i = 1 > 0 → parent = (1 − 1) / 2 = 0. Compare: heap[0] = 5 < heap[1] = 6.

60i512243insert(6)n = 4 · heap.length = 40615223445▲ i

6 > 5 → swap (index 1 ↔ 0); the item moves up one level, i = 0.

60i512243insert(6)n = 4 · heap.length = 40615223445▲ i

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)

60512243staten = 4 · heap.length = 40615223445

Start: heap = [6, 5, 2, 4], n = 4 and capacity 4 → the array is completely full.

60512243insert(1)n = 4 · heap.length = 80615223445

insert(1): n == heap.length (4) → the array is full. resize(8) doubles the capacity and copies the items into a new array.

6051224314newinsert(1)n = 5 · heap.length = 806152234415▲ new

1 goes into the first free slot, heap[4]; n = 5. The tree stays complete, but heap order may break.

6051parent224314iinsert(1)n = 5 · heap.length = 806152234415▲ i▲ parent

i = 4 > 0 → parent = (4 − 1) / 2 = 1. Compare: heap[1] = 5 ≥ heap[4] = 1.

6051parent224314iinsert(1)n = 5 · heap.length = 806152234415▲ i▲ parent

5 ≥ 1: the parent is already ≥ the item → heap order holds, return. 1 is in place.

605122431435newinsert(3)n = 6 · heap.length = 8061522344153▲ new

insert(3): 3 goes into the first free slot, heap[5]; n = 6. The tree stays complete, but heap order may break.

605122parent431435iinsert(3)n = 6 · heap.length = 8061522344153▲ i▲ parent

i = 5 > 0 → parent = (5 − 1) / 2 = 2. Compare: heap[2] = 2 < heap[5] = 3.

605132i431425insert(3)n = 6 · heap.length = 8061523344152▲ i

3 > 2 → swap (index 5 ↔ 2); the item moves up one level, i = 2.

60parent5132i431425insert(3)n = 6 · heap.length = 8061523344152▲ i▲ parent

i = 2 > 0 → parent = (2 − 1) / 2 = 0. Compare: heap[0] = 6 ≥ heap[2] = 3.

60parent5132i431425insert(3)n = 6 · heap.length = 8061523344152▲ i▲ parent

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)

60512243resize(8)heap6524

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.

60512243resize(8)heap6524newArr0001020304050607

A new 8-cell array, newArr, is allocated; Java fills an int array with 0s.

60512243resize(8)heap6524newArr6001020304050607

k = 0 < n → newArr[0] = heap[0] = 6.

60512243resize(8)heap6524newArr6051020304050607

k = 1 < n → newArr[1] = heap[1] = 5.

60512243resize(8)heap6524newArr6051220304050607

k = 2 < n → newArr[2] = heap[2] = 2.

60512243resize(8)heap6524newArr6051224304050607

k = 3 < n → newArr[3] = heap[3] = 4.

60512243resize(8)heap6524newArr6051224304050607

k = 4 = n → the loop ends. n items were copied, so this resize costs O(n).

60512243resize(8)old6524heap6051224304050607

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.

60512243resize(8)old6524heap6051224304050607

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)

  1. Save the item at the root (the largest); it is the return value.
  2. Move the last item (heap[n − 1]) to the root and decrease n by one.
  3. Swap the root with its larger child.
  4. Keep moving the item down until it reaches a leaf or is greater than or equal to both children.
124071823354659 out, 4 to the root → sinks via the larger childmax = 9
Why the larger child?If the smaller child moved up, it would become the parent of a larger sibling. In a min-heap, pick the smaller child instead.
CostTwo comparisons per level, at most h levels → O(log n).

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
  }
}
Empty heapReal code first throws an exception if (n == 0).
shrinkOptional: halve the array once n drops to ¼ of the capacity.
SwapThe maximum stays at the end of the array, which is what heap sort relies on.

Insertion and Removal · Step through the code

Code Trace: removeMax() and sink

90max316223145546removeMax()n = 709132632415564▲ max

removeMax(): the largest item is always at the root → max = heap[0] = 9.

40316223145596removeMax()n = 604132632415569

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.

40i31left62right,j23145596removeMax()n = 604132632415569▲ i▲ left▲ rightj

Left child 1 (3), right child 2 (6) → the larger is 6, j = 2.

603142i23145596removeMax()n = 606132432415569▲ i

4 < 6 → swap (index 0 ↔ 2); the larger child rises, i = 2.

603142i231455left,j96removeMax()n = 606132432415569▲ i▲ leftj

Only a left child: index 5 (5) → j = 5.

603152231445i96removeMax()n = 606132532415469▲ i

4 < 5 → swap (index 2 ↔ 5); the larger child rises, i = 5.

603152231445i96removeMax()n = 606132532415469▲ i

i = 5: 2·5 + 1 = 11 ≥ n = 6 → no children (a leaf), so the loop ends.

60315223144596removeMax()n = 606132532415469

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

100max9162831455462738removeMax() #1n = 90101926384155647283▲ max

removeMax() #1: the largest item is always at the root → max = heap[0] = 10.

3091628314554627108removeMax() #1n = 80319263841556472810

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.

30i91left,j62right8314554627108removeMax() #1n = 80319263841556472810▲ i▲ leftj▲ right

Left child 1 (9), right child 2 (6) → the larger is 9, j = 1.

9031i628314554627108removeMax() #1n = 80913263841556472810▲ i

3 < 9 → swap (index 0 ↔ 1); the larger child rises, i = 1.

9031i6283left,j14right554627108removeMax() #1n = 80913263841556472810▲ i▲ leftj▲ right

Left child 3 (8), right child 4 (1) → the larger is 8, j = 3.

90816233i14554627108removeMax() #1n = 80918263341556472810▲ i

3 < 8 → swap (index 1 ↔ 3); the larger child rises, i = 3.

90816233i14554627left,j108removeMax() #1n = 80918263341556472810▲ i▲ leftj

Only a left child: index 7 (2) → j = 7.

90816233i14554627j108removeMax() #1n = 80918263341556472810▲ i▲ j

heap[3] = 3 ≥ heap[7] = 2 → no smaller than either child, so heap order holds: return.

9081623314554627108removeMax() #1n = 80918263341556472810

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).

90max81623314554627108removeMax() #2n = 80918263341556472810▲ max

removeMax() #2: the largest item is always at the root → max = heap[0] = 9.

2081623314554697108removeMax() #2n = 70218263341556479810

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.

20i81left,j62right3314554697108removeMax() #2n = 70218263341556479810▲ i▲ leftj▲ right

Left child 1 (8), right child 2 (6) → the larger is 8, j = 1.

8021i623314554697108removeMax() #2n = 70812263341556479810▲ i

2 < 8 → swap (index 0 ↔ 1); the larger child rises, i = 1.

8021i6233left,j14right554697108removeMax() #2n = 70812263341556479810▲ i▲ leftj▲ right

Left child 3 (3), right child 4 (1) → the larger is 3, j = 3.

80316223i14554697108removeMax() #2n = 70813263241556479810▲ i

2 < 3 → swap (index 1 ↔ 3); the larger child rises, i = 3.

80316223i14554697108removeMax() #2n = 70813263241556479810▲ i

i = 3: 2·3 + 1 = 7 ≥ n = 7 → no children (a leaf), so the loop ends.

8031622314554697108removeMax() #2n = 70813263241556479810

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()

20513293648546heap[]20513293648546min-heapn = 7

Initial min-heap: every parent ≤ its children, and the smallest item (2) is at the root.

2051329364854617heap[]2051329364854617insert(1)n = 8

insert(1): 1 goes into the first free slot (index 7). Its parent: (7−1)/2 = 3 → 9.

2051321364854697heap[]2051321364854697insert(1)n = 8

1 < 9 → swap (index 7 ↔ 3).

2011325364854697heap[]2011325364854697insert(1)n = 8

1 < 5 → swap (index 3 ↔ 1).

1021325364854697heap[]1021325364854697insert(1)n = 8

1 < 2 → swap (index 1 ↔ 0). At the root → stop.

1021325364854697heap[]1021325364854697removeMin()n = 8

removeMin(): min = heap[0] = 1 (the root) is the value to return.

9021325364854617heap[]9021325364854617removeMin()n = 7

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.

2091325364854617heap[]2091325364854617removeMin()n = 7

Children: 2 and 3 → the smaller is 2. 9 > 2 → swap (0 ↔ 1).

2051329364854617heap[]2051329364854617removeMin()n = 7

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

OperationCostWhy?
size, isEmptyO(1)a counter n is kept
max / min (peek)O(1)the item is always at heap[0]
insertO(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 itemO(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:

90316223145546Entry: key = 1index = 4our handlereference eheap[]
Locator: each entry knows its position in the heap array, updated on every swap.
replaceKey(e, k)Change heap[e.index]; if the key grew, up-heap; if it shrank, down-heap (max-heap).
remove(e)Swap with the last item, n − 1, then up-heap or down-heap the item that took its place.
Why a locator?Without one, finding the entry is O(n); with one it is O(1), so both operations are O(log n).

Insertion and Removal · Step through the code

Code Trace: changeKey(4, 8)

9031622314i5546changeKey(4, 8)n = 709132632415564▲ i

changeKey(4, 8): the key at index 4 is 1. Save the old value: old = 1.

9031622384i5546changeKey(4, 8)n = 709132632485564▲ i

heap[4] = 8. The tree is still complete, but heap order may now be broken around this node.

9031622384i5546changeKey(4, 8)n = 709132632485564▲ i

8 > 1: the key increased, so it can only conflict with its parent → call swim(4).

9031parent622384i5546swimn = 709132632485564▲ i▲ parent

Parent (4 − 1) / 2 = 1: heap[1] = 3 < 8 → swap needed.

9081i6223345546swimn = 709182632435564▲ i

Swap (index 4 ↔ 1); i = 1. The 3 that moved down was already larger than its other child.

90parent81i6223345546swimn = 709182632435564▲ i▲ parent

Parent 0: heap[0] = 9 ≥ 8 → order holds, return.

90816223345546changeKey(4, 8)n = 709182632435564

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)

90i816223345546changeKey(0, 1)n = 709182632435564▲ i

changeKey(0, 1): the key at index 0 is 9. Save the old value: old = 9.

10i816223345546changeKey(0, 1)n = 701182632435564▲ i

heap[0] = 1. The tree is still complete, but heap order may now be broken around this node.

10i816223345546changeKey(0, 1)n = 701182632435564▲ i

1 < 9: the key decreased, so it can only conflict with its children → call sink(0).

10i81left,j62right23345546sinkn = 701182632435564▲ i▲ leftj▲ right

Children 8 and 6 → the larger is 8, j = 1.

8011i6223345546sinkn = 708112632435564▲ i

1 < 8 → swap (0 ↔ 1); the larger child moves up, i = 1.

8011i6223left34right,j5546sinkn = 708112632435564▲ i▲ left▲ rightj

Children 2 and 3 → the larger is 3, j = 4.

8031622314i5546sinkn = 708132632415564▲ i

1 < 3 → swap (1 ↔ 4); the larger child moves up, i = 4.

8031622314i5546sinkn = 708132632415564▲ i

i = 4: 2·4 + 1 = 9 ≥ n → a leaf, so the loop ends.

80316223145546changeKey(0, 1)n = 708132632415564

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)

40113223164k951061478879buildHeap()n = 1004112332416596107148897▲ k

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).

40113223164i951061478879left,jsink(4)n = 1004112332416596107148897▲ i▲ leftj

k = 4 → sink(4). Only a left child: index 9 (7) → j = 9.

40113223164i951061478879jsink(4)n = 1004112332416596107148897▲ i▲ j

heap[4] = 16 ≥ heap[9] = 7 → no smaller than either child, so heap order holds: return.

40113223i16495106147left,j88right79sink(3)n = 1004112332416596107148897▲ i▲ leftj▲ right

k = 3 → sink(3). Left child 7 (14), right child 8 (8) → the larger is 14, j = 7.

4011321431649510627i8879sink(3)n = 1004112331441659610728897▲ i

2 < 14 → swap (index 3 ↔ 7); the larger child rises, i = 7.

4011321431649510627i8879sink(3)n = 1004112331441659610728897▲ i

i = 7: 2·7 + 1 = 15 ≥ n = 10 → no children (a leaf), so the loop ends.

401132i14316495left106right,j278879sink(2)n = 1004112331441659610728897▲ i▲ left▲ rightj

k = 2 → sink(2). Left child 5 (9), right child 6 (10) → the larger is 10, j = 6.

40111021431649536i278879sink(2)n = 1004112103144165963728897▲ i

3 < 10 → swap (index 2 ↔ 6); the larger child rises, i = 6.

40111021431649536i278879sink(2)n = 1004112103144165963728897▲ i

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

4011i102143left164right,j9536278879sink(1)n = 1004112103144165963728897▲ i▲ left▲ rightj

k = 1 → sink(1). Left child 3 (14), right child 4 (16) → the larger is 16, j = 4.

4016110214314i9536278879sink(1)n = 1004116210314415963728897▲ i

1 < 16 → swap (index 1 ↔ 4); the larger child rises, i = 4.

4016110214314i9536278879left,jsink(1)n = 1004116210314415963728897▲ i▲ leftj

Only a left child: index 9 (7) → j = 9.

40161102143749536278819isink(1)n = 1004116210314475963728891▲ i

1 < 7 → swap (index 4 ↔ 9); the larger child rises, i = 9.

40161102143749536278819isink(1)n = 1004116210314475963728891▲ i

i = 9: 2·9 + 1 = 19 ≥ n = 10 → no children (a leaf), so the loop ends.

40i161left,j102right143749536278819sink(0)n = 1004116210314475963728891▲ i▲ leftj▲ right

k = 0 → sink(0). Left child 1 (16), right child 2 (10) → the larger is 16, j = 1.

16041i102143749536278819sink(0)n = 1001614210314475963728891▲ i

4 < 16 → swap (index 0 ↔ 1); the larger child rises, i = 1.

16041i102143left,j74right9536278819sink(0)n = 1001614210314475963728891▲ i▲ leftj▲ right

Left child 3 (14), right child 4 (7) → the larger is 14, j = 3.

16014110243i749536278819sink(0)n = 1001611421034475963728891▲ i

4 < 14 → swap (index 1 ↔ 3); the larger child rises, i = 3.

16014110243i74953627left88right,j19sink(0)n = 1001611421034475963728891▲ i▲ left▲ rightj

Left child 7 (2), right child 8 (8) → the larger is 8, j = 8.

160141102837495362748i19sink(0)n = 1001611421038475963728491▲ i

4 < 8 → swap (index 3 ↔ 8); the larger child rises, i = 8.

160141102837495362748i19sink(0)n = 1001611421038475963728491▲ i

i = 8: 2·8 + 1 = 17 ≥ n = 10 → no children (a leaf), so the loop ends.

16014110283749536274819buildHeap()n = 1001611421038475963728491

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 heightNodesMax swapsTotal
0 (leaves)800
1414
2224
3 (root)133
n = 1511
  • 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.
The cost of down-heap depends on a node's height; the cost of up-heap depends on its depth. Most nodes are near the bottom.

Heap Construction and Heap Sort

Heap Sort

Phase 1: build the heapTurn the array into a max-heap bottom-up. O(n)
Phase 2: n − 1 removeMax stepsSwap the root (the largest) with the item at the end, shrink the heap by one, and down-heap the root. O(n log n)
402132135645max-heap (size 4)sorted part← root here
  • 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;
  }
}
The same sinkSame as in MaxHeap, except that the array and the heap size are passed as parameters.
Try it{4, 1, 3, 2, 16, 9, 10, 14, 8, 7}→ {1, 2, 3, 4, 7, 8, 9, 10, 14, 16}

Heap Construction and Heap Sort · Step through the code

Code Trace: heapSort([6, 5, 3, 4, 1, 2])

605132431425heapSort(a)heap size = 6061523344152

Input a = [6, 5, 3, 4, 1, 2]. The whole array is the heap region; the sorted part on the right is still empty.

6051324314251) buildheap size = 6061523344152

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.

205132431465last = 5heap size = 5021523344156faded: sorted part

last = 5: the root (the largest, 6) swaps with a[5] = 2 → 6 is in its final place; heap size 5.

5021i32431465sink(a, 0, 5)heap size = 5051223344156▲ ifaded: sorted part

sink(a, 0, 5): at i = 0, the larger child a[1] = 5 > 2 → swap (0 ↔ 1).

50413223i1465sink(a, 0, 5)heap size = 5051423324156▲ ifaded: sorted part

sink(a, 0, 5): at i = 1, the larger child a[3] = 4 > 2 → swap (1 ↔ 3). Index 3 is a leaf → sink ends.

104132235465last = 4heap size = 4011423324556faded: sorted part

last = 4: the root (the largest, 5) swaps with a[4] = 1 → 5 is in its final place; heap size 4.

4011i32235465sink(a, 0, 4)heap size = 4041123324556▲ ifaded: sorted part

sink(a, 0, 4): at i = 0, the larger child a[1] = 4 > 1 → swap (0 ↔ 1).

40213213i5465sink(a, 0, 4)heap size = 4041223314556▲ ifaded: sorted part

sink(a, 0, 4): at i = 1, the larger child a[3] = 2 > 1 → swap (1 ↔ 3). Index 3 is a leaf → sink ends.

102132435465last = 3heap size = 3011223344556faded: sorted part

last = 3: the root (the largest, 4) swaps with a[3] = 1 → 4 is in its final place; heap size 3.

302112i435465sink(a, 0, 3)heap size = 3031221344556▲ ifaded: sorted part

sink(a, 0, 3): at i = 0, the larger child a[2] = 3 > 1 → swap (0 ↔ 2). Index 2 is a leaf → sink ends.

102132435465last = 2heap size = 2011223344556faded: sorted part

last = 2: the root (the largest, 3) swaps with a[2] = 1 → 3 is in its final place; heap size 2.

2011i32435465sink(a, 0, 2)heap size = 2021123344556▲ ifaded: sorted part

sink(a, 0, 2): at i = 0, the larger child a[1] = 2 > 1 → swap (0 ↔ 1). Index 1 is a leaf → sink ends.

102132435465last = 1heap size = 1011223344556faded: sorted part

last = 1: the root (the largest, 2) swaps with a[1] = 1 → 2 is in its final place; heap size 1.

102132435465sink(a, 0, 1)heap size = 1011223344556faded: sorted part

sink(a, 0, 1): heap size 1, the root has no children → nothing to do.

102132435465sortedheap size = 0011223344556

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:

Implementationn × insertn × removeMinResulting algorithm
Unsorted listO(n)O(n²)Selection sort, O(n²)
Sorted listO(n²)O(n)Insertion sort, O(n²)
Binary heapO(n log n)O(n log n)Heap sort, O(n log n)
Same ADT, different data structure: the running time drops from O(n²) to O(n log n). A concrete case of the ADT vs. data structure distinction.

Heap Construction and Heap Sort

Kinds of Heaps: d-ary, Binomial, Fibonacci

97845236113-ary max-heap: children 3i+1, 3i+2, 3i+3
  • 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 typeinsertremoveMindecrease keymerge
Binary heapO(log n)O(log n)O(log n)O(n)
d-ary heapO(logd n)O(d logd n)O(logd n)O(n)
Binomial heapO(log n)O(log n)O(log n)O(log n)
Fibonacci heapO(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/poll are O(log n); peek and size are O(1); remove(obj) and contains are 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

new PriorityQueuesize = 00123

An empty PriorityQueue. Internally it keeps a binary min-heap in an array (queue), with the smallest item at the root.

50offer(5)size = 105123

offer(5): the heap was empty, so 5 becomes the root (index 0).

50parent11newoffer(1)size = 2051123▲ new▲ parent

offer(1): added at index 1; its parent 5 > 1 → min-heap order is broken.

10root51offer(1)size = 2011523▲ root

siftUp (up-heap): 1 moves up to the root → queue = [1, 5]. Cost: O(log n).

10parent5182newoffer(8)size = 30115283▲ new▲ parent

offer(8): added at index 2; parent 1 ≤ 8 → no up-heap needed.

10root5182peek()size = 30115283▲ root

peek(): returns the root, 1, but does not remove it → O(1).

8051poll()size = 2081523

poll(): takes the root, 1; the last item, 8, moves to the root; size = 2.

5081poll()size = 2051823

siftDown (down-heap): 8 swaps with its smaller child → queue = [5, 8]. 1 is printed.

80poll()size = 108123

poll(): takes the root, 5; the last item, 8, moves to the root; size = 1.

80poll()size = 108123

One item is left, so no siftDown is needed. 5 is printed.

80size()size = 108123

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)

a501192337465pq (min-heap)(empty)queue[]size = 00123root = smallest of the k largest

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.

a501192337465▲ xpq (min-heap)5queue[]size = 150123root = smallest of the k largest

x = 5: offer(5) → added at the end, up-heap. size = 1.

a501192337465▲ xpq (min-heap)5queue[]size = 150123root = smallest of the k largest

size = 1 ≤ k = 3 → nothing to drop yet.

a501192337465▲ xpq (min-heap)15queue[]size = 2105123root = smallest of the k largest

x = 1: offer(1) → added at the end, up-heap (it swam up: 1 swap). size = 2.

a501192337465▲ xpq (min-heap)15queue[]size = 2105123root = smallest of the k largest

size = 2 ≤ k = 3 → nothing to drop yet.

a501192337465▲ xpq (min-heap)159queue[]size = 31051923root = smallest of the k largest

x = 9: offer(9) → added at the end, up-heap. size = 3.

a501192337465▲ xpq (min-heap)159queue[]size = 31051923root = smallest of the k largest

size = 3 ≤ k = 3 → nothing to drop yet.

a501192337465▲ xpq (min-heap)1395queue[]size = 410319253root = smallest of the k largest

x = 3: offer(3) → added at the end, up-heap (it swam up: 1 swap). size = 4.

a501192337465▲ xpq (min-heap)359queue[]size = 33051923dropped: 1root = smallest of the k largest

size = 4 > k → poll(): the smallest, 1, is dropped (it cannot be among the k largest). Last item to the root, down-heap.

a501192337465▲ xpq (min-heap)3597queue[]size = 430519273root = smallest of the k largest

x = 7: offer(7) → added at the end, up-heap. size = 4.

a501192337465▲ xpq (min-heap)579queue[]size = 35071923dropped: 3root = smallest of the k largest

size = 4 > k → poll(): the smallest, 3, is dropped (it cannot be among the k largest). Last item to the root, down-heap.

a501192337465▲ xpq (min-heap)5697queue[]size = 450619273root = smallest of the k largest

x = 6: offer(6) → added at the end, up-heap (it swam up: 1 swap). size = 4.

a501192337465▲ xpq (min-heap)679queue[]size = 36071923dropped: 5root = smallest of the k largest

size = 4 > k → poll(): the smallest, 5, is dropped (it cannot be among the k largest). Last item to the root, down-heap.

a501192337465pq (min-heap)679queue[]size = 36071923root = smallest of the k largest

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

High (0)P4Medium (1)P1P5Low (2)P2P3highest firstnon-empty levelCPU
Strict priorityThe CPU always picks the process at the front of the highest-priority non-empty queue.
FIFO within a levelProcesses with the same priority run in arrival order in their own queue (round robin if needed).
Starvation and agingLow-priority processes may never get to run (starvation). Raising a process's priority as it waits (aging) fixes this.

Simulator: Priority Scheduler Simulator (three levels, a time quantum, optional aging).

Summary

Summary

Priority queueinsert, min, removeMin. Items leave in priority order.
Heap propertyComplete binary tree + parent ≥ child (max) or ≤ child (min).
Array representationchildren 2i+1, 2i+2 · parent ⌊(i−1)/2⌋
OperationHeapHow?
peek (max/min)O(1)heap[0]
insertO(log n)put at heap[n], up-heap
removeMax / removeMinO(log n)move the last item to the root, down-heap
building a heapO(n)down-heap from ⌊n/2⌋ − 1 down to 0
heap sortO(n log n)build + move the root to the end n − 1 times

Summary

Interactive Simulators

Binary Heap SimulatorInsert, remove the max or min, and watch up-heap and down-heap in both the tree and the array view.
Priority Queue SimulatorThe ADT's behavior as you insert and remove prioritized items.
Priority Scheduler SimulatorOperating-system-style priority scheduling.

Summary

Common Mistakes

"A heap is sorted"It is not; only the root is determined. After inserting 3, 1, 2 into a PriorityQueue, println(pq) → [1, 3, 2].
Index formulas0-based: 2i+1, 2i+2, (i−1)/2. 1-based: 2i, 2i+1, i/2. Mixing the two scrambles the tree.
Right-child boundForget the j + 1 < n check and you read an item outside the heap (one already removed).
Wrong childIn a max-heap, down-heap swaps with the larger child; picking the smaller one breaks the order.
Searching a heapcontains and remove(obj) are O(n). Fast access needs a locator or a map.
Wrong directionPriorityQueue is a min-heap by default; for a max-heap, use Comparator.reverseOrder().

Summary

Sample Problems

  1. Insert 5, 12, 8, 3, 15, 10 in this order into an empty max-heap. What is the final array?
  2. Apply removeMax() once to the heap from problem 1. What is returned, and what is the new array?
  3. Turn the array {3, 1, 6, 5, 2, 4} into a max-heap bottom-up.
  4. In a 0-based array, at which indices are the parent and the children of the node at index 13?
  5. Is {10, 7, 9, 8, 2, 5} a max-heap? If not, why not?
  6. What does it cost to find the largest item in a min-heap?
Show answers
  1. {15, 12, 10, 3, 5, 8}
  2. Returns 15; the new array is {12, 8, 10, 3, 5}.
  3. {6, 5, 4, 1, 2, 3}
  4. Parent ⌊(13 − 1)/2⌋ = 6; children 27 and 28.
  5. No: the 7 at index 1 has a child 8 (index 3) that is larger.
  6. O(n): the largest item is one of the leaves, and all of the roughly n/2 leaves must be checked.

Summary

Exercises

  1. Apply heap sort to [4, 10, 3, 5, 1]. What is the array after each step?
  2. What is the height of a binary heap with 100 items? At most how many swaps can one insertion make?
  3. In a max-heap with distinct keys, at which indices can the 2nd and 3rd largest items be?
  4. Rewrite isHeap for a min-heap. What does it return for an empty array and for a one-item array?
  5. How would you merge k sorted lists with N items in total using a min-heap? What does it cost?
  6. In the max-heap [9, 7, 8, 3, 5, 6, 4], what is the array after changeKey(5, 10)?
Answers and hints
  1. 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].
  2. ⌊log₂ 100⌋ = 6; at most 6 swaps (leaf → root).
  3. The 2nd largest is a child of the root: index 1 or 2. The 3rd largest: any of indices 1–6.
  4. if (a[p] > a[i]) return false; The loop never runs → true in both cases.
  5. 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. 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 PriorityQueue API documentation.

The End

Chapter 11: Priority Queues and Heaps
Next deck: Chapter 12: Maps and Hash Tables · Study questions · Simulators: Binary Heap Simulator, Priority Queue Simulator, Priority Scheduler Simulator · Glossary
1 / 1 Sercan KÜLCÜ. All rights reserved.