Heaps & Heapsort

Heap ও Heapsort

Read: ~40 min Advanced 7 practice problems Live code runner

1. The Heap Property

A binary heap is a complete binary tree where every parent is ≥ both children (max-heap) or ≤ both children (min-heap). It is stored as a 0-indexed array with parent/child arithmetic — no pointers needed.

IndexFormula
parent(i)(i − 1) / 2
left(i)2·i + 1
right(i)2·i + 2
Heap হলো একটি complete binary tree, যেখানে প্রতিটি parent তার দুই child-এর চেয়ে বড় (max-heap) বা ছোট (min-heap)। এটিকে আমরা একটি 0-indexed array-এ store করি — pointer-এর প্রয়োজন নেই, indexing-এর গণিতেই সব হয়ে যায়।
90 75 80 30 60 70 50 Array: 90 75 80 30 60 70 50 Figure 15.1 — A max-heap as a tree (top) and as the array [90, 75, 80, 30, 60, 70, 50].

2. The Two Workhorses: Sift-Up & Sift-Down

push(x): append x at the end, then sift up while x > parent. pop(): replace root with last element, shrink, then sift down while it is smaller than the larger child. Both O(log n).

max_heap.cpp
#include <bits/stdc++.h>
using namespace std;

struct MaxHeap {
    vector<int> h;

    void siftUp(int i) {
        while (i > 0) {
            int p = (i - 1) / 2;
            if (h[p] >= h[i]) break;
            swap(h[p], h[i]); i = p;
        }
    }
    void siftDown(int i) {
        int n = h.size();
        while (2*i + 1 < n) {
            int c = 2*i + 1;
            if (c+1 < n && h[c+1] > h[c]) c++;
            if (h[i] >= h[c]) break;
            swap(h[i], h[c]); i = c;
        }
    }
    void push(int x) { h.push_back(x); siftUp(h.size() - 1); }
    int  top()        { return h[0]; }
    void pop() {
        h[0] = h.back(); h.pop_back();
        if (!h.empty()) siftDown(0);
    }
};

int main() {
    MaxHeap mh;
    for (int x : {3, 10, 1, 7, 15, 9}) mh.push(x);
    while (!mh.h.empty()) { cout << mh.top() << " "; mh.pop(); }
}

Output: 15 10 9 7 3 1 — descending. That is exactly heapsort.

3. Build-Heap Is O(n) — The Beautiful Proof

Naïve thinking: n inserts × O(log n) = O(n log n). But if we start from the array and sift down from i = n/2 − 1 down to 0, the total work is:

Σ (number of nodes at height h) × O(h) = Σ (n / 2^(h+1)) × h = O(n)

The geometric series converges: most nodes are leaves with height 0; only a few are deep.

Build-heap proof sketch Σ_{h=0}^{log n} (n / 2^(h+1)) · h ≤ n · Σ h / 2^(h+1) = n · O(1) = O(n).
অধিকাংশ নোডই leaf — তাদের জন্য sift-down O(0)। তাই মোট কাজ O(n)।
heapsort.cpp
#include <bits/stdc++.h>
using namespace std;

void siftDown(vector<int>& a, int i, int n) {
    while (2*i + 1 < n) {
        int c = 2*i + 1;
        if (c+1 < n && a[c+1] > a[c]) c++;
        if (a[i] >= a[c]) return;
        swap(a[i], a[c]); i = c;
    }
}

void heapsort(vector<int>& a) {
    int n = a.size();
    // O(n) build
    for (int i = n/2 - 1; i >= 0; i--) siftDown(a, i, n);
    // O(n log n) extract
    for (int i = n - 1; i > 0; i--) {
        swap(a[0], a[i]);
        siftDown(a, 0, i);
    }
}

int main() {
    vector<int> a = {5, 2, 8, 1, 9, 3, 7};
    heapsort(a);
    for (int x : a) cout << x << " ";
}

4. Practical Pattern: K-th Largest with a Min-Heap of Size k

Maintain a min-heap of the k largest values seen so far. Each new value: push, and if size exceeds k, pop the minimum. After all values, the root is the k-th largest. O(n log k).

top-k pattern: size-k min-heap চালান। প্রতিবার push, তারপর size > k হলে root pop। শেষে root-ই k-th largest। মেমরি O(k) — পুরো array sort করার চেয়ে অনেক ভালো যখন k ছোট।

5. Practice Problems

  1. Find the k-th smallest element of an array using a max-heap of size k.
    size-k max-heap দিয়ে k-th smallest বের করুন।
    ✨ Show Answer (উত্তর দেখুন)
    a1.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        vector<int> a = {7,10,4,3,20,15}; int k = 3;
        priority_queue<int> pq;
        for (int x : a) {
            pq.push(x);
            if ((int)pq.size() > k) pq.pop();
        }
        cout << pq.top();
    }
  2. Connect ropes to minimise total cost (always join the two shortest).
    Rope-গুলো এমনভাবে join করুন যাতে মোট cost সর্বনিম্ন হয়।
    ✨ Show Answer (উত্তর দেখুন)
    a2.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        vector<int> r = {4,3,2,6};
        priority_queue<int, vector<int>, greater<>> pq(r.begin(), r.end());
        long long cost = 0;
        while (pq.size() > 1) {
            int a = pq.top(); pq.pop();
            int b = pq.top(); pq.pop();
            cost += a + b;
            pq.push(a + b);
        }
        cout << cost;
    }
  3. Running median of a stream of integers using two heaps (max-heap + min-heap).
    দুটি heap ব্যবহার করে stream-এর running median বের করুন।
    ✨ Show Answer (উত্তর দেখুন)
    a3.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        priority_queue<int> lo;                                // max-heap (lower half)
        priority_queue<int, vector<int>, greater<>> hi;     // min-heap (upper half)
        for (int x : {1,3,5,2,4,6,7}) {
            lo.push(x);
            hi.push(lo.top()); lo.pop();
            if (hi.size() > lo.size()) { lo.push(hi.top()); hi.pop(); }
            double med = lo.size() == hi.size() ? (lo.top() + hi.top()) / 2.0 : lo.top();
            cout << med << " ";
        }
    }
  4. Sort a nearly-sorted array (each element at most k positions away from its sorted slot) in O(n log k).
    প্রায়-sorted array (k দূরত্বের মধ্যে সব) — O(n log k)-এ sort করুন।
    ✨ Show Answer (উত্তর দেখুন)

    Approach: push first k+1 elements into a min-heap. For each remaining element, pop one (the smallest of the window) into the output, push the new one. After all input is exhausted, drain the heap.

  5. Top-k frequent words in a list (lexicographic tie-break).
    Top-k frequent word — tie-break dictionary order-এ।
    ✨ Show Answer (উত্তর দেখুন)
    a5.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        vector<string> w = {"i","love","leetcode","i","love","coding"};
        int k = 2;
        map<string,int> cnt;
        for (auto& s : w) cnt[s]++;
        auto cmp = [](auto& a, auto& b) {
            return a.second != b.second ? a.second < b.second : a.first > b.first;
        };
        priority_queue<pair<string,int>, vector<pair<string,int>>, decltype(cmp)> pq(cmp);
        for (auto& p : cnt) pq.push(p);
        while (k--) { cout << pq.top().first << " "; pq.pop(); }
    }
  6. Use std::make_heap / push_heap / pop_heap on a vector to confirm the STL heap behaves the same.
    STL heap functions দিয়ে নিজেদের impl-এর সাথে আউটপুট মিলিয়ে দেখুন।
    ✨ Show Answer (উত্তর দেখুন)
    a6.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        vector<int> v = {3,10,1,7,15,9};
        make_heap(v.begin(), v.end());
        while (!v.empty()) {
            cout << v.front() << " ";
            pop_heap(v.begin(), v.end());
            v.pop_back();
        }
    }
  7. Why is heapsort O(n log n) worst-case but quicksort isn't? When would you choose heapsort over quicksort?
    Heapsort worst-case-এও O(n log n) কেন? কখন quicksort-এর বদলে heapsort বেছে নেবেন?
    ✨ Show Answer (উত্তর দেখুন)

    Answer: heapsort always extracts log-n levels exactly n times — the worst case is the average case. Quicksort's worst-case is O(n²) on adversarial inputs (sorted with bad pivot). Choose heapsort when worst-case guarantees matter (real-time systems, kernel) or memory must be O(1) (heapsort is in-place; quicksort uses O(log n) stack).

Summary — Module 15

A binary heap is a complete tree stored as an array with parent/child arithmetic. push and pop are O(log n); build-heap is O(n). Heapsort uses build-heap + repeated extract-max in O(n log n) worst-case, in-place. The same structure is the engine of std::priority_queue — used by Dijkstra, Huffman, and many top-k problems.

Heap = array-backed complete tree। push/pop O(log n), build O(n)। Heapsort worst-case-এও O(n log n) — quicksort-এর বদলে কখনো এটি বেছে নিতে হয়।

Next Module → Trees: Terminology, Traversals, Representation — Phase 4 শুরু।