Heap Exercises: Answer Sheet & Worked Solutions
This is the answer sheet for the heap exercises. Each solution gives the key idea, the optimal complexity, the code in all four languages (use the switcher), and a common pitfall. Try the problems first — then check your work here.
JavaScript note: JS has no built-in heap, so each JS solution includes a small comparator-based
Heapclass (cmp(a, b) < 0meansahas higher priority). It’s the same sift-up/sift-down logic from heap operations. C++ usesstd::priority_queue, Java usesPriorityQueue, and Python usesheapq— see priority queues.
Q1. Kth largest element in an array
Idea: Keep a min-heap of size k. Push every element; whenever the heap exceeds k, pop the smallest. The heap ends holding the k largest values, and its root is the kth largest.
Complexity: O(n log k) time, O(k) space.
#include <queue>
#include <vector>
int findKthLargest(std::vector<int>& nums, int k) {
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
for (int x : nums) {
minHeap.push(x);
if ((int)minHeap.size() > k) minHeap.pop();
}
return minHeap.top();
}
import java.util.PriorityQueue;
int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int x : nums) {
minHeap.add(x);
if (minHeap.size() > k) minHeap.poll();
}
return minHeap.peek();
}
class Heap {
constructor(cmp) { this.a = []; this.c = cmp; }
size() { return this.a.length; }
peek() { return this.a[0]; }
push(x) {
const a = this.a; a.push(x);
for (let i = a.length - 1; i > 0; ) {
const p = (i - 1) >> 1;
if (this.c(a[i], a[p]) >= 0) break;
[a[i], a[p]] = [a[p], a[i]]; i = p;
}
}
pop() {
const a = this.a, top = a[0], last = a.pop();
if (!a.length) return top;
a[0] = last;
for (let i = 0, n = a.length; ; ) {
let s = i; const l = 2*i+1, r = 2*i+2;
if (l < n && this.c(a[l], a[s]) < 0) s = l;
if (r < n && this.c(a[r], a[s]) < 0) s = r;
if (s === i) break;
[a[i], a[s]] = [a[s], a[i]]; i = s;
}
return top;
}
}
function findKthLargest(nums, k) {
const h = new Heap((a, b) => a - b); // min-heap
for (const x of nums) {
h.push(x);
if (h.size() > k) h.pop();
}
return h.peek();
}
import heapq
def find_kth_largest(nums, k):
min_heap = []
for x in nums:
heapq.heappush(min_heap, x)
if len(min_heap) > k:
heapq.heappop(min_heap)
return min_heap[0]
Pitfall: “kth largest” means kth in sorted order, not the kth distinct value — duplicates count. Also, a max-heap of all n elements then k pops is O(n + k log n); the size-k min-heap is cheaper in space and scales to streams.
Q2. Last stone weight
Idea: A max-heap always hands you the two heaviest stones. Smash them, push back the difference if nonzero, and repeat until one or zero stones remain.
Complexity: O(n log n) time, O(n) space.
#include <queue>
#include <vector>
int lastStoneWeight(std::vector<int>& stones) {
std::priority_queue<int> pq(stones.begin(), stones.end()); // max-heap
while (pq.size() > 1) {
int a = pq.top(); pq.pop();
int b = pq.top(); pq.pop();
if (a != b) pq.push(a - b);
}
return pq.empty() ? 0 : pq.top();
}
import java.util.*;
int lastStoneWeight(int[] stones) {
PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());
for (int s : stones) pq.add(s);
while (pq.size() > 1) {
int a = pq.poll(), b = pq.poll();
if (a != b) pq.add(a - b);
}
return pq.isEmpty() ? 0 : pq.peek();
}
class Heap {
constructor(cmp) { this.a = []; this.c = cmp; }
size() { return this.a.length; }
peek() { return this.a[0]; }
push(x) {
const a = this.a; a.push(x);
for (let i = a.length - 1; i > 0; ) {
const p = (i - 1) >> 1;
if (this.c(a[i], a[p]) >= 0) break;
[a[i], a[p]] = [a[p], a[i]]; i = p;
}
}
pop() {
const a = this.a, top = a[0], last = a.pop();
if (!a.length) return top;
a[0] = last;
for (let i = 0, n = a.length; ; ) {
let s = i; const l = 2*i+1, r = 2*i+2;
if (l < n && this.c(a[l], a[s]) < 0) s = l;
if (r < n && this.c(a[r], a[s]) < 0) s = r;
if (s === i) break;
[a[i], a[s]] = [a[s], a[i]]; i = s;
}
return top;
}
}
function lastStoneWeight(stones) {
const h = new Heap((a, b) => b - a); // max-heap
for (const s of stones) h.push(s);
while (h.size() > 1) {
const a = h.pop(), b = h.pop();
if (a !== b) h.push(a - b);
}
return h.size() ? h.peek() : 0;
}
import heapq
def last_stone_weight(stones):
pq = [-s for s in stones]
heapq.heapify(pq) # build max-heap (negated) in O(n)
while len(pq) > 1:
a = -heapq.heappop(pq)
b = -heapq.heappop(pq)
if a != b:
heapq.heappush(pq, -(a - b))
return -pq[0] if pq else 0
Pitfall: Forgetting the empty-heap case returns garbage — return 0 when no stones remain. With Python’s min-only
heapq, you must negate on the way in and out.
Q3. Top K frequent elements
Idea: Count frequencies with a hash map, then keep a min-heap of size k keyed by count. Elements with the highest counts survive; pop whenever the heap exceeds k.
Complexity: O(m log k) time where m is the number of distinct values, O(m) space.
#include <queue>
#include <vector>
#include <unordered_map>
std::vector<int> topKFrequent(std::vector<int>& nums, int k) {
std::unordered_map<int,int> freq;
for (int x : nums) freq[x]++;
using P = std::pair<int,int>; // {count, value}
std::priority_queue<P, std::vector<P>, std::greater<P>> pq; // min by count
for (auto& [val, cnt] : freq) {
pq.push({cnt, val});
if ((int)pq.size() > k) pq.pop();
}
std::vector<int> res;
while (!pq.empty()) { res.push_back(pq.top().second); pq.pop(); }
return res;
}
import java.util.*;
int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> freq = new HashMap<>();
for (int x : nums) freq.merge(x, 1, Integer::sum);
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]); // {count, value}
for (var e : freq.entrySet()) {
pq.add(new int[]{e.getValue(), e.getKey()});
if (pq.size() > k) pq.poll();
}
int[] res = new int[k];
for (int i = 0; i < k; i++) res[i] = pq.poll()[1];
return res;
}
class Heap {
constructor(cmp) { this.a = []; this.c = cmp; }
size() { return this.a.length; }
peek() { return this.a[0]; }
push(x) {
const a = this.a; a.push(x);
for (let i = a.length - 1; i > 0; ) {
const p = (i - 1) >> 1;
if (this.c(a[i], a[p]) >= 0) break;
[a[i], a[p]] = [a[p], a[i]]; i = p;
}
}
pop() {
const a = this.a, top = a[0], last = a.pop();
if (!a.length) return top;
a[0] = last;
for (let i = 0, n = a.length; ; ) {
let s = i; const l = 2*i+1, r = 2*i+2;
if (l < n && this.c(a[l], a[s]) < 0) s = l;
if (r < n && this.c(a[r], a[s]) < 0) s = r;
if (s === i) break;
[a[i], a[s]] = [a[s], a[i]]; i = s;
}
return top;
}
}
function topKFrequent(nums, k) {
const freq = new Map();
for (const x of nums) freq.set(x, (freq.get(x) || 0) + 1);
const h = new Heap((a, b) => a[0] - b[0]); // min by count, items [count, value]
for (const [val, cnt] of freq) {
h.push([cnt, val]);
if (h.size() > k) h.pop();
}
const res = [];
while (h.size()) res.push(h.pop()[1]);
return res;
}
import heapq
from collections import Counter
def top_k_frequent(nums, k):
freq = Counter(nums)
heap = [] # (count, value) min-heap of size k
for val, cnt in freq.items():
heapq.heappush(heap, (cnt, val))
if len(heap) > k:
heapq.heappop(heap)
return [val for cnt, val in heap]
Pitfall: Sorting all distinct values is O(m log m); the size-k heap is O(m log k). If k is close to m, bucket sort by count is an even cleaner O(n). Python’s
heapq.nlargest(k, freq, key=freq.get)is the idiomatic one-liner.
Q4. K closest points to origin
Idea: Keep a max-heap of size k keyed by squared distance. When the heap exceeds k, pop the farthest. The k closest survive. Compare squared distances — no sqrt needed.
Complexity: O(n log k) time, O(k) space.
#include <queue>
#include <vector>
std::vector<std::vector<int>> kClosest(std::vector<std::vector<int>>& points, int k) {
std::priority_queue<std::pair<int,int>> pq; // max-heap of {dist2, index}
for (int i = 0; i < (int)points.size(); i++) {
int d = points[i][0]*points[i][0] + points[i][1]*points[i][1];
pq.push({d, i});
if ((int)pq.size() > k) pq.pop();
}
std::vector<std::vector<int>> res;
while (!pq.empty()) { res.push_back(points[pq.top().second]); pq.pop(); }
return res;
}
import java.util.*;
int[][] kClosest(int[][] points, int k) {
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[0] - a[0]); // max by dist2
for (int i = 0; i < points.length; i++) {
int d = points[i][0]*points[i][0] + points[i][1]*points[i][1];
pq.add(new int[]{d, i});
if (pq.size() > k) pq.poll();
}
int[][] res = new int[k][];
for (int i = 0; i < k; i++) res[i] = points[pq.poll()[1]];
return res;
}
class Heap {
constructor(cmp) { this.a = []; this.c = cmp; }
size() { return this.a.length; }
peek() { return this.a[0]; }
push(x) {
const a = this.a; a.push(x);
for (let i = a.length - 1; i > 0; ) {
const p = (i - 1) >> 1;
if (this.c(a[i], a[p]) >= 0) break;
[a[i], a[p]] = [a[p], a[i]]; i = p;
}
}
pop() {
const a = this.a, top = a[0], last = a.pop();
if (!a.length) return top;
a[0] = last;
for (let i = 0, n = a.length; ; ) {
let s = i; const l = 2*i+1, r = 2*i+2;
if (l < n && this.c(a[l], a[s]) < 0) s = l;
if (r < n && this.c(a[r], a[s]) < 0) s = r;
if (s === i) break;
[a[i], a[s]] = [a[s], a[i]]; i = s;
}
return top;
}
}
function kClosest(points, k) {
const h = new Heap((a, b) => b[0] - a[0]); // max-heap by dist2, items [dist2, point]
for (const [x, y] of points) {
h.push([x*x + y*y, [x, y]]);
if (h.size() > k) h.pop();
}
const res = [];
while (h.size()) res.push(h.pop()[1]);
return res;
}
import heapq
def k_closest(points, k):
heap = [] # max-heap by squared distance via negation
for x, y in points:
d = x * x + y * y
heapq.heappush(heap, (-d, [x, y]))
if len(heap) > k:
heapq.heappop(heap)
return [p for _, p in heap]
Pitfall: Taking the square root is wasteful and risks float rounding — ordering by
x*x + y*ygives identical results. Use a max-heap (not min) so the size-k version can evict the farthest point.
Q5. Merge k sorted lists
Idea: Seed a min-heap with the first element of each list. Repeatedly pop the smallest, append it, and push the next element from the same list. The heap never holds more than k entries.
Complexity: O(N log k) time (N = total elements), O(k) space for the heap.
#include <queue>
#include <vector>
#include <tuple>
std::vector<int> mergeKSorted(std::vector<std::vector<int>>& lists) {
using T = std::tuple<int,int,int>; // {value, listIdx, elemIdx}
std::priority_queue<T, std::vector<T>, std::greater<T>> pq;
for (int i = 0; i < (int)lists.size(); i++)
if (!lists[i].empty()) pq.push({lists[i][0], i, 0});
std::vector<int> res;
while (!pq.empty()) {
auto [val, li, ei] = pq.top(); pq.pop();
res.push_back(val);
if (ei + 1 < (int)lists[li].size())
pq.push({lists[li][ei + 1], li, ei + 1});
}
return res;
}
import java.util.*;
int[] mergeKSorted(int[][] lists) {
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]); // [value, li, ei]
int total = 0;
for (int i = 0; i < lists.length; i++) {
total += lists[i].length;
if (lists[i].length > 0) pq.add(new int[]{lists[i][0], i, 0});
}
int[] res = new int[total];
int w = 0;
while (!pq.isEmpty()) {
int[] t = pq.poll();
res[w++] = t[0];
int li = t[1], ei = t[2];
if (ei + 1 < lists[li].length) pq.add(new int[]{lists[li][ei + 1], li, ei + 1});
}
return res;
}
class Heap {
constructor(cmp) { this.a = []; this.c = cmp; }
size() { return this.a.length; }
peek() { return this.a[0]; }
push(x) {
const a = this.a; a.push(x);
for (let i = a.length - 1; i > 0; ) {
const p = (i - 1) >> 1;
if (this.c(a[i], a[p]) >= 0) break;
[a[i], a[p]] = [a[p], a[i]]; i = p;
}
}
pop() {
const a = this.a, top = a[0], last = a.pop();
if (!a.length) return top;
a[0] = last;
for (let i = 0, n = a.length; ; ) {
let s = i; const l = 2*i+1, r = 2*i+2;
if (l < n && this.c(a[l], a[s]) < 0) s = l;
if (r < n && this.c(a[r], a[s]) < 0) s = r;
if (s === i) break;
[a[i], a[s]] = [a[s], a[i]]; i = s;
}
return top;
}
}
function mergeKSorted(lists) {
const h = new Heap((a, b) => a[0] - b[0]); // [value, listIdx, elemIdx]
lists.forEach((list, i) => { if (list.length) h.push([list[0], i, 0]); });
const res = [];
while (h.size()) {
const [val, li, ei] = h.pop();
res.push(val);
if (ei + 1 < lists[li].length) h.push([lists[li][ei + 1], li, ei + 1]);
}
return res;
}
import heapq
def merge_k_sorted(lists):
heap = [] # (value, list_idx, elem_idx)
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst[0], i, 0))
res = []
while heap:
val, li, ei = heapq.heappop(heap)
res.append(val)
if ei + 1 < len(lists[li]):
heapq.heappush(heap, (lists[li][ei + 1], li, ei + 1))
return res
Pitfall: Pushing entire lists into the heap makes it O(N log N) and defeats the purpose — push only the current front of each list. Skip empty lists when seeding, and include the list/element indices so you know where to pull the next value.
Q6. Find median from a data stream
Idea: Keep two heaps split at the median: a max-heap lo for the lower half and a min-heap hi for the upper half. Keep them balanced so lo has the same size as hi or one more. The median is lo’s top (odd count) or the average of both tops (even count).
Complexity: O(log n) per addNum, O(1) per findMedian, O(n) space.
#include <queue>
#include <vector>
class MedianFinder {
std::priority_queue<int> lo; // max-heap: lower half
std::priority_queue<int, std::vector<int>, std::greater<int>> hi; // min-heap: upper half
public:
void addNum(int x) {
lo.push(x);
hi.push(lo.top()); lo.pop(); // shuttle the new top across the split
if (hi.size() > lo.size()) { lo.push(hi.top()); hi.pop(); }
}
double findMedian() {
if (lo.size() > hi.size()) return lo.top();
return (lo.top() + hi.top()) / 2.0;
}
};
import java.util.*;
class MedianFinder {
private final PriorityQueue<Integer> lo = new PriorityQueue<>(Collections.reverseOrder());
private final PriorityQueue<Integer> hi = new PriorityQueue<>();
void addNum(int x) {
lo.add(x);
hi.add(lo.poll()); // shuttle the new top across the split
if (hi.size() > lo.size()) lo.add(hi.poll());
}
double findMedian() {
if (lo.size() > hi.size()) return lo.peek();
return (lo.peek() + hi.peek()) / 2.0;
}
}
class Heap {
constructor(cmp) { this.a = []; this.c = cmp; }
size() { return this.a.length; }
peek() { return this.a[0]; }
push(x) {
const a = this.a; a.push(x);
for (let i = a.length - 1; i > 0; ) {
const p = (i - 1) >> 1;
if (this.c(a[i], a[p]) >= 0) break;
[a[i], a[p]] = [a[p], a[i]]; i = p;
}
}
pop() {
const a = this.a, top = a[0], last = a.pop();
if (!a.length) return top;
a[0] = last;
for (let i = 0, n = a.length; ; ) {
let s = i; const l = 2*i+1, r = 2*i+2;
if (l < n && this.c(a[l], a[s]) < 0) s = l;
if (r < n && this.c(a[r], a[s]) < 0) s = r;
if (s === i) break;
[a[i], a[s]] = [a[s], a[i]]; i = s;
}
return top;
}
}
class MedianFinder {
constructor() {
this.lo = new Heap((a, b) => b - a); // max-heap: lower half
this.hi = new Heap((a, b) => a - b); // min-heap: upper half
}
addNum(x) {
this.lo.push(x);
this.hi.push(this.lo.pop()); // shuttle the new top across the split
if (this.hi.size() > this.lo.size()) this.lo.push(this.hi.pop());
}
findMedian() {
if (this.lo.size() > this.hi.size()) return this.lo.peek();
return (this.lo.peek() + this.hi.peek()) / 2;
}
}
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap (negated): lower half
self.hi = [] # min-heap: upper half
def add_num(self, x):
heapq.heappush(self.lo, -x)
heapq.heappush(self.hi, -heapq.heappop(self.lo)) # shuttle across the split
if len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def find_median(self):
if len(self.lo) > len(self.hi):
return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2
Pitfall: Push-to-
lothen movelo’s top tohiguarantees the two halves stay correctly ordered relative to each other; skipping that step lets a small number sit inhi. For an even count, average both tops as a float — integer division truncates the median.
That’s the full answer sheet. Back to the heap exercises, revisit priority queues, or continue to tries.