Context:FIT1008_MOC ยท the efficient implementation of a Priority Queue (ADT) ยท backbone clustering its bottom-up ฮ(n) construction ยท a complete binary tree under two invariants
Quick Revision
๐ฏ Objective: complete + heap-ordered binary tree โ O(logn) add/get_max โ the array-backed priority queue.
โก Key Constraint: ops O(logn), peekO(1), build ฮ(n) (not nlogn) โ but only min/max, no arbitrary search.
๐ Core
1. The Heap (Two Invariants)
Complete โ every level full except possibly the last, filled left-to-right โ balanced.
Heap-order โ every node โฅ its children โ max at the root (min-heap is the dual).
Array representation โ 1-indexed, no pointers: parent=โi/2โ, children=2i,2i+1 โ cache-friendly vs a pointer Binary Tree.
2. Operations (Rise / Sink)
add โ append at n+1 then rise (swap up while > parent).
get_max โ return index 1, move last leaf to root, shrink, then sink.
Sink rule โ swap with the larger child (else a bigger sibling stays beneath); both ops are one root-to-leaf path โ O(logn).
3. Bottom-Up Construction (ฮ(n) Heapify)
Mechanism โ sink each internal node right-to-left from โn/2โ to root (leaves are trivial heaps).
Cost โ ฮ(n), not O(nlogn) โ sum of node heights =2hโ1โh=Nโh=ฮ(N).
Boundary โ requires all elements up front (offline); a stream must use add/rise (O(logn) each).
โ๏ธ Core Implementation
๐น rise / sink
the two heap-repair primitives
def _rise(self, k): # add: swim up while > parent while k > 1 and self.a[k] > self.a[k//2]: self.a[k], self.a[k//2] = self.a[k//2], self.a[k]; k //= 2def _sink(self, k): # get_max: swim down, swap LARGER child while 2*k <= self.n: child = 2*k if child < self.n and self.a[child+1] > self.a[child]: child += 1 if self.a[k] >= self.a[child]: break self.a[k], self.a[child] = self.a[child], self.a[k]; k = child
๐ก Common Mistake:Swap with the larger child โ the child += 1 guard (careful at the only-child boundary child < self.n); swapping the smaller violates heap-order.
๐น Bottom-up build_heap
ฮ(n) heapify
def build_heap(an_array): # 1-indexed; cell 0 unused a = [None] + list(an_array); n = len(an_array) for i in range(n // 2, 0, -1): # last internal node -> root; leaves already heaps sink(a, i, n) # each sink: O(height of i) return a
๐ก Common Mistake:Start at โn/2โ, go downward โ guarantees both subtrees of node i are valid heaps when sunk, so one sink fixes i; inserting one-by-one is O(nlogn).
When It Flips: a heap is always balanced + constant-space-per-element but supports only min/max (search(x) is O(n)) โ use a balanced BST when you need search, ordered iteration, or predecessor/successor. Bottom-up build never dominates heapsort: ฮ(n)+ฮ(nlogn)=ฮ(nlogn).
๐ Exam Execution Trace
Applied Exercise
Problem: Prove bottom-up heap construction is ฮ(n), not ฮ(nlogn).
Derivation Proof / Hand-Calculation Walkthrough:
Final Extracted Output: build is ฮ(N) โ most nodes are near-leaves (sink O(1)); the logn height is paid by very few.
๐ง Active Recall
Prove that building a heap from an arbitrary array is O(n), not O(nlogn).
Hint: Weight node heights by their counts.
Answer
Short answer: Total =โhโ2h+1nโO(h)=O(nโhโh/2h), and โhโฅ0โh/2h=2.
Why:Convergent series โ O(2n)=O(n); most nodes are leaves/near-leaves with tiny h, only the rare top nodes pay O(logn).
Why start at index โn/2โ and go downward, and why sink?
Hint: Subtrees must be heaps before fixing their root.
Answer
Short answer:โn/2โ is the last internal node โ everything after is a leaf (trivial heap, skipped).
Why:Bottom-up ordering โ iterating โn/2โโ1 makes both subtrees of i valid heaps when sunk; sink (vs rise) because i may be too small for its descendants.
Both a heap and a balanced BST give O(logn) priority operations โ when must you use the BST?
Hint: Heap-order locates only the extreme.
Answer
Short answer: When you need search of arbitrary keys, ordered iteration, or predecessor/successor.
Why:Partial vs total order โ a heap only enforces parent โฅ child โน search(x) is O(n); a BST maintains full key order โน O(logn) search/insert/delete and min/max.