tests and docs

This commit is contained in:
Acid
2026-07-14 17:54:25 -04:00
parent d5054d55e9
commit adb18ed13b
3 changed files with 263 additions and 0 deletions
+87
View File
@@ -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.
+136
View File
@@ -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)
}
}
}
+40
View File
@@ -28,3 +28,43 @@ func HeapSrort[T cmp.Ordered](arg []T) []T {
return sorted 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
}