Command Palette

Search for a command to run...

MTH 4300

Lecture 17

What Is a Priority Queue?

A regular queue (lecture 16) serves elements in the order they arrive — first in, first out. A priority queue serves elements in priority order — the element with the highest priority is always dequeued next, regardless of insertion order.

The three core operations are:

  • push(value) — insert an element
  • pop() — remove the highest-priority element
  • top() — read the highest-priority element without removing it

Binary Heaps

The standard implementation of a priority queue is a binary max-heap: a complete binary tree where every node's value is the values of its children. The largest element always sits at the root.

A complete binary tree maps neatly onto an array. For a node at index i:

Heap index relationships
parent      = (i - 1) / 2
left child  = 2 * i + 1
right child = 2 * i + 2

No pointers needed — the structure is encoded entirely by position.

PriorityQueue<T> — max-heap
template <typename T>
class PriorityQueue {
public:
    PriorityQueue() : size_(0), capacity_(8) {
        data_ = new T[capacity_];
    }

    ~PriorityQueue() { delete[] data_; }

    void push(const T& value) {
        if (size_ == capacity_) grow();
        data_[size_++] = value;
        sift_up(size_ - 1);
    }

    void pop() {
        if (size_ == 0) return;
        data_[0] = data_[--size_]; // move last element to root
        sift_down(0);
    }

    const T& top() const { return data_[0]; }

    bool empty() const { return size_ == 0; }
    int  size()  const { return size_; }

private:
    T*  data_;
    int size_;
    int capacity_;

    void sift_up(int i) {
        while (i > 0) {
            int parent = (i - 1) / 2;
            if (data_[i] > data_[parent]) {
                T tmp = data_[i]; data_[i] = data_[parent]; data_[parent] = tmp;
                i = parent;
            } else break;
        }
    }

    void sift_down(int i) {
        while (true) {
            int largest = i;
            int left    = 2 * i + 1;
            int right   = 2 * i + 2;
            if (left  < size_ && data_[left]  > data_[largest]) largest = left;
            if (right < size_ && data_[right] > data_[largest]) largest = right;
            if (largest == i) break;
            T tmp = data_[i]; data_[i] = data_[largest]; data_[largest] = tmp;
            i = largest;
        }
    }

    void grow() {
        capacity_ *= 2;
        T* next = new T[capacity_];
        for (int i = 0; i < size_; i++) next[i] = data_[i];
        delete[] data_;
        data_ = next;
    }
};

Push — Sift Up

push appends the new element at the end of the array, then calls sift_up to restore the heap property. sift_up repeatedly compares the element with its parent and swaps upward until the element is no longer larger than its parent (or reaches the root).

Push trace — inserting 40 into [50, 30, 20]
// Before: [50, 30, 20]    heap is valid
// Append 40: [50, 30, 20, 40]
// 40 at index 3, parent at index 1 (value 30) — 40 > 30, swap
// [50, 40, 20, 30]
// 40 at index 1, parent at index 0 (value 50) — 50 ≥ 40, stop
// After: [50, 40, 20, 30]

Pop — Sift Down

pop removes the root (the maximum). To avoid a gap, it moves the last element into the root position, then calls sift_down to restore the heap property. sift_down repeatedly swaps the element with its largest child until neither child is larger (or the element reaches a leaf).

Pop trace — removing 50 from [50, 40, 20, 30]
// Move last to root: [30, 40, 20]    (size_ decremented, 50 gone)
// sift_down(0): children are 40 (index 1) and 20 (index 2)
//   largest child = 40 — 40 > 30, swap → [40, 30, 20]
// sift_down(1): children are out of bounds — stop
// After: [40, 30, 20]

Both push and pop are O(log n) because the heap height is ⌊log₂ n⌋ and each sift operation does at most one swap per level.

Calling top() or pop() on an empty priority queue accesses data_[0] on an empty or invalid array. Always check empty() first.

Min-Heap Variant

A min-heap serves the smallest element first. The only change is flipping the comparison in sift_up and sift_down from > to <:

Min-heap — flip comparisons
// sift_up:    if (data_[i] < data_[parent])  // was >
// sift_down:  if (data_[left]  < data_[largest])  // was >
//             if (data_[right] < data_[largest])  // was >

Everything else — the index arithmetic, the array layout, push and pop structure — stays identical.

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>

int main() {
    PriorityQueue<int> pq;

    pq.push(10);
    pq.push(50);
    pq.push(30);

    std::cout << pq.top() << "\n"; // line A
    pq.pop();
    std::cout << pq.top() << "\n"; // line B
    pq.push(40);
    std::cout << pq.top() << "\n"; // line C
    std::cout << pq.size() << "\n"; // line D

    return 0;
}

Q2.

Without running the code, trace the heap array after each operation and predict the output.

#include <iostream>

int main() {
    PriorityQueue<int> pq;

    pq.push(5);   // array: [5]
    pq.push(15);  // array after sift_up?
    pq.push(10);  // array after sift_up?
    pq.push(20);  // array after sift_up?

    while (!pq.empty()) {
        std::cout << pq.top() << "\n";
        pq.pop();
    }

    return 0;
}

Q3.

Each snippet has a bug related to the heap. Identify what's wrong.

// Snippet A — pop writes EMPTY instead of moving the last element
void pop() {
    if (size_ == 0) return;
    data_[0] = T{};  // clears the root
    --size_;
    sift_down(0);
}

// Snippet B — sift_up compares with the wrong parent index
void sift_up(int i) {
    while (i > 0) {
        int parent = (i - 1) / 2;
        if (data_[i] > data_[i - 1]) { // wrong parent
            T tmp = data_[i]; data_[i] = data_[i - 1]; data_[i - 1] = tmp;
            i = i - 1;
        } else break;
    }
}

// Snippet C — sift_down picks the first child unconditionally
void sift_down(int i) {
    while (true) {
        int left = 2 * i + 1;
        if (left >= size_) break;
        if (data_[left] > data_[i]) {
            T tmp = data_[i]; data_[i] = data_[left]; data_[left] = tmp;
            i = left;
        } else break;
    }
}

// Snippet D — push does not call sift_up
void push(const T& value) {
    if (size_ == capacity_) grow();
    data_[size_++] = value;
    // heap property may now be violated
}

Q4.

Implement push and pop for the priority queue below. Use the provided sift_up and sift_down helpers.

#include <iostream>

template <typename T>
class PriorityQueue {
public:
    PriorityQueue() : size_(0), capacity_(8) {
        data_ = new T[capacity_];
    }
    ~PriorityQueue() { delete[] data_; }

    void push(const T& value) {
        // TODO: grow if necessary, append value, sift up
    }

    void pop() {
        // TODO: move last element to root, decrement size, sift down
    }

    const T& top() const { return data_[0]; }
    bool empty() const   { return size_ == 0; }
    int  size()  const   { return size_; }

private:
    T*  data_;
    int size_;
    int capacity_;

    void sift_up(int i) {
        while (i > 0) {
            int parent = (i - 1) / 2;
            if (data_[i] > data_[parent]) {
                T tmp = data_[i]; data_[i] = data_[parent]; data_[parent] = tmp;
                i = parent;
            } else break;
        }
    }

    void sift_down(int i) {
        while (true) {
            int largest = i;
            int left    = 2 * i + 1;
            int right   = 2 * i + 2;
            if (left  < size_ && data_[left]  > data_[largest]) largest = left;
            if (right < size_ && data_[right] > data_[largest]) largest = right;
            if (largest == i) break;
            T tmp = data_[i]; data_[i] = data_[largest]; data_[largest] = tmp;
            i = largest;
        }
    }

    void grow() {
        capacity_ *= 2;
        T* next = new T[capacity_];
        for (int i = 0; i < size_; i++) next[i] = data_[i];
        delete[] data_;
        data_ = next;
    }
};

int main() {
    PriorityQueue<int> pq;
    int values[] = {3, 1, 4, 1, 5, 9, 2, 6};
    for (int v : values) pq.push(v);

    while (!pq.empty()) {
        std::cout << pq.top() << "\n";
        pq.pop();
    }
    return 0;
}

Q5.

Implement sift_up and sift_down for the min-heap priority queue below. The min-heap always serves the smallest element first.

#include <iostream>

template <typename T>
class MinPriorityQueue {
public:
    MinPriorityQueue() : size_(0), capacity_(8) {
        data_ = new T[capacity_];
    }
    ~MinPriorityQueue() { delete[] data_; }

    void push(const T& value) {
        if (size_ == capacity_) grow();
        data_[size_++] = value;
        sift_up(size_ - 1);
    }

    void pop() {
        if (size_ == 0) return;
        data_[0] = data_[--size_];
        sift_down(0);
    }

    const T& top() const { return data_[0]; }
    bool empty() const   { return size_ == 0; }
    int  size()  const   { return size_; }

private:
    T*  data_;
    int size_;
    int capacity_;

    void sift_up(int i) {
        // TODO: sift toward root while smaller than parent
    }

    void sift_down(int i) {
        // TODO: sift toward leaves, swapping with the smallest child
    }

    void grow() {
        capacity_ *= 2;
        T* next = new T[capacity_];
        for (int i = 0; i < size_; i++) next[i] = data_[i];
        delete[] data_;
        data_ = next;
    }
};

int main() {
    MinPriorityQueue<int> pq;
    int values[] = {3, 1, 4, 1, 5, 9, 2, 6};
    for (int v : values) pq.push(v);

    while (!pq.empty()) {
        std::cout << pq.top() << "\n";
        pq.pop();
    }
    return 0;
}

Practice Problems