diff --git a/explanations/Min_Binary_Heap.md b/explanations/Min_Binary_Heap.md new file mode 100644 index 0000000..c16117b --- /dev/null +++ b/explanations/Min_Binary_Heap.md @@ -0,0 +1,87 @@ +# Explanations by AI using my code as example + +## Min Binary Heap + +> Source: `trees/heap/minHeap.go` + +A binary heap is a complete binary tree flattened into a single slice +(`MinHeap.Array`). No node pointers — the tree shape lives in the index math. + +### The invariant + +Every parent is **≤** both its children. The smallest value therefore always sits +at the root (`Array[0]`). This is a *partial* order: siblings are unordered, so the +array is "loosely sorted", which is cheaper to maintain than a fully sorted array. + +### Index math (implicit tree) + +For a node at index `i`: + +| Relation | Index | +| ------------ | ----------- | +| left child | `2*i + 1` | +| right child | `2*i + 2` | +| parent | `(i-1) / 2` | + +### The two repair operations + +Both walk one root-to-leaf path, so both are **O(log n)**. + +- **`siftUp(i)`** — used by `Insert`. A value that may be *too small* for its position + bubbles **up**, swapping with its parent while it's smaller than the parent. +- **`siftDown(i)`** — used by `PopMin` and `Heapify`. A value that may be *too large* + sinks **down**, repeatedly swapping with its **smallest** child until both children + are ≥ it (or it hits a leaf). + +### Methods + +| Method | What it does | Cost | +| ------------ | -------------------------------------------------------------- | ------------ | +| `NewMinHeap` | returns an empty heap | O(1) | +| `Insert` | appends to the end, then `siftUp` to restore the invariant | O(log n) | +| `PopMin` | returns the root; see the swap-and-sink dance below | O(log n) | +| `Heapify` | bulk-builds a heap from an arbitrary slice (empty heap only) | O(n) | + +### PopMin, step by step + +You can't just delete `Array[0]` — that leaves a hole. Instead: + +1. Save the root (`Array[0]`) as the return value. +2. Move the **last** element into the root slot. +3. Zero the old last slot and shrink the slice by one (order matters — clear the slot + *before* reslicing, or the index is out of range). +4. `siftDown(0)` to push that moved-up value back to its rightful depth. + +Returns `(zero, false)` when the heap is empty. + +> The zeroing (`Array[last] = zero`) only matters when `T` holds a pointer +> (e.g. `string`, which `cmp.Ordered` allows) — it lets the GC reclaim the dropped +> element instead of keeping it alive in the backing array. + +### Heapify: why it's O(n), not O(n log n) + +Inserting `n` items one by one would be O(n log n). `Heapify` is faster: it copies the +slice, then calls `siftDown` on every **non-leaf** node, from the last parent up to the +root: + +```go +for i := len(h.Array)/2 - 1; i >= 0; i-- { + h.siftDown(i) +} +``` + +Starting at `len/2 - 1` skips the leaves (they're already trivially valid heaps of +size 1). Working bottom-up means each `siftDown` sinks into subtrees that are *already* +valid heaps. Most nodes are near the bottom and barely move, so the total work sums to +O(n), not O(n log n). + +It refuses to run on a non-empty heap (returns an error) to avoid clobbering existing +data. + +### Summary + +- **Array + index math** = a tree with no pointers and cache-friendly memory. +- **Invariant (parent ≤ children)** = the min is always `Array[0]`, peek is O(1). +- **`siftUp` / `siftDown`** = the O(log n) repairs that keep the invariant after an + insert or a pop. +- **`Heapify`** = build the whole heap in O(n) by sinking non-leaves bottom-up. diff --git a/tests/treeTests/priorityQueue_test.go b/tests/treeTests/priorityQueue_test.go new file mode 100644 index 0000000..65ff7a8 --- /dev/null +++ b/tests/treeTests/priorityQueue_test.go @@ -0,0 +1,136 @@ +package tests + +import ( + "math/rand" + "slices" + "testing" + + "datastructures/trees/heap" +) + +func TestPQPopEmpty(t *testing.T) { + q := heap.NewPriorityQueue[int]() + if v, ok := q.Pop(); ok { + t.Fatalf("Pop on empty queue = (%d, true), want (0, false)", v) + } +} + +func TestPQPeekEmpty(t *testing.T) { + q := heap.NewPriorityQueue[int]() + if v, ok := q.Peek(); ok { + t.Fatalf("Peek on empty queue = (%d, true), want (0, false)", v) + } +} + +func TestPQSizeTracksPushPop(t *testing.T) { + q := heap.NewPriorityQueue[int]() + if q.Size() != 0 { + t.Fatalf("new queue Size = %d, want 0", q.Size()) + } + for i, v := range []int{9, 4, 7, 1} { + q.Push(v) + if q.Size() != i+1 { + t.Fatalf("after %d pushes Size = %d, want %d", i+1, q.Size(), i+1) + } + } + for want := 3; want >= 0; want-- { + q.Pop() + if q.Size() != want { + t.Fatalf("Size = %d, want %d", q.Size(), want) + } + } +} + +func TestPQPeekReturnsMinWithoutRemoving(t *testing.T) { + q := heap.NewPriorityQueue[int]() + for _, v := range []int{8, 3, 5, 1, 9} { + q.Push(v) + } + v, ok := q.Peek() + if !ok || v != 1 { + t.Fatalf("Peek = (%d, %v), want (1, true)", v, ok) + } + // Peek must not mutate the queue. + if q.Size() != 5 { + t.Fatalf("Size after Peek = %d, want 5", q.Size()) + } + v2, _ := q.Peek() + if v2 != v { + t.Fatalf("second Peek = %d, want same %d", v2, v) + } +} + +func TestPQPopsInPriorityOrder(t *testing.T) { + in := []int{5, 3, 8, 1, 9, 2, 7, 0, 4, 6} + q := heap.NewPriorityQueue[int]() + for _, v := range in { + q.Push(v) + } + + got := []int{} + for q.Size() > 0 { + // Peek must always agree with the next Pop. + p, ok := q.Peek() + if !ok { + t.Fatal("Peek returned ok=false while Size > 0") + } + v, ok := q.Pop() + if !ok { + t.Fatal("Pop returned ok=false while Size > 0") + } + if p != v { + t.Fatalf("Peek returned %d but Pop returned %d", p, v) + } + got = append(got, v) + } + + want := slices.Clone(in) + slices.Sort(want) + if !slices.Equal(got, want) { + t.Fatalf("pop order = %v, want ascending %v", got, want) + } +} + +func TestPQStrings(t *testing.T) { + q := heap.NewPriorityQueue[string]() + for _, s := range []string{"pear", "apple", "cherry", "banana"} { + q.Push(s) + } + got := []string{} + for q.Size() > 0 { + v, _ := q.Pop() + got = append(got, v) + } + want := []string{"apple", "banana", "cherry", "pear"} + if !slices.Equal(got, want) { + t.Fatalf("pop order = %v, want %v", got, want) + } +} + +func TestPQFuzzAgainstSort(t *testing.T) { + rng := rand.New(rand.NewSource(7)) + for trial := 0; trial < 200; trial++ { + n := rng.Intn(50) + in := make([]int, n) + for i := range in { + in[i] = rng.Intn(100) + } + + q := heap.NewPriorityQueue[int]() + for _, v := range in { + q.Push(v) + } + + got := []int{} + for q.Size() > 0 { + v, _ := q.Pop() + got = append(got, v) + } + + want := slices.Clone(in) + slices.Sort(want) + if !slices.Equal(got, want) { + t.Fatalf("trial %d: got %v, want %v (input %v)", trial, got, want, in) + } + } +} diff --git a/trees/heap/heapAlgorithms.go b/trees/heap/heapAlgorithms.go index 6e8195e..3962c28 100644 --- a/trees/heap/heapAlgorithms.go +++ b/trees/heap/heapAlgorithms.go @@ -28,3 +28,43 @@ func HeapSrort[T cmp.Ordered](arg []T) []T { return sorted } + +// PriorityQueue +type PriorityQueue[T cmp.Ordered] struct { + heap MinHeap[T] +} + +// NewPriorityQueue() -> creates a Priority Queue using min Heap +func NewPriorityQueue[T cmp.Ordered]() *PriorityQueue[T] { + return &PriorityQueue[T]{} +} + +// Push() -> adds new value to queue +func (q *PriorityQueue[T]) Push(v T) { + q.heap.Insert(v) +} + +// Pop() -> removes and returns the min value +func (q *PriorityQueue[T]) Pop() (T, bool) { + return q.heap.PopMin() +} + +// Peek() -> returns the upcoming item without removing it. +// ok is false when the queue is empty. +func (q *PriorityQueue[T]) Peek() (T, bool) { + var zero T + if len(q.heap.Array) == 0 { + return zero, false + } + return q.heap.Array[0], true +} + +// Size() -> return the Size of queue +func (q PriorityQueue[T]) Size() int { + return len(q.heap.Array) +} + +// Display() -> return the whole queue +func (q PriorityQueue[T]) Display() []T { + return q.heap.Array +}