|
| 1 | +# Interview Journal: #3 - Max Heap and Min Heap in Golang |
| 2 | + |
| 3 | +In this entry of the Interview Journal, we're diving into **Heaps**. Specifically, how to implement Max Heaps and Min Heaps in Go (Golang). This is a classic interview topic and a fundamental data structure for priority queues, graph algorithms (like Dijkstra's), and efficient sorting. |
| 4 | + |
| 5 | +## What is a Heap? |
| 6 | + |
| 7 | +A **Heap** is a specialized tree-based data structure which is essentially an almost complete tree that satisfies the **heap property**: |
| 8 | + |
| 9 | +* **Max Heap:** For any given node `I`, the value of `I` is greater than or equal to the values of its children. The largest element is at the root. |
| 10 | +* **Min Heap:** For any given node `I`, the value of `I` is less than or equal to the values of its children. The smallest element is at the root. |
| 11 | + |
| 12 | +Heaps are usually implemented using arrays (or slices in Go) because they are complete binary trees. |
| 13 | + |
| 14 | +* **Parent Index:** `(i - 1) / 2` |
| 15 | +* **Left Child Index:** `2*i + 1` |
| 16 | +* **Right Child Index:** `2*i + 2` |
| 17 | + |
| 18 | +### Visualizing a Max Heap |
| 19 | + |
| 20 | +```mermaid |
| 21 | +graph TD |
| 22 | + root((100)) |
| 23 | + child1((19)) |
| 24 | + child2((36)) |
| 25 | + child1_1((17)) |
| 26 | + child1_2((3)) |
| 27 | + child2_1((25)) |
| 28 | + child2_2((1)) |
| 29 | + |
| 30 | + root --- child1 |
| 31 | + root --- child2 |
| 32 | + child1 --- child1_1 |
| 33 | + child1 --- child1_2 |
| 34 | + child2 --- child2_1 |
| 35 | + child2 --- child2_2 |
| 36 | + |
| 37 | + classDef node fill:#240224,stroke:#333,stroke-width:2px; |
| 38 | + class root,child1,child2,child1_1,child1_2,child2_1,child2_2 node; |
| 39 | +``` |
| 40 | + |
| 41 | +**Array Representation:** `[100, 19, 36, 17, 3, 25, 1]` |
| 42 | + |
| 43 | +## Why do we need Heaps? |
| 44 | + |
| 45 | +Heaps solve a specific problem efficiently: **repeatedly accessing the minimum or maximum element** in a dynamic set of data. |
| 46 | + |
| 47 | +| Data Structure | Find Max | Insert | Remove Max | |
| 48 | +| :--- | :--- | :--- | :--- | |
| 49 | +| **Unsorted Array** | O(N) | O(1) | O(N) | |
| 50 | +| **Sorted Array** | O(1) | O(N) | O(1) | |
| 51 | +| **Heap** | **O(1)** | **O(log N)** | **O(log N)** | |
| 52 | + |
| 53 | +**Real-world Use Cases:** |
| 54 | +1. **Priority Queues:** Scheduling jobs where "High Priority" tasks run before "Oldest" tasks (e.g., OS process scheduling, bandwidth management). |
| 55 | +2. **Graph Algorithms:** Essential for **Dijkstra’s algorithm** (shortest path) and **Prim’s algorithm** (minimum spanning tree). |
| 56 | +3. **Heapsort:** An efficient, in-place sorting algorithm with O(N log N) complexity. |
| 57 | + |
| 58 | +## Go's `container/heap` |
| 59 | + |
| 60 | +Go provides a standard library package `container/heap` that defines a heap interface. To use it, your type just needs to implement the `heap.Interface`. |
| 61 | + |
| 62 | +```go |
| 63 | +type Interface interface { |
| 64 | + sort.Interface // Len, Less, Swap |
| 65 | + Push(x any) // add x as element Len() |
| 66 | + Pop() any // remove and return element Len() - 1. |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +### Implementing a Min Heap |
| 71 | + |
| 72 | +Let's implement a simple `MinHeap` for integers. |
| 73 | + |
| 74 | +```go |
| 75 | +package main |
| 76 | + |
| 77 | +import ( |
| 78 | + "container/heap" |
| 79 | + "fmt" |
| 80 | +) |
| 81 | + |
| 82 | +// IntHeap is a min-heap of ints. |
| 83 | +type IntHeap []int |
| 84 | + |
| 85 | +func (h IntHeap) Len() int { return len(h) } |
| 86 | +func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } // < for MinHeap |
| 87 | +func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } |
| 88 | + |
| 89 | +func (h *IntHeap) Push(x any) { |
| 90 | + *h = append(*h, x.(int)) |
| 91 | +} |
| 92 | + |
| 93 | +func (h *IntHeap) Pop() any { |
| 94 | + old := *h |
| 95 | + n := len(old) |
| 96 | + x := old[n-1] |
| 97 | + *h = old[0 : n-1] |
| 98 | + return x |
| 99 | +} |
| 100 | + |
| 101 | +func main() { |
| 102 | + h := &IntHeap{2, 1, 5} |
| 103 | + heap.Init(h) |
| 104 | + heap.Push(h, 3) |
| 105 | + fmt.Printf("minimum: %d |
| 106 | +", (*h)[0]) // 1 |
| 107 | + |
| 108 | + for h.Len() > 0 { |
| 109 | + fmt.Printf("%d ", heap.Pop(h)) |
| 110 | + } |
| 111 | + // Output: 1 2 3 5 |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +### Implementing a Max Heap |
| 116 | + |
| 117 | +To turn the above into a `MaxHeap`, we only need to change the `Less` function. |
| 118 | + |
| 119 | +```go |
| 120 | +// For MaxHeap, we want the larger element to come "first" (be the root) |
| 121 | +func (h IntHeap) Less(i, j int) bool { return h[i] > h[j] } // > for MaxHeap |
| 122 | +``` |
| 123 | + |
| 124 | +Alternatively, if you are just dealing with numbers, you can store negative values in a Min Heap to simulate a Max Heap, but implementing the interface is cleaner. |
| 125 | + |
| 126 | +## From Scratch (For Interviews) |
| 127 | + |
| 128 | +Sometimes interviewers ask you to implement `push` and `pop` logic without using the library. This tests your understanding of **Bubbling Up (Heapify Up)** and **Bubbling Down (Heapify Down)**. |
| 129 | + |
| 130 | +### Heapify Up (Push) |
| 131 | + |
| 132 | +When we add a new element, we append it to the end of the array. Then we check if it violates the heap property with its parent. If it does, we swap them. We repeat this until the property is restored or we reach the root. |
| 133 | + |
| 134 | +```go |
| 135 | +func (h *MaxHeap) Push(val int) { |
| 136 | + h.slice = append(h.slice, val) |
| 137 | + h.heapifyUp(len(h.slice) - 1) |
| 138 | +} |
| 139 | + |
| 140 | +func (h *MaxHeap) heapifyUp(index int) { |
| 141 | + for h.slice[parent(index)] < h.slice[index] { |
| 142 | + h.swap(parent(index), index) |
| 143 | + index = parent(index) |
| 144 | + } |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +### Heapify Down (Pop) |
| 149 | + |
| 150 | +When we remove the root (max/min), we take the *last* element in the array and put it at the root. Then we compare it with its children. If it violates the heap property, we swap it with the larger (or smaller for min-heap) of the two children. Repeat until the property is restored or we reach a leaf. |
| 151 | + |
| 152 | +```go |
| 153 | +func (h *MaxHeap) Pop() int { |
| 154 | + max := h.slice[0] |
| 155 | + last := len(h.slice) - 1 |
| 156 | + h.slice[0] = h.slice[last] |
| 157 | + h.slice = h.slice[:last] |
| 158 | + h.heapifyDown(0) |
| 159 | + return max |
| 160 | +} |
| 161 | + |
| 162 | +func (h *MaxHeap) heapifyDown(index int) { |
| 163 | + lastIndex := len(h.slice) - 1 |
| 164 | + l, r := left(index), right(index) |
| 165 | + childToCompare := 0 |
| 166 | + |
| 167 | + for l <= lastIndex { |
| 168 | + if l == lastIndex { // only left child |
| 169 | + childToCompare = l |
| 170 | + } else if h.slice[l] > h.slice[r] { // left is larger |
| 171 | + childToCompare = l |
| 172 | + } else { // right is larger |
| 173 | + childToCompare = r |
| 174 | + } |
| 175 | + |
| 176 | + if h.slice[index] < h.slice[childToCompare] { |
| 177 | + h.swap(index, childToCompare) |
| 178 | + index = childToCompare |
| 179 | + l, r = left(index), right(index) |
| 180 | + } else { |
| 181 | + return |
| 182 | + } |
| 183 | + } |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +## Time Complexity |
| 188 | + |
| 189 | +| Operation | Time Complexity | |
| 190 | +| :--- | :--- | |
| 191 | +| **Push** | O(log N) | |
| 192 | +| **Pop** | O(log N) | |
| 193 | +| **Peek (Top)** | O(1) | |
| 194 | +| **Build Heap** | O(N) | |
| 195 | + |
| 196 | +## Summary |
| 197 | + |
| 198 | +* Use `container/heap` for production code. |
| 199 | +* Remember `Less(i, j)` determines the order. `h[i] < h[j]` is Min Heap. `h[i] > h[j]` is Max Heap. |
| 200 | +* Understand the array indices math: `2*i+1`, `2*i+2`, `(i-1)/2`. |
| 201 | +* "Bubble Up" for insertion, "Bubble Down" for deletion. |
0 commit comments