diff --git a/deheap.go b/deheap.go index 4e06962..b73dd0b 100644 --- a/deheap.go +++ b/deheap.go @@ -430,6 +430,52 @@ func Remove(h heap.Interface, i int) (q interface{}) { return q } +// Fix re-establishes the heap ordering after the element at index i +// has changed its value. Equivalent to, but cheaper than, Remove(h, i) +// followed by Push of the new value. +// +// The index i must be in the range [0, h.Len()). +// It panics if i is out of bounds. +// +// The complexity is O(log n) where n = h.Len(). +func Fix(h heap.Interface, i int) { + l := h.Len() + // Sift down from i. At each cross-level fixup, immediately + // bubbleup the displaced element so it is not lost when the + // loop overwrites the position on the next iteration. + min := isMinHeap(i) + pos := i + for { + j := min2(h, l, min, hlchild(pos)) + if j >= l { + break + } + k := min4(h, l, min, lchild(pos)) + v := min3(h, l, min, pos, j, k) + if v == pos || v >= l { + break + } + h.Swap(v, pos) + if v == j { + pos = v + break + } + p := hparent(v) + if (min && h.Less(p, v)) || (!min && h.Less(v, p)) { + h.Swap(p, v) + bubbleup(h, isMinHeap(p), p) + } + pos = v + } + // Fix upward from the final sift position (where the modified + // element landed) and from the original position (which now + // holds a descendant that may violate ancestor constraints). + bubbleup(h, isMinHeap(pos), pos) + if pos != i { + bubbleup(h, isMinHeap(i), i) + } +} + // Push adds element o to the heap, maintaining the min-max heap property. // // The element is appended to the end of the slice (the next open slot @@ -443,16 +489,68 @@ func Push(h heap.Interface, o interface{}) { bubbleup(h, isMinHeap(i), i) } +// valid reports whether h satisfies the min-max heap property. +// +// It checks each node against its binary parent (adjacent level type) +// and grandparent (same level type). These two local checks suffice: +// transitivity along grandparent chains establishes the global property. +// +// The scan is sequential over indices 1..l-1, making it cache-friendly. +func valid(h heap.Interface, l int) bool { + for i := 1; i < l; i++ { + hp := hparent(i) + if isMinHeap(i) { + // i on min level, hp on max level: hp must be ≥ i. + if h.Less(hp, i) { + return false + } + } else { + // i on max level, hp on min level: i must be ≥ hp. + if h.Less(i, hp) { + return false + } + } + if i >= 3 { + gp := hparent(hp) + if isMinHeap(i) { + // Both min: gp must be ≤ i. + if h.Less(i, gp) { + return false + } + } else { + // Both max: gp must be ≥ i. + if h.Less(gp, i) { + return false + } + } + } + } + return true +} + +// Verify reports whether h satisfies the min-max heap property. +// +// Time complexity is O(n), where n = h.Len(). +func Verify(h heap.Interface) bool { + return valid(h, h.Len()) +} + // Init establishes the min-max heap ordering on an arbitrary slice. // Call this once on a non-empty slice before calling Pop, PopMax, or Push. // -// It works by iterating through every element and bubbling it up, -// effectively inserting elements one at a time into a growing heap. +// If the data already satisfies the heap property, Init returns after +// a linear scan with no modifications. Otherwise it uses Floyd's +// bottom-up heap construction, processing nodes from the last non-leaf +// down to the root. Most work happens near the leaves where subtrees +// are small and memory accesses are local. // // Time complexity is O(n), where n = h.Len(). func Init(h heap.Interface) { l := h.Len() - for i := 0; i < l; i++ { - bubbleup(h, isMinHeap(i), i) + if valid(h, l) { + return + } + for i := (l - 1) / 2; i >= 0; i-- { + bubbledown(h, l, isMinHeap(i), i) } } diff --git a/deheap_test.go b/deheap_test.go index 0e9c294..0b8d86e 100644 --- a/deheap_test.go +++ b/deheap_test.go @@ -224,13 +224,13 @@ func TestBubbleUp(t *testing.T) { h = &IntHeap{2, 15, 13, 4, 6, 8, 1} bubbleup(h, isMinHeap(6), 6) - if _, _, ok := isHeap(t, h); !ok { + if !Verify(h) { t.Fatalf("unexpected value: %v", h) } h = &IntHeap{1, 15, 14, 2, 3, 4, 5, 13, 12, 11, 10, 6, 7, 8, 9} bubbleup(h, isMinHeap(14), 14) - if _, _, ok := isHeap(t, h); !ok { + if !Verify(h) { t.Fatalf("unexpected value: %v", h) } @@ -238,7 +238,7 @@ func TestBubbleUp(t *testing.T) { // TestBubbleDown verifies the downward sift operation on hand-crafted heaps. // Cases: 3-element swap, 7-element with grandchild swap, 12-element with -// multi-level cascade, and two larger heaps validated via isHeap. +// multi-level cascade, and two larger heaps validated via Verify. func TestBubbleDown(t *testing.T) { h := &IntHeap{15, 1, 2} @@ -261,13 +261,13 @@ func TestBubbleDown(t *testing.T) { h = &IntHeap{14, 15, 12, 4, 2, 3, 5, 13} bubbledown(h, h.Len(), isMinHeap(0), 0) - if _, _, ok := isHeap(t, h); !ok { + if !Verify(h) { t.Fatalf("unexpected value: %v", h) } h = &IntHeap{13, 14, 15, 3, 4, 5, 6, 7, 8, 9, 10} bubbledown(h, h.Len(), isMinHeap(0), 0) - if _, _, ok := isHeap(t, h); !ok { + if !Verify(h) { t.Fatalf("unexpected value: %v", h) } @@ -366,8 +366,8 @@ func TestMin3(t *testing.T) { func TestInit(t *testing.T) { h := &IntHeap{15, 1, 2, 14, 13, 12, 11, 3, 4, 5, 6, 7, 8, 9, 10} Init(h) - if x, y, ok := isHeap(t, h); !ok { - t.Fatalf("unexpected value: %v %v %v", x, y, h) + if !Verify(h) { + t.Fatalf("not a valid heap: %v", *h) } } @@ -505,6 +505,139 @@ func TestRemoveTwoElements(t *testing.T) { } } +// TestFix verifies Fix restores the heap property after modifying a single +// element. Cases cover both directions (bubbledown and bubbleup), both +// level types (min and max), root vs interior vs leaf, and no-op. +func TestFix(t *testing.T) { + // Increase root (min level) — needs bubbledown. + h := &IntHeap{1, 9, 5, 4, 6, 3, 2} + (*h)[0] = 100 + Fix(h, 0) + if !Verify(h) { + t.Fatalf("Fix root increase: not a heap: %v", *h) + } + + // Decrease root — already min, no movement needed. + h = &IntHeap{1, 9, 5, 4, 6, 3, 2} + (*h)[0] = -1 + Fix(h, 0) + if !Verify(h) { + t.Fatalf("Fix root decrease: not a heap: %v", *h) + } + + // Decrease min-level node to new global min — bubbleup through min chain. + h = &IntHeap{1, 9, 5, 4, 6, 3, 2} + (*h)[3] = -1 + Fix(h, 3) + if !Verify(h) { + t.Fatalf("Fix min node decrease: not a heap: %v", *h) + } + if (*h)[0] != -1 { + t.Fatalf("Fix min node decrease: root = %d, want -1", (*h)[0]) + } + + // Increase min-level leaf — bubbleup through max chain. + h = &IntHeap{1, 9, 5, 4, 6, 3, 2} + (*h)[6] = 100 + Fix(h, 6) + if !Verify(h) { + t.Fatalf("Fix min leaf increase: not a heap: %v", *h) + } + + // Decrease max-level node — needs bubbledown or cross-level swap. + h = &IntHeap{1, 9, 5, 4, 6, 3, 2} + (*h)[1] = 0 + Fix(h, 1) + if !Verify(h) { + t.Fatalf("Fix max node decrease: not a heap: %v", *h) + } + + // Increase max-level node — no movement needed (already max). + h = &IntHeap{1, 9, 5, 4, 6, 3, 2} + (*h)[1] = 100 + Fix(h, 1) + if !Verify(h) { + t.Fatalf("Fix max node increase: not a heap: %v", *h) + } + + // No-op: value unchanged. + h = &IntHeap{1, 9, 5, 4, 6, 3, 2} + Fix(h, 3) + if !Verify(h) { + t.Fatalf("Fix no-op: not a heap: %v", *h) + } + + // Single element. + h = &IntHeap{42} + Init(h) + (*h)[0] = 99 + Fix(h, 0) + if !Verify(h) { + t.Fatalf("Fix single: not a heap: %v", *h) + } + + // Two elements: fix min. + h = &IntHeap{3, 7} + Init(h) + (*h)[0] = 10 + Fix(h, 0) + if !Verify(h) { + t.Fatalf("Fix two (min): not a heap: %v", *h) + } + + // Two elements: fix max. + h = &IntHeap{3, 7} + Init(h) + (*h)[1] = 1 + Fix(h, 1) + if !Verify(h) { + t.Fatalf("Fix two (max): not a heap: %v", *h) + } +} + +// TestFixRandomized modifies random elements in random heaps and verifies +// Fix restores the heap property. Validates both structure (Verify) and +// content (sorted drain matches oracle). +func TestFixRandomized(t *testing.T) { + s := rand.New(rand.NewSource(time.Now().UnixNano())) + + for iter := 0; iter < 1000; iter++ { + n := s.Intn(64) + 2 + h := randIntHeapWithDups(t, n, 0.1) + + // Build sorted oracle from current heap state. + oracle := make([]int, h.Len()) + copy(oracle, *h) + sort.Ints(oracle) + + // Modify a random element and fix. + idx := s.Intn(h.Len()) + oldVal := (*h)[idx] + newVal := s.Intn(n*2) - n + (*h)[idx] = newVal + Fix(h, idx) + + // Update oracle: remove old, insert new. + j := sort.SearchInts(oracle, oldVal) + oracle = append(oracle[:j], oracle[j+1:]...) + k := sort.SearchInts(oracle, newVal) + oracle = append(oracle, 0) + copy(oracle[k+1:], oracle[k:]) + oracle[k] = newVal + + if !Verify(h) { + t.Fatalf("iter %d: Fix(%d) broke heap: %v", iter, idx, *h) + } + + for di, want := range oracle { + got := Pop(h).(int) + if got != want { + t.Fatalf("iter %d: Pop[%d] = %d, want %d", iter, di, got, want) + } + } + } +} + // TestPush verifies Push maintains the heap property under three insertion // patterns: ascending order, descending order (validated after each push), // and alternating high/low values. @@ -514,15 +647,15 @@ func TestPush(t *testing.T) { for i := 0; i < 32; i++ { Push(h, i) } - if x, y, ok := isHeap(t, h); !ok { - t.Fatalf("unexpected value: %v %v %v", x, y, h) + if !Verify(h) { + t.Fatalf("not a valid heap: %v", *h) } h = &IntHeap{} for i := 3; i >= 0; i-- { Push(h, i) - if x, y, ok := isHeap(t, h); !ok { - t.Fatalf("unexpected value: %v %v %v", x, y, h) + if !Verify(h) { + t.Fatalf("not a valid heap: %v", *h) } } @@ -535,8 +668,8 @@ func TestPush(t *testing.T) { } Push(h, k) } - if x, y, ok := isHeap(t, h); !ok { - t.Fatalf("unexpected value: %v %v %v", x, y, h) + if !Verify(h) { + t.Fatalf("not a valid heap: %v", *h) } } @@ -601,8 +734,8 @@ func TestPops(t *testing.T) { for ti, tv := range ts { - if a, b, ok := isHeap(t, &tv.h); !ok { - t.Fatalf("unexpected value: %d %d %v", a, b, tv.h) + if !Verify(&tv.h) { + t.Fatalf("not a valid heap: %v", tv.h) } t.Run(fmt.Sprintf("%d Pop", ti), func(t1 *testing.T) { @@ -668,7 +801,7 @@ func TestRemove(t *testing.T) { if x != 3 { t.Fatalf("unexpected value") } - if _, _, ok := isHeap(t, h); !ok { + if !Verify(h) { t.Fatalf("unexpected value") } if !reflect.DeepEqual(h, &IntHeap{0, 9, 5, 6, 1, 2, 4, 8, 7}) { @@ -679,7 +812,7 @@ func TestRemove(t *testing.T) { if x != 5 { t.Fatalf("unexpected value") } - if _, _, ok := isHeap(t, h); !ok { + if !Verify(h) { t.Fatalf("unexpected value") } if !reflect.DeepEqual(h, &IntHeap{0, 9, 7, 6, 1, 2, 4, 8}) { @@ -690,7 +823,7 @@ func TestRemove(t *testing.T) { if x != 0 { t.Fatalf("unexpected value") } - if _, _, ok := isHeap(t, h); !ok { + if !Verify(h) { t.Fatalf("unexpected value") } if !reflect.DeepEqual(h, &IntHeap{1, 9, 7, 6, 8, 2, 4}) && !reflect.DeepEqual(h, &IntHeap{1, 9, 8, 6, 7, 2, 4}) { @@ -730,7 +863,7 @@ func TestOps(t *testing.T) { } y1 = x } - if _, _, ok := isHeap(t, h); !ok { + if !Verify(h) { t.Fatalf("unexpected value") } } @@ -741,8 +874,8 @@ func TestOps(t *testing.T) { copy(h0, []int(*h)) x := s.Intn(h.Len()) Remove(h, x) - if i, j, ok := isHeap(t, h); !ok { - t.Fatalf("unexpected value: %d %d %d\n%v\n%v", x, i, j, h0, h) + if !Verify(h) { + t.Fatalf("unexpected value: removed %d\n%v\n%v", x, h0, h) } } @@ -980,6 +1113,9 @@ func FuzzV1PushPop(f *testing.F) { Push(h, v) oracle.push(v) } + if !Verify(h) { + t.Fatalf("Verify() failed") + } if h.Len() != oracle.len() { t.Fatalf("length mismatch: heap=%d, oracle=%d", h.Len(), oracle.len()) } @@ -1006,6 +1142,40 @@ func FuzzV1Remove(f *testing.F) { for h.Len() > 0 { idx := s.Intn(h.Len()) Remove(h, idx) + if !Verify(h) { + t.Fatalf("Verify() failed after Remove(%d)", idx) + } + } + }) +} + +// FuzzV1Fix pushes the first half of bytes onto a heap, then interprets the +// second half as (index, newValue) pairs for Fix operations. Validates the +// heap property after every Fix. +func FuzzV1Fix(f *testing.F) { + f.Add([]byte{5, 3, 8, 1, 9, 2, 7, 4, 6}) + f.Add([]byte{1, 1, 1, 1, 1, 1}) + f.Add([]byte{}) + + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) < 2 { + return + } + mid := len(data) / 2 + h := &IntHeap{} + for _, c := range data[:mid] { + Push(h, int(c)) + } + for i := mid; i+1 < len(data); i += 2 { + if h.Len() == 0 { + break + } + idx := int(data[i]) % h.Len() + (*h)[idx] = int(data[i+1]) + Fix(h, idx) + if !Verify(h) { + t.Fatalf("Fix(%d) broke heap", idx) + } } }) } @@ -1040,51 +1210,6 @@ func (s *sortedOracle) len() int { return len(*s) } -// isHeap validates the min-max heap property for every node. -// For each node i it checks: -// - Grandparent (same level type): on a min level grandparent <= node, -// on a max level grandparent >= node. -// - Binary parent (opposite level type): on a min level the max-level -// parent >= node, on a max level the min-level parent <= node. -func isHeap(t *testing.T, h heap.Interface) (int, int, bool) { - t.Helper() - l := h.Len() - for i := l - 1; i >= 0; i-- { - min := isMinHeap(i) - p0 := parent(i) - p1 := hparent(i) - // Grandparent (same level type as i): - // min level: grandparent <= node, violation if node < grandparent - // max level: grandparent >= node, violation if grandparent < node - if p0 >= 0 { - if min && h.Less(i, p0) { - return p0, i, false - } - if !min && h.Less(p0, i) { - return p0, i, false - } - } - // Binary parent (opposite level type from i): - // i on min level, parent on max: parent >= i, violation if parent < i - // i on max level, parent on min: parent <= i, violation if i < parent - if p1 >= 0 { - if min && h.Less(p1, i) { - return p1, i, false - } - if !min && h.Less(i, p1) { - return p1, i, false - } - } - } - if l > 1 && h.Less(1, 0) { - return 1, 0, false - } - if l > 2 && h.Less(2, 0) { - return 2, 0, false - } - return 0, 0, true -} - // randIntHeapWithDups builds a random heap of n elements. The fraction // parameter controls how many duplicate values are inserted (e.g. 0.1 // means ~10% extra duplicates). @@ -1107,8 +1232,8 @@ func randIntHeapWithDups(t *testing.T, n int, fraction float64) *IntHeap { Push(h, q) } - if x, y, ok := isHeap(t, h); !ok { - panic(fmt.Sprintf("not a heap!: %d %d", x, y)) + if !Verify(h) { + panic(fmt.Sprintf("not a heap!: %v", *h)) } return h diff --git a/example_intheap_test.go b/example_intheap_test.go index 3a9a62e..a0ec1e4 100644 --- a/example_intheap_test.go +++ b/example_intheap_test.go @@ -50,3 +50,17 @@ func Example_intHeap() { // minimum: 1 // 6 5 1 2 middle value: 3 } + +// This example demonstrates using Verify to check heap validity. +func ExampleVerify() { + h := &IntDeheap{2, 1, 5, 6} + deheap.Init(h) + fmt.Println(deheap.Verify(h)) + + // Corrupt the heap by placing a small value at a max-level position. + (*h)[1] = -1 + fmt.Println(deheap.Verify(h)) + // Output: + // true + // false +} diff --git a/example_ordered_test.go b/example_ordered_test.go index a4b75d8..439b706 100644 --- a/example_ordered_test.go +++ b/example_ordered_test.go @@ -46,3 +46,14 @@ func Example_ordered() { // maximum: 6 // 6 5 1 2 middle value: 3 } + +func ExampleDeheap_Verify() { + h := deheap.From(2, 1, 5, 6) + fmt.Println(h.Verify()) + + h.Push(3) + fmt.Println(h.Verify()) + // Output: + // true + // true +} diff --git a/ordered.go b/ordered.go index e59f1ca..39201d0 100644 --- a/ordered.go +++ b/ordered.go @@ -48,12 +48,18 @@ func New[T cmp.Ordered]() *Deheap[T] { } // From constructs a Deheap from the given elements and initializes -// the heap ordering. +// the heap ordering using Floyd's bottom-up heap construction. +// +// If the input already satisfies the heap property, From returns +// after a linear scan with no modifications. func From[T cmp.Ordered](items ...T) *Deheap[T] { q := &Deheap[T]{items: make([]T, len(items))} copy(q.items, items) - for i := range q.items { - orderedBubbleup(q.items, isMinHeap(i), i) + l := len(q.items) + if !orderedValid(q.items, l) { + for i := (l - 1) / 2; i >= 0; i-- { + orderedBubbledown(q.items, l, isMinHeap(i), i) + } } return q } @@ -118,6 +124,46 @@ func (p *Deheap[T]) Remove(i int) T { return v } +// Fix re-establishes the heap ordering after the element at index i +// has changed its value. Equivalent to, but cheaper than, Remove(i) +// followed by Push of the new value. +// +// The index i must be in the range [0, p.Len()). +// It panics if i is out of bounds. +// +// The complexity is O(log n) where n = p.Len(). +func (p *Deheap[T]) Fix(i int) { + l := len(p.items) + min := isMinHeap(i) + pos := i + for { + j := orderedMin2(p.items, l, min, hlchild(pos)) + if j >= l { + break + } + k := orderedMin4(p.items, l, min, lchild(pos)) + v := orderedMin3(p.items, l, min, pos, j, k) + if v == pos || v >= l { + break + } + p.items[v], p.items[pos] = p.items[pos], p.items[v] + if v == j { + pos = v + break + } + hp := hparent(v) + if orderedLess(p.items, min, hp, v) { + p.items[hp], p.items[v] = p.items[v], p.items[hp] + orderedBubbleup(p.items, isMinHeap(hp), hp) + } + pos = v + } + orderedBubbleup(p.items, isMinHeap(pos), pos) + if pos != i { + orderedBubbleup(p.items, isMinHeap(i), i) + } +} + // Len returns the number of elements in the heap. func (p *Deheap[T]) Len() int { return len(p.items) @@ -153,6 +199,13 @@ func (p *Deheap[T]) PeekMax() T { return p.items[2] } +// Verify reports whether the heap satisfies the min-max heap property. +// +// Time complexity is O(n), where n = p.Len(). +func (p *Deheap[T]) Verify() bool { + return orderedValid(p.items, len(p.items)) +} + // --------------------------------------------------------------------------- // Generic algorithm functions // @@ -166,6 +219,36 @@ func (p *Deheap[T]) PeekMax() T { // type dependency. // --------------------------------------------------------------------------- +// orderedValid reports whether items satisfies the min-max heap property. +// See valid in deheap.go for the algorithm description. +func orderedValid[T cmp.Ordered](items []T, l int) bool { + for i := 1; i < l; i++ { + hp := hparent(i) + if isMinHeap(i) { + if items[hp] < items[i] { + return false + } + } else { + if items[i] < items[hp] { + return false + } + } + if i >= 3 { + gp := hparent(hp) + if isMinHeap(i) { + if items[i] < items[gp] { + return false + } + } else { + if items[gp] < items[i] { + return false + } + } + } + } + return true +} + // orderedLess compares two elements, respecting the min flag. // When min=true, returns whether items[a] < items[b]. // When min=false, returns whether items[a] > items[b]. diff --git a/ordered_test.go b/ordered_test.go index 6770627..f0eba90 100644 --- a/ordered_test.go +++ b/ordered_test.go @@ -26,6 +26,7 @@ package deheap import ( "math" "math/rand" + "reflect" "sort" "testing" "time" @@ -105,6 +106,9 @@ func TestOrderedPushPop(t *testing.T) { for i := 32; i >= 0; i-- { h.Push(i) } + if !h.Verify() { + t.Fatalf("Verify() failed after Push") + } prev := -1 for h.Len() > 0 { v := h.Pop() @@ -122,6 +126,9 @@ func TestOrderedPopMax(t *testing.T) { for i := 0; i < 33; i++ { h.Push(i) } + if !h.Verify() { + t.Fatalf("Verify() failed after Push") + } prev := math.MaxInt for h.Len() > 0 { v := h.PopMax() @@ -136,6 +143,9 @@ func TestOrderedPopMax(t *testing.T) { // and drains correctly via Pop. func TestOrderedFrom(t *testing.T) { h := From(5, 3, 8, 1, 9, 2, 7, 4, 6) + if !h.Verify() { + t.Fatalf("Verify() failed after From") + } want := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} for i, w := range want { v := h.Pop() @@ -197,6 +207,9 @@ func TestOrderedRemove(t *testing.T) { for k := 0; k < len(items); k++ { h := From(items...) removed := h.Remove(k) + if !h.Verify() { + t.Fatalf("Verify() failed after Remove(%d)", k) + } // Collect remaining via Pop var got []int for h.Len() > 0 { @@ -249,6 +262,9 @@ func TestOrderedRandomPopPopMax(t *testing.T) { } hi = v } + if !h.Verify() { + t.Fatalf("iter %d: Verify() failed", iter) + } } } } @@ -276,6 +292,9 @@ func TestOrderedRandomRemove(t *testing.T) { for h.Len() > 0 { idx := s.Intn(h.Len()) removed := h.Remove(idx) + if !h.Verify() { + t.Fatalf("iter %d: Verify() failed after Remove(%d)", iter, idx) + } j := sort.SearchInts(oracle, removed) oracle = append(oracle[:j], oracle[j+1:]...) } @@ -326,6 +345,9 @@ func FuzzOrderedPushPop(f *testing.F) { copy(oracle[i+1:], oracle[i:]) oracle[i] = v } + if !h.Verify() { + t.Fatalf("Verify() failed") + } if h.Len() != len(oracle) { t.Fatalf("length mismatch: heap=%d, oracle=%d", h.Len(), len(oracle)) } @@ -356,6 +378,9 @@ func FuzzOrderedRemove(f *testing.F) { for h.Len() > 0 { idx := s.Intn(h.Len()) removed := h.Remove(idx) + if !h.Verify() { + t.Fatalf("Verify() failed after Remove") + } j := sort.SearchInts(oracle, removed) oracle = append(oracle[:j], oracle[j+1:]...) } @@ -366,6 +391,62 @@ func FuzzOrderedRemove(f *testing.F) { }) } +// FuzzOrderedFix pushes the first half of bytes onto a heap, then interprets +// the second half as (index, newValue) pairs for Fix operations. Drains the +// heap and validates sorted output against an oracle. +func FuzzOrderedFix(f *testing.F) { + f.Add([]byte{5, 3, 8, 1, 9, 2, 7, 4, 6}) + f.Add([]byte{1, 1, 1, 1, 1, 1}) + f.Add([]byte{}) + + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) < 2 { + return + } + mid := len(data) / 2 + h := New[int]() + var oracle []int + for _, c := range data[:mid] { + v := int(c) + h.Push(v) + i := sort.SearchInts(oracle, v) + oracle = append(oracle, 0) + copy(oracle[i+1:], oracle[i:]) + oracle[i] = v + } + + for i := mid; i+1 < len(data); i += 2 { + if h.Len() == 0 { + break + } + idx := int(data[i]) % h.Len() + newVal := int(data[i+1]) + oldVal := h.items[idx] + + // Update oracle. + j := sort.SearchInts(oracle, oldVal) + oracle = append(oracle[:j], oracle[j+1:]...) + k := sort.SearchInts(oracle, newVal) + oracle = append(oracle, 0) + copy(oracle[k+1:], oracle[k:]) + oracle[k] = newVal + + h.items[idx] = newVal + h.Fix(idx) + if !h.Verify() { + t.Fatalf("Verify() failed after Fix") + } + } + + for di, want := range oracle { + got := h.Pop() + if got != want { + t.Fatalf("Pop[%d] = %d, want %d", di, got, want) + } + } + }) +} + // TestOrderedRemoveSingleElement verifies Remove(0) on a 1-element heap // returns the correct value and leaves the heap empty. func TestOrderedRemoveSingleElement(t *testing.T) { @@ -422,6 +503,143 @@ func TestOrderedRemoveTwoElements(t *testing.T) { } } +// TestOrderedFix verifies Fix restores the heap property after modifying +// a single element. Covers bubbledown, bubbleup, both level types, +// root/leaf/interior, and edge cases. +func TestOrderedFix(t *testing.T) { + drain := func(h *Deheap[int]) []int { + var out []int + for h.Len() > 0 { + out = append(out, h.Pop()) + } + return out + } + + // Increase root (min level) — needs bubbledown. + h := From(1, 9, 5, 4, 6, 3, 2) + h.items[0] = 100 + h.Fix(0) + if !h.Verify() { + t.Fatalf("Fix root increase: Verify() failed") + } + got := drain(h) + want := []int{2, 3, 4, 5, 6, 9, 100} + if !reflect.DeepEqual(got, want) { + t.Fatalf("Fix root increase: got %v, want %v", got, want) + } + + // Decrease min-level node to new global min — bubbleup. + h = From(1, 9, 5, 4, 6, 3, 2) + h.items[3] = -1 + h.Fix(3) + if !h.Verify() { + t.Fatalf("Fix min node decrease: Verify() failed") + } + if h.Peek() != -1 { + t.Fatalf("Fix min node decrease: Peek = %d, want -1", h.Peek()) + } + + // Increase min-level leaf — bubbleup through max chain. + h = From(1, 9, 5, 4, 6, 3, 2) + h.items[6] = 100 + h.Fix(6) + if !h.Verify() { + t.Fatalf("Fix min leaf increase: Verify() failed") + } + if h.PeekMax() != 100 { + t.Fatalf("Fix min leaf increase: PeekMax = %d, want 100", h.PeekMax()) + } + + // Decrease max-level node. + h = From(1, 9, 5, 4, 6, 3, 2) + h.items[1] = 0 + h.Fix(1) + if !h.Verify() { + t.Fatalf("Fix max node decrease: Verify() failed") + } + if h.Peek() != 0 { + t.Fatalf("Fix max node decrease: Peek = %d, want 0", h.Peek()) + } + + // No-op: value unchanged. + h = From(1, 9, 5, 4, 6, 3, 2) + h.Fix(3) + if !h.Verify() { + t.Fatalf("Fix no-op: Verify() failed") + } + got = drain(h) + want = []int{1, 2, 3, 4, 5, 6, 9} + if !reflect.DeepEqual(got, want) { + t.Fatalf("Fix no-op: got %v, want %v", got, want) + } + + // Single element. + h = From(42) + h.items[0] = 99 + h.Fix(0) + if !h.Verify() { + t.Fatalf("Fix single: Verify() failed") + } + if h.Pop() != 99 { + t.Fatalf("Fix single: unexpected value") + } + + // Two elements: fix min to exceed max. + h = From(3, 7) + h.items[0] = 10 + h.Fix(0) + if !h.Verify() { + t.Fatalf("Fix two (min): Verify() failed") + } + if h.Peek() != 7 { + t.Fatalf("Fix two (min): Peek = %d, want 7", h.Peek()) + } +} + +// TestOrderedFixRandomized modifies random elements in random heaps and +// verifies Fix restores the heap property. Validates content by draining. +func TestOrderedFixRandomized(t *testing.T) { + s := rand.New(rand.NewSource(time.Now().UnixNano())) + + for iter := 0; iter < 1000; iter++ { + n := s.Intn(64) + 2 + h := New[int]() + for i := 0; i < n; i++ { + h.Push(s.Intn(n)) + } + + // Build sorted oracle from current heap state. + oracle := make([]int, len(h.items)) + copy(oracle, h.items) + sort.Ints(oracle) + + // Modify a random element and fix. + idx := s.Intn(h.Len()) + oldVal := h.items[idx] + newVal := s.Intn(n*2) - n + h.items[idx] = newVal + h.Fix(idx) + if !h.Verify() { + t.Fatalf("iter %d: Verify() failed after Fix(%d)", iter, idx) + } + + // Update oracle: remove old, insert new. + j := sort.SearchInts(oracle, oldVal) + oracle = append(oracle[:j], oracle[j+1:]...) + k := sort.SearchInts(oracle, newVal) + oracle = append(oracle, 0) + copy(oracle[k+1:], oracle[k:]) + oracle[k] = newVal + + for di, want := range oracle { + got := h.Pop() + if got != want { + t.Fatalf("iter %d: Pop[%d] = %d, want %d", iter, di, got, want) + } + } + } +} + // TestOrderedPeekSmall verifies Peek on 1-element and 2-element heaps. func TestOrderedPeekSmall(t *testing.T) { h := From(42)