Learn C++ STL

Lesson 1 of 9 ¡ priority_queue: The Biggest One Comes Out First

Module 7 ¡ priority_queue: The Biggest One Comes Out First

priority_queue: The Largest Leaves First, Whenever It Came

FreeReading

In this lesson

  • Declare a std::priority_queue, push, look with top() and pop, and say which value comes back first: the largest, whatever the order of arrival.
  • Picture the heap inside: a tree kept in a vector, level by level. Explain why a push climbs and a pop sinks at most about log2 n levels.
  • Write the read-then-pop idiom behind an empty() check, and a smallest-first heap in one line with greater<int>.

Maria runs the help desk of the school's computer lab, and every report there gets an urgency number. She always takes the most urgent open report next, whenever it came in. Module 6's queue would make the projector that died before an exam wait behind forty password resets. A vector would need a full scan, or a sort, before every take. This lesson gives her a container that hands back the largest first, and shows why that is cheap.

Who leaves first: the largest

Module 6's lesson 04 asked one question of every job: who leaves first? A stack answers the newest, and a queue the oldest. Maria needs the third answer, the most important, and its container is the priority_queue: it always gives back the largest value it holds. Here the same four urgencies arrive at both.

A queue and a priority_queue given the same arrivals The same four arrivals, two orders out they arrive: 4 9 2 7 queue<int> the oldest leaves first priority_queue<int> the largest leaves first they leave: they leave: 4 9 2 7 9 7 4 2 the same order they came in 9 came second and leaves first

The queue gives the urgencies back as they came. The priority_queue gives back 9 first, although it arrived second. So with a priority_queue the order of arrival never decides; only the values do.

A heap: a tree with one promise

To find the largest quickly, push after push, a priority_queue keeps its values in a heap, a kind of tree. Each value sits in a node, and a node can have up to two children under it. The node right above a child is its parent, and the top node, with no parent, is the root.

A heap keeps one promise: every parent is at least as large as its children. So the values never shrink on the way up, and the root holds the largest of all. Nothing else is promised: two children can be in either order. So the largest always waits at the root, while the rest is only partly sorted.

The tree lives in a vector

The tree is not built from separate nodes. It is written into one vector, level by level and left to right. A level is one row of the tree, and a slot is one position in the vector, counted from 0. The drawing shows the heap that the next section builds.

A heap's tree and the vector that holds it The heap after six pushes: a tree, kept in a vector level 0 level 1 level 2 9 7 8 4 5 2 slot 0 slot 1 slot 2 slot 3 slot 4 slot 5 slot 6 is free the vector: 9 7 8 4 5 2 slot: 0 1 2 3 4 5 6 level 0 level 1 level 2

Each level holds twice as many slots as the one above: 1, 2, 4, 8 and so on. So 7 values fill 3 levels, and 1,000,000 values need only 20. That number is about log2 n, how many times you can halve n before you reach 1. Lesson 05 works out which slot is whose parent. So a value moves through at most about log2 n levels, which keeps a heap fast.

A push climbs

Now watch the heap move. Six values go into an empty priority_queue<int>: 4, 9, 2, 7, 5 and 8. Step through them and watch where each new value lands, and how far it climbs.

Six pushes into one priority_queue<int>, with the vector written slot 0 first after every step.

StepThe vector after it, slot 0 firstWhat happened
priority_queue<int> pq(empty)The heap starts empty: no slot is used yet.
push(4)44 goes into slot 0, the first free slot, and is the top.
push(9)4 99 goes into the next free slot, slot 1, under 4.
sift up9 49 is larger than its parent 4, so the two swap and 9 is the top.
push(2)9 4 22 goes into slot 2 and stays, because its parent 9 is larger.
push(7)9 4 2 77 goes into slot 3, under 4.
sift up9 7 2 47 beats its parent 4 and swaps up, then stops under 9.
push(5)9 7 2 4 55 goes into slot 4 and stays, because its parent 7 is larger.
push(8)9 7 2 4 5 88 goes into slot 5, under 2.
sift up9 7 8 4 5 28 beats its parent 2 and swaps up, then stops under 9.

Every push did the same two things. The new value went into the next free slot, on the bottom level. Then it climbed, swapping with its parent while it was larger; each such swap is called a sift up. 9 climbed to the root, 7 and 8 one level each, and 2 and 5 not at all. So a push makes at most one swap per level.

A pop sinks

A pop removes the root, and the heap must close the gap without breaking its promise. Watch two pops on the heap the six pushes built.

Two pops on the heap the six pushes built, with the vector written slot 0 first after every step.

StepThe vector after it, slot 0 firstWhat happened
after the six pushes9 7 8 4 5 2The heap the six pushes built: 9 is the top.
pop()2 7 8 4 5pop() removes 9, and the last value, 2, moves into slot 0.
sift down8 7 2 4 52 is smaller than its larger child, 8, so they swap and 8 is the top.
pop()5 7 2 4pop() removes 8, and the last value, 5, moves into slot 0.
sift down7 5 2 45 swaps with its larger child, 7, and stops above 4.

Each pop moved the last value into slot 0, since a vector removes its last slot cheaply. Then that value sank: while its larger child was larger, the two swapped, a step called a sift down. The larger child goes up, so the promise holds. So a pop, like a push, moves one value through at most about log2 n levels.

After each whole push or pop, the widgets show the vector GCC 12's priority_queue held on Compiler Explorer. Inside a pop the library takes a shortcut to the same vector, which lesson 05 shows. So these pictures are the real layout.

The syntax you need on day one

std::priority_queue and its everyday calls

#include <queue>

priority_queue<T> pq;      an empty priority_queue of T
pq.push(x);                add x; it climbs to its place
pq.top()                   the largest element; it stays in
pq.pop();                  remove the largest; returns nothing
pq.size()    pq.empty()    how many, and whether there are none

#include <functional>     for greater
#include <vector>
priority_queue<T, vector<T>, greater<T>> small;
                           the smallest on top instead
  • priority_queue<T>: one thing in the angle brackets, the element type, as for a vector. It lives in <queue>, beside queue.
  • push, top and pop: the three names a stack uses, with a different rule for which element top shows.
  • top() and pop(): only when the priority_queue is not empty. Check empty() first.
  • greater<T>: turns the order around, so the smallest is on top. Lesson 02 explains the three things in the angle brackets.

So a priority_queue has the three working calls of a stack, push, top and pop, and top() always shows the largest.

push, top and pop

push(x) puts x into the heap. top() tells you the largest value inside now, and pop() removes it. This program prints the top and the size after each change.

#include <iostream>
#include <queue>
using namespace std;

int main() {
    priority_queue<int> pq;
    pq.push(4);
    pq.push(9);
    pq.push(2);
    cout << "top " << pq.top() << ", size " << pq.size() << '\n';
    pq.pop();
    cout << "top " << pq.top() << ", size " << pq.size() << '\n';
    pq.push(7);
    cout << "top " << pq.top() << ", size " << pq.size() << '\n';
    return 0;
}
top 9, size 3
top 4, size 2
top 7, size 3

After the pop, 4 became the top, because it was the largest of what was left. Then 7 arrived and went straight to the top, ahead of the older 4. So top() always answers with the largest value still inside, whenever it came.

pop returns nothing: read first, then pop

Bob wants the largest value and its removal in one line, so he writes this.

priority_queue<int> pq;
pq.push(4);
int x = pq.pop();

GCC 12 stops at the runner's flags with error: void value not ignored as it ought to be. void means "no value": pop() removes the top and hands nothing back, just like a stack's pop() in Module 6. The fix is two lines, a read and then a pop.

#include <iostream>
#include <queue>
using namespace std;

int main() {
    priority_queue<int> pq;
    pq.push(4);
    pq.push(9);
    pq.push(2);
    int x = pq.top();
    pq.pop();
    cout << "took " << x << ", now the top is " << pq.top() << '\n';
    return 0;
}
took 9, now the top is 4

So the pair you met in Module 6 stays: look with top(), then pop().

The smallest first, in one line

Some jobs want the smallest first: the cheapest fare, the shortest wait. Give the priority_queue two more things in its angle brackets: the vector it keeps inside, and greater<int>, which turns the order around. Lesson 02 explains both; for now, copy the line.

#include <functional>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main() {
    priority_queue<int, vector<int>, greater<int>> pq;
    for (int x : {4, 9, 2, 7, 5, 8}) {
        pq.push(x);
    }
    cout << "smallest first:";
    while (!pq.empty()) {
        cout << ' ' << pq.top();
        pq.pop();
    }
    cout << '\n';
    return 0;
}
smallest first: 2 4 5 7 8 9

The loop while (!pq.empty()) reads the top, prints it and pops it until nothing is left, which is called a drain. So one longer declaration turns the largest-first heap into a smallest-first one, and every call stays the same.

The empty priority_queue: Zara's first test

Zara always tries the empty case first. She asks an empty priority_queue for its top.

priority_queue<int> pq;
cout << "top: " << pq.top() << '\n';

It compiled with no message at the runner's flags. On Compiler Explorer's GCC 12.2, one run printed nothing and crashed with SIGSEGV, the signal for reading memory it does not own. Even top: never appeared, because cout still held it. Then she pops an empty one.

priority_queue<int> pq;
pq.pop();
cout << "size after pop: " << pq.size() << '\n';

That run printed size after pop: 18446744073709551615 and ended normally. The pop slid the vector's end below its start, so the size wrapped around to the largest number it can hold. A push(7) and a top() after it crashed with SIGSEGV. Both are undefined behaviour: the C++ standard says nothing about what happens. So write if (!pq.empty()) before every top() or pop() whose input you do not control.

Example 1: three pushes, popped largest first

The smallest priority_queue program: three pushes, then a drain.

#include <iostream>
#include <queue>
using namespace std;

int main() {
    priority_queue<int> pq;
    pq.push(5);
    pq.push(1);
    pq.push(8);
    while (!pq.empty()) {
        cout << "pop " << pq.top() << '\n';
        pq.pop();
    }
    return 0;
}
pop 8
pop 5
pop 1

The values went in as 5, 1, 8 and came out as 8, 5, 1: only their size decided the order. The loop test !pq.empty() is Zara's check built into the loop, so top() never meets an empty heap.

Run in Compiler
Example 2: the best score so far

Kenji pushes each player's score as the player finishes, and prints the best so far. At the end, the best player goes home.

#include <iostream>
#include <queue>
using namespace std;

int main() {
    priority_queue<int> scores;
    for (int s : {120, 340, 90, 410, 275}) {
        scores.push(s);
        cout << "pushed " << s << ", best so far " << scores.top() << '\n';
    }
    scores.pop();
    cout << "the best player leaves; next best " << scores.top() << '\n';
    return 0;
}
pushed 120, best so far 120
pushed 340, best so far 340
pushed 90, best so far 340
pushed 410, best so far 410
pushed 275, best so far 410
the best player leaves; next best 340

Kenji says one variable could track the best score. True, until the best player leaves: the heap still knows the next best, 340. So a heap earns its place when the largest also leaves.

Run in Compiler
Example 3: words, the largest first

A priority_queue works with any type that < can compare. For strings, < means dictionary order, as Module 3's lesson 02 showed.

#include <iostream>
#include <queue>
#include <string>
using namespace std;

int main() {
    priority_queue<string> words;
    words.push("pear");
    words.push("apple");
    words.push("fig");
    words.push("banana");
    cout << "largest first:";
    while (!words.empty()) {
        cout << ' ' << words.top();
        words.pop();
    }
    cout << '\n';
    return 0;
}
largest first: pear fig banana apple

Maria asks why pear counts as the largest. Strings compare letter by letter, and p comes after b, f and a, so pear is the last in dictionary order. So "largest" always means "last in the order < gives", whatever the type.

Run in Compiler
Example 4: exam marks, from the highest to the lowest

The program a beginner would actually write: read the marks, push each one, then drain the heap to print them from the highest down. Its input is the number of marks, then the marks themselves.

#include <iostream>
#include <queue>
using namespace std;

int main() {
    int n;
    cin >> n;
    priority_queue<int> marks;
    for (int i = 0; i < n; i++) {
        int m;
        cin >> m;
        marks.push(m);
    }
    int place = 1;
    while (!marks.empty()) {
        cout << place << ". " << marks.top() << '\n';
        marks.pop();
        place++;
    }
    return 0;
}
1. 92
2. 92
3. 88
4. 81
5. 75
6. 67
7. 59

The two 92s both came out: a heap keeps equal values. Pushing everything, then draining, sorts the values from the largest down, and lesson 04 measures that against one call to sort.

Run in Compiler

Where this is used

  • Python's heapq module. It keeps a heap in a plain Python list, where every parent is no larger than its children. So the smallest item is always at heap[0].
  • Java's java.util.PriorityQueue. Its documentation says it is based on a priority heap and that its head is the least element. So Java gives back the smallest first by default, the opposite of C++.
  • libuv, the event-loop library under Node.js. It keeps every running timer in a heap ordered by the time each one is due. The loop reads the top to know how long it may sleep.

Common mistakes

1. Forgetting the header.

#include <iostream>
using namespace std;

int main() {
    priority_queue<int> pq;
    pq.push(5);
    cout << pq.top() << '\n';
    return 0;
}

An error at the runner's flags: error: 'priority_queue' was not declared in this scope, then GCC 12's own fix: note: 'std::priority_queue' is defined in header '<queue>'; did you forget to '#include <queue>'?. There is no header called <priority_queue>: it shares <queue> with queue. You will forget it because the name sounds like a header of its own.

2. Looking at the front, as on a queue.

priority_queue<int> pq;
pq.push(5);
cout << pq.front() << '\n';

An error at the runner's flags: error: 'class std::priority_queue<int>' has no member named 'front', with no "did you mean". Its one look is top(). You will type front() because the name ends in queue.

3. The largest first when the job wanted the smallest.

priority_queue<int> fares;
fares.push(30);
fares.push(12);
fares.push(45);
cout << "cheapest fare first:";
while (!fares.empty()) {
    cout << ' ' << fares.top();
    fares.pop();
}
cout << '\n';

No message at any command line, and it prints cheapest fare first: 45 30 12. A plain priority_queue puts the largest on top, whatever the variable is called; declare it with greater<int> for the smallest. You will do this after Java or Python, where the smallest comes first by default.

Brain teaser

Zara pushes 1, 2, 3 and 4, in that order, into an empty priority_queue<int>. With the climb rule from the first widget, draw the tree after each push, and write its vector, slot 0 first.

In which slot does 1 end up, and how many swaps were made? Then push 5: is the vector still sorted from the largest down?

A new value starts in the next free slot and swaps with its parent while it is larger.

Exercise 1Easy

Each report at Maria's help desk has an urgency from 1 to 1000, and a bigger number is more urgent. When she is free, she takes the most urgent open report, whenever it came in. Sometimes she checks when none is open.

Input. A line with n, then n commands, one per line. add U opens a report with urgency U, and take takes the most urgent open report, which is then closed.

Output. One line for every take: the urgency taken, or the word none if no report is open. Then one last line: the urgencies still open, from the highest to the lowest, separated by single spaces, or the word empty.

Constraints. 1 <= n <= 100000. 1 <= U <= 1000, and two reports may have the same urgency.

Sample. Input 8, then take, add 5, add 9, add 2, take, add 7, take, add 4 gives none, 9, 7 and 5 4 2. The first take finds nothing open, and 9 goes before the older 5.

#include <iostream>
#include <queue>
#include <string>
using namespace std;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n = 0;
    cin >> n;
    priority_queue<int> reports;
    for (int i = 0; i < n; i++) {
        string op;
        cin >> op;
        if (op == "add") {
            int urgency = 0;
            cin >> urgency;
            // Add the new report's urgency to reports.
        } else {
            // op is "take": print the highest urgency and remove it,
            // or print none if no report is open.
        }
    }

    // Print the urgencies still open, from the highest to the lowest,
    // separated by single spaces, or the word empty if none is left.

    return 0;
}

Graded as help-desk, a free problem in this module's problem set. The hidden tests include a take before any report, one report that is never taken, and equal urgencies. They also include a run that ends with every report taken, and 100000 commands.

Run in Compiler
Exercise 2Medium

Alice lines up her class for a photo in a zigzag. The tallest student stands first, then the shortest, then the second tallest, then the second shortest, and so on. Every student stands in the row exactly once.

Input. A line with n, then n heights in centimetres.

Output. One line: the n heights in Alice's order, separated by single spaces.

Constraints. 1 <= n <= 100000. Every height is between 100 and 250, and two students may be equally tall.

Sample. Input 6 and 150 162 158 171 149 165 gives 171 149 165 150 162 158.

#include <functional>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    priority_queue<int> tallest;
    priority_queue<int, vector<int>, greater<int>> shortest;
    for (int i = 0; i < n; i++) {
        int h;
        cin >> h;
        // Push h into both heaps.
    }

    // Print n heights on one line, separated by single spaces: the tallest,
    // then the shortest, then the next tallest, and so on. Every student
    // stands in the row exactly once, so print exactly n heights.

    return 0;
}

Not graded on its own. It practises both orders side by side, and the count that stops the zigzag before anyone stands twice.

Run in Compiler
Exercise 3Hard

A priority_queue never shows its vector, so David builds the same vector himself, with push_back and the first widget's climb rule.

Input. A line with n, then the n values pushed, in order.

Output. After each push, one line: the vector, slot 0 first, separated by single spaces.

Constraints. 1 <= n <= 1000. Every value is between 1 and 1000000, and equal values do not swap.

Sample. Input 6 and 4 9 2 7 5 8 gives the first widget's six vectors, one per line: 4, 9 4, 9 4 2, 9 7 2 4, 9 7 2 4 5 and 9 7 8 4 5 2.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> heap;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        heap.push_back(x);
        // x is now in the last slot. While it is larger than the value in
        // its parent's slot, swap the two and move up to the parent's slot.
        // Then print heap, slot 0 first, separated by single spaces.
    }
    return 0;
}

Not graded on its own. The idea the lesson only pointed at: the drawing of the slots. Slots 1 and 2 hang under slot 0, 3 and 4 under 1, and 5 and 6 under 2. Find the rule for a slot's parent, then let each value climb.

Run in Compiler

Common doubts

  • Why is it called a queue when it ignores the arrival order?

    Values wait in it and leave one at a time, as in a queue, but they leave by priority. Even equal priorities leave in no promised order; lesson 02 shows how to break a tie on purpose.

  • Why not sort a vector once and read it from the end?

    When every value is known before the first take, one sort is fine, and lesson 04 measures it. The heap wins when values keep arriving between takes, as at Maria's desk.

  • Can I see the second largest without popping the largest?

    Not through a priority_queue: top() is its only look. Lesson 04 shows which container to pick when a job needs more than the top.

Key takeaways

  • A priority_queue gives back the largest value first, whatever the order of arrival; with greater<int>, the smallest.
  • Inside is a heap: a tree kept in a vector level by level, every parent at least as large as its children.
  • A push climbs and a pop sinks through at most about log2 n levels: 20 levels hold a million values.
  • pop() returns nothing, so read top() first; int x = pq.pop(); gets void value not ignored as it ought to be.
  • top() or pop() on an empty priority_queue is undefined: one run crashed, another printed a size of 18446744073709551615. Check empty() first.
  • Go deeper: Under the Hood, the heap inside a vector and the sift arithmetic (Pro).

Next, lesson 02 goes through every operation of priority_queue, one by one, each with its cost, and every way to choose its order.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Every priority_queue Operation, One by One, With Its Cost