← All Posts
DSA · Heaps · Part 5 of 17

std::priority_queue & the Heap Algorithms

Parts 1–4 built a heap from scratch so the machinery is not a black box. In production you will almost always use the standard library instead. This post maps everything we built onto what C++ ships, and settles the comparator convention that trips up nearly everyone the first time.

For an exhaustive tour of the STL surface — custom comparators over structs, lambdas, pairs, and the full algorithm family — see the dedicated C++ STL: Heaps & Priority Queues post. Here we focus on the correspondence with the data structure.

The Default Is a MAX-Heap

This is the single most common surprise. std::priority_queue defaults to std::less, and that yields a max-heaptop() is the largest element.

#include <queue>

std::priority_queue<int> pq;      // MAX-heap by default
pq.push(3); pq.push(9); pq.push(5);
pq.top();                          // 9, not 3

Meanwhile std::map, std::set and std::sort also default to std::less and give you ascending order. So the same comparator produces smallest-first everywhere else and largest-first here. That inconsistency is the source of the confusion, and it is worth understanding rather than memorising.

Why it is actually consistent. The rule for a priority queue is: Compare(x, y) == true means “x comes before y in the ordering, so x has lower priority”. The element that sorts last under the comparator is the one at the top. With std::less, the last element in ascending order is the maximum — hence a max-heap. Read it as “top = the element that would sort to the end” and every case follows without memorisation.

Getting a Min-Heap

Flip the comparator to std::greater. The template signature requires you to name the container explicitly, because Compare is the third parameter:

#include <queue>
#include <vector>
#include <functional>

std::priority_queue<int, std::vector<int>, std::greater<int>> minpq;
minpq.push(3); minpq.push(9); minpq.push(5);
minpq.top();                       // 3

Since C++14 you can write std::greater<> and let it deduce. A common shorthand worth keeping in a header:

template <class T>
using MinHeap = std::priority_queue<T, std::vector<T>, std::greater<T>>;

MinHeap<int> pq;                   // reads much better at the call site

The negation trick — pushing -x into a max-heap to simulate a min-heap — works for signed integers and is popular in contests for its brevity. Be aware of its failure modes: it breaks on unsigned types, on floating point with signed zero, and on INT_MIN (whose negation is UB). It also makes the code lie about its intent. Prefer std::greater in anything you will read again.

Mapping to What We Built

Our implementationStandard libraryCost
top()pq.top()O(1)
push() + sift_uppq.push() / pq.emplace()O(log n)
pop() + sift_downpq.pop()O(log n)
build_heap()range constructor, std::make_heapO(n)
is_valid()std::is_heapO(n)

Note pop() returns void. To consume the top you must read it first — a deliberate design choice, because returning by value while popping cannot be made exception-safe:

int best = pq.top();      // read
pq.pop();                 // then remove

The std::*_heap Algorithms

std::priority_queue is an adaptor: it wraps a container and hides it, so you cannot iterate the elements. When you need the underlying array — to inspect it, to sort it in place, or to implement something the adaptor does not expose — use the algorithm family directly on any random-access range:

#include <algorithm>
#include <vector>

std::vector<int> v = {9, 4, 7, 1, 8, 3};

std::make_heap(v.begin(), v.end());        // O(n)   -> max-heap
std::is_heap  (v.begin(), v.end());        // O(n)   -> validity check

v.push_back(11);
std::push_heap(v.begin(), v.end());        // O(log n) sift-up the LAST element

std::pop_heap (v.begin(), v.end());        // O(log n) move max to the BACK
int biggest = v.back();
v.pop_back();

std::sort_heap(v.begin(), v.end());        // O(n log n) -> fully sorted ascending

The contract to remember: push_heap assumes the new element is already at the back and everything before it is a valid heap. pop_heap does not remove anything — it swaps the root to the back and re-heapifies the prefix, leaving the erase to you. Both are exactly the sift_up / sift_down from Part 3 with the boundary handling exposed.

Custom Comparators

For your own types, a stateless functor is the clearest option:

struct Task {
    int  priority;
    long deadline;
};

struct ByPriorityThenDeadline {
    bool operator()(const Task& x, const Task& y) const {
        if (x.priority != y.priority) return x.priority < y.priority;  // higher priority first
        return x.deadline > y.deadline;                                 // earlier deadline first
    }
};

std::priority_queue<Task, std::vector<Task>, ByPriorityThenDeadline> queue;

Read it through the “top = sorts last” rule: x.priority < y.priority returns true when x is less important, so the highest priority sorts last and lands on top. The tie-break reverses the comparison because a smaller deadline should win.

A lambda works too, but the type must be threaded through the template, so it is wordier:

auto cmp = [](const Task& x, const Task& y) { return x.priority < y.priority; };
std::priority_queue<Task, std::vector<Task>, decltype(cmp)> pq(cmp);

Since C++20 a captureless lambda is default-constructible, so the (cmp) argument can be dropped. Before C++20 it is mandatory — omitting it fails to compile.

Strict weak ordering is not optional. Your comparator must be irreflexive (cmp(x,x) is false), antisymmetric, and transitive. The classic violation is writing <= instead of <: it makes cmp(x,x) true, which breaks the ordering axioms and permits the library to walk off the end of the container. It is undefined behaviour, it usually does not crash immediately, and it is very hard to debug. Part 15 returns to this with the full list of comparator traps.

What the Adaptor Cannot Do

Four hard limitations, each of which motivates a later post:

Next

Foundations are complete: you can build a heap by hand or reach for the library one, and you know precisely which operations each supports. Part 6 starts the techniques half of the series with the algorithm that falls straight out of build plus repeated pop.