Compare commits
2 Commits
d5054d55e9
...
9db06d7a7a
| Author | SHA1 | Date | |
|---|---|---|---|
| 9db06d7a7a | |||
| adb18ed13b |
@@ -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.
|
||||
@@ -0,0 +1,204 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"datastructures/trees/heap"
|
||||
)
|
||||
|
||||
// isMaxHeap verifies the max-heap property over the backing array: every
|
||||
// parent is >= each of its children.
|
||||
func isMaxHeap(a []int) bool {
|
||||
for i := range a {
|
||||
left := 2*i + 1
|
||||
right := 2*i + 2
|
||||
if left < len(a) && a[i] < a[left] {
|
||||
return false
|
||||
}
|
||||
if right < len(a) && a[i] < a[right] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestMaxHeap_PopEmpty(t *testing.T) {
|
||||
h := heap.NewMaxHeap[int]()
|
||||
|
||||
val, ok := h.PopMax()
|
||||
if ok {
|
||||
t.Fatalf("PopMax on empty heap: got ok=true, val=%d; want ok=false", val)
|
||||
}
|
||||
if val != 0 {
|
||||
t.Fatalf("PopMax on empty heap: got val=%d; want zero value 0", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxHeap_InsertMaintainsHeapProperty(t *testing.T) {
|
||||
h := heap.NewMaxHeap[int]()
|
||||
|
||||
for _, v := range []int{5, 3, 8, 1, 9, 2, 7, 6, 4, 0} {
|
||||
h.Insert(v)
|
||||
if !isMaxHeap(h.Array) {
|
||||
t.Fatalf("heap property violated after inserting %d: %v", v, h.Array)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxHeap_PopMaxReturnsDescending(t *testing.T) {
|
||||
h := heap.NewMaxHeap[int]()
|
||||
|
||||
input := []int{5, 3, 8, 1, 9, 2, 7, 6, 4, 0}
|
||||
for _, v := range input {
|
||||
h.Insert(v)
|
||||
}
|
||||
|
||||
var got []int
|
||||
for {
|
||||
val, ok := h.PopMax()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
got = append(got, val)
|
||||
|
||||
// heap property must hold after every pop
|
||||
if !isMaxHeap(h.Array) {
|
||||
t.Fatalf("heap property violated after pop: %v", h.Array)
|
||||
}
|
||||
}
|
||||
|
||||
want := make([]int, len(input))
|
||||
copy(want, input)
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(want)))
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("popped %d values; want %d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("pop order = %v; want descending %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxHeap_Duplicates(t *testing.T) {
|
||||
h := heap.NewMaxHeap[int]()
|
||||
|
||||
for _, v := range []int{4, 4, 4, 2, 2, 7, 7, 7, 7} {
|
||||
h.Insert(v)
|
||||
}
|
||||
|
||||
prev := int(^uint(0) >> 1) // max int, so first pop always <= prev
|
||||
for {
|
||||
val, ok := h.PopMax()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if val > prev {
|
||||
t.Fatalf("pop order not non-increasing: got %d after %d", val, prev)
|
||||
}
|
||||
prev = val
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxHeap_Heapify(t *testing.T) {
|
||||
h := heap.NewMaxHeap[int]()
|
||||
|
||||
input := []int{5, 3, 8, 1, 9, 2, 7, 6, 4, 0}
|
||||
if err := h.Heapify(input); err != nil {
|
||||
t.Fatalf("Heapify returned error: %v", err)
|
||||
}
|
||||
|
||||
if !isMaxHeap(h.Array) {
|
||||
t.Fatalf("Heapify did not produce a valid max heap: %v", h.Array)
|
||||
}
|
||||
|
||||
// Heapify must clone: mutating the source must not affect the heap.
|
||||
input[0] = 999
|
||||
if h.Array[0] == 999 {
|
||||
t.Fatalf("Heapify did not clone the input slice")
|
||||
}
|
||||
|
||||
// The root must be the maximum.
|
||||
if h.Array[0] != 9 {
|
||||
t.Fatalf("root after Heapify = %d; want max 9", h.Array[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxHeap_HeapifyOnNonEmptyErrors(t *testing.T) {
|
||||
h := heap.NewMaxHeap[int]()
|
||||
h.Insert(1)
|
||||
|
||||
if err := h.Heapify([]int{2, 3}); err == nil {
|
||||
t.Fatal("Heapify on non-empty heap: got nil error; want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxHeap_HeapifyThenPopSorts(t *testing.T) {
|
||||
input := []int{42, -7, 0, 15, 15, 3, 99, -100, 8}
|
||||
|
||||
h := heap.NewMaxHeap[int]()
|
||||
if err := h.Heapify(input); err != nil {
|
||||
t.Fatalf("Heapify returned error: %v", err)
|
||||
}
|
||||
|
||||
var got []int
|
||||
for {
|
||||
val, ok := h.PopMax()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
got = append(got, val)
|
||||
}
|
||||
|
||||
want := make([]int, len(input))
|
||||
copy(want, input)
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(want)))
|
||||
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("Heapify+PopMax = %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxHeap_Randomized(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(1))
|
||||
|
||||
for trial := 0; trial < 50; trial++ {
|
||||
n := r.Intn(200)
|
||||
input := make([]int, n)
|
||||
for i := range input {
|
||||
input[i] = r.Intn(1000) - 500
|
||||
}
|
||||
|
||||
h := heap.NewMaxHeap[int]()
|
||||
for _, v := range input {
|
||||
h.Insert(v)
|
||||
}
|
||||
if !isMaxHeap(h.Array) {
|
||||
t.Fatalf("trial %d: invalid heap after inserts: %v", trial, h.Array)
|
||||
}
|
||||
|
||||
var got []int
|
||||
for {
|
||||
val, ok := h.PopMax()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
got = append(got, val)
|
||||
}
|
||||
|
||||
want := make([]int, len(input))
|
||||
copy(want, input)
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(want)))
|
||||
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("trial %d: pop order = %v; want %v", trial, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package heap
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// node is stored at index i:
|
||||
// left child is at index 2*i + 1.
|
||||
// right child is at index 2*i + 2.
|
||||
// parent index [(i-1)/2].
|
||||
|
||||
// MaxHeap :: binary heap implementation, Insertion - O(log n).
|
||||
// Array[] is managed internally , treat it as read only
|
||||
type MaxHeap[T cmp.Ordered] struct {
|
||||
Array []T
|
||||
}
|
||||
|
||||
// NewMinHeap() :: Creates a new min heap tree.
|
||||
func NewMaxHeap[T cmp.Ordered]() *MaxHeap[T] {
|
||||
return &MaxHeap[T]{}
|
||||
}
|
||||
|
||||
// Insert() -> adds new value to heap.
|
||||
// O(logn)
|
||||
func (h *MaxHeap[T]) Insert(val T) {
|
||||
h.Array = append(h.Array, val)
|
||||
|
||||
h.siftUp(len(h.Array) - 1)
|
||||
}
|
||||
|
||||
// siftUp() :: moves the smaller value node up, used when inserting.
|
||||
// O(logn)
|
||||
func (h *MaxHeap[T]) siftUp(i int) {
|
||||
for i > 0 {
|
||||
parentIndex := (i - 1) / 2
|
||||
|
||||
if h.Array[i] > h.Array[parentIndex] {
|
||||
// swap places
|
||||
h.Array[i], h.Array[parentIndex] = h.Array[parentIndex], h.Array[i]
|
||||
i = parentIndex
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// siftDown() :: receives an index to move its value down the tree.
|
||||
// O(logn)
|
||||
func (h *MaxHeap[T]) siftDown(i int) {
|
||||
n := len(h.Array)
|
||||
|
||||
for {
|
||||
|
||||
smallest := i
|
||||
|
||||
left := 2*i + 1
|
||||
right := 2*i + 2
|
||||
|
||||
if left < n && h.Array[left] > h.Array[smallest] {
|
||||
smallest = left
|
||||
}
|
||||
if right < n && h.Array[right] > h.Array[smallest] {
|
||||
smallest = right
|
||||
}
|
||||
|
||||
if smallest == i {
|
||||
break
|
||||
}
|
||||
|
||||
// sink down
|
||||
h.Array[i], h.Array[smallest] = h.Array[smallest], h.Array[i]
|
||||
i = smallest
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// PopMin() -> returns the root
|
||||
func (h *MaxHeap[T]) PopMax() (T, bool) {
|
||||
var zero T
|
||||
if len(h.Array) <= 0 {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
last := len(h.Array) - 1
|
||||
root := h.Array[0]
|
||||
|
||||
// move last value to the root
|
||||
h.Array[0] = h.Array[last]
|
||||
|
||||
// GC cleanup
|
||||
h.Array[last] = zero
|
||||
|
||||
h.Array = h.Array[:last]
|
||||
|
||||
h.siftDown(0)
|
||||
|
||||
return root, true
|
||||
}
|
||||
|
||||
// Heapify() -> makes arbitrary slice a heap from scratch . returns error if
|
||||
// the heap is not empty
|
||||
func (h *MaxHeap[T]) Heapify(array []T) error {
|
||||
if len(h.Array) != 0 {
|
||||
return errors.New("Can only init Heapify on empty heap")
|
||||
}
|
||||
|
||||
h.Array = slices.Clone(array)
|
||||
|
||||
for i := len(h.Array)/2 - 1; i >= 0; i-- {
|
||||
h.siftDown(i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -39,9 +39,10 @@ func (h *MinHeap[T]) siftUp(i int) {
|
||||
if h.Array[i] < h.Array[parentIndex] {
|
||||
// swap places
|
||||
h.Array[i], h.Array[parentIndex] = h.Array[parentIndex], h.Array[i]
|
||||
i = parentIndex
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
i = parentIndex
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user