C++ In Practice - Priority Queues
Heap / Priority Queue (C++11 Deep Dive)
C++11 provides a robust, highly optimized standard library container: std::priority_queue. Additionally, the STL exposes low-level heap algorithms (std::make_heap, std::push_heap, std::pop_heap) if you need to treat a raw vector as a heap.
C++11 Architecture & std::priority_queue
Under the Hood:
- Container Adaptor:
std::priority_queueis not a container itself; it wraps a sequence container (default:std::vector). - Structure: It maintains the heap property (Max-Heap by default) using the underlying container's Random Access Iterators.
- Memory: Contiguous memory block. Cache-friendly.
- Complexity:
push(): O(log N) -push_backthenswim(bubble up).pop(): O(log N) -swap(root, last),pop_back, thensink(bubble down).top(): O(1).
Default Behavior (Max-Heap):
std::priority_queue<int> pq; // Max-Heap (Largest on top)
Min-Heap Declaration:
// Type, Underlying Container, Comparator
std::priority_queue<int, std::vector<int>, std::greater<int>> minPQ;
1. Top K Frequent Elements
Pattern: HashMap + Min-Heap. Complexity: Time O(N log K) | Space O(N).
C++11 Detail:
We store pair<count, value> in the heap. The comparator std::greater compares pairs lexicographically (first element, then second). This correctly prioritizes by count.
#include <vector>
#include <unordered_map>
#include <queue>
std::vector<int> topKFrequent(const std::vector<int>& nums, int k) {
// 1. Count frequencies O(N)
std::unordered_map<int, int> counts;
for (int num : nums) {
counts[num]++;
}
// 2. Min-Heap O(N log K)
// We want to KEEP the K largest, so we use a Min-Heap to evict the smallest.
// Pair: <Frequency, Value>
using Item = std::pair<int, int>;
std::priority_queue<Item, std::vector<Item>, std::greater<Item>> minHeap;
for (const auto& entry : counts) {
minHeap.push({entry.second, entry.first});
if (minHeap.size() > k) {
minHeap.pop(); // Remove the element with the smallest frequency
}
}
// 3. Extract O(K)
std::vector<int> result;
result.reserve(k);
while (!minHeap.empty()) {
result.push_back(minHeap.top().second);
minHeap.pop();
}
return result;
}
2. Kth Largest Element (QuickSelect vs. Heap)
Problem: Find the Kth largest element.
Approach A: Min-Heap (O(N log K)) Maintain a Min-Heap of size K. The root is the Kth largest.
int findKthLargestHeap(const std::vector<int>& nums, int k) {
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
for (int n : nums) {
minHeap.push(n);
if (minHeap.size() > k) minHeap.pop();
}
return minHeap.top();
}
Approach B: QuickSelect (O(N) Average, O(N^2) Worst)
C++11 provides std::nth_element. This is a highly optimized QuickSelect implementation. It partially sorts the range such that the element at nth position is the one that would be there if the range were fully sorted.
#include <vector>
#include <algorithm>
#include <functional> // for std::greater
int findKthLargestQuickSelect(std::vector<int>& nums, int k) {
// We want the Kth largest.
// In a sorted (descending) array, this is at index k-1.
// std::nth_element rearranges elements such that:
// [0, n) contains elements >= nums[n]
// nums[n] is the nth largest
// [n+1, end) contains elements <= nums[n]
auto target = nums.begin() + k - 1;
std::nth_element(nums.begin(), target, nums.end(), std::greater<int>());
return *target;
}
3. Merge K Sorted Lists
Pattern: Min-Heap stores iterators. Complexity: Time O(N log K) where N is total elements, K is number of lists.
C++11 Implementation:
We need a custom comparator struct because we are storing ListNode*.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
struct CompareNode {
// Min-Heap requires 'greater' logic (returns true if a > b)
bool operator()(ListNode* const& p1, ListNode* const& p2) {
return p1->val > p2->val;
}
};
ListNode* mergeKLists(std::vector<ListNode*>& lists) {
std::priority_queue<ListNode*, std::vector<ListNode*>, CompareNode> minHeap;
// Initialize heap
for (ListNode* list : lists) {
if (list) minHeap.push(list);
}
ListNode dummy(0);
ListNode* tail = &dummy;
while (!minHeap.empty()) {
ListNode* smallest = minHeap.top();
minHeap.pop();
tail->next = smallest;
tail = tail->next;
if (smallest->next) {
minHeap.push(smallest->next);
}
}
return dummy.next;
}
4. Find Median from Data Stream
Pattern: Two Heaps (Max-Heap for lower half, Min-Heap for upper half).
Complexity: addNum: O(log N), findMedian: O(1).
Visual:
[Small Elements | MaxHeap] <==> [MinHeap | Large Elements]
#include <queue>
#include <vector>
#include <functional>
class MedianFinder {
std::priority_queue<int> lowerHalf; // Max-Heap
std::priority_queue<int, std::vector<int>, std::greater<int>> upperHalf; // Min-Heap
public:
void addNum(int num) {
// 1. Add to lower half
lowerHalf.push(num);
// 2. Balance: Move largest of lower to upper
upperHalf.push(lowerHalf.top());
lowerHalf.pop();
// 3. Maintain Size Property: Lower half size >= Upper half size
if (lowerHalf.size() < upperHalf.size()) {
lowerHalf.push(upperHalf.top());
upperHalf.pop();
}
}
double findMedian() {
if (lowerHalf.size() > upperHalf.size()) {
return lowerHalf.top();
}
return (lowerHalf.top() + upperHalf.top()) * 0.5;
}
};
5. Advanced: make_heap, push_heap, pop_heap
Sometimes you want to manipulate a vector as a heap without the wrapper of priority_queue (e.g., to access arbitrary elements or sort in-place).
#include <vector>
#include <algorithm>
#include <iostream>
void heapAlgorithms() {
std::vector<int> v = {3, 1, 4, 1, 5, 9};
// 1. O(N) Build Heap
std::make_heap(v.begin(), v.end());
// v is now a Max-Heap structure: {9, 5, 4, 1, 1, 3}
// 2. Push Heap O(log N)
v.push_back(6);
std::push_heap(v.begin(), v.end());
// Moves the last element into heap position
// 3. Pop Heap O(log N)
std::pop_heap(v.begin(), v.end());
// Moves root (max) to the end of vector, then re-heapifies the rest.
int maxVal = v.back();
v.pop_back(); // Remove it physically
// 4. Sort Heap O(N log N)
// Converts a heap into a sorted array (in-place)
std::sort_heap(v.begin(), v.end());
}
Summary of Heap Patterns
| Scenario | C++ Container/Algo | Complexity | Key Insight |
|---|---|---|---|
| Top K Largest | priority_queue<T, vector<T>, greater<T>> |
O(N log K) | Use Min-Heap of size K. |
| Top K Smallest | priority_queue<T> (Default Max) |
O(N log K) | Use Max-Heap of size K. |
| Find Kth Element | std::nth_element |
O(N) | Pivot-based partitioning (QuickSelect). |
| Merge Streams | priority_queue with Custom Comparator |
O(N log K) | Keep one active element per stream. |
| Rolling Median | Max-Heap + Min-Heap | O(log N) | Balance elements between two halves. |