Implement a true priority queue.
* Add a binary max-heap implementation, `binheap`. * Rename `precise` package to `mq`.
This commit is contained in:
102
binheap/lib.go
Normal file
102
binheap/lib.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package binheap
|
||||
|
||||
import "golang.org/x/exp/constraints"
|
||||
|
||||
// H is a generic, non-concurrent binary max-heap.
|
||||
//
|
||||
// `I` is the type of the priority IDs, and `E` the type of the elements.
|
||||
type H[I constraints.Ordered, E any] struct {
|
||||
heap []I
|
||||
elems []E
|
||||
len int
|
||||
}
|
||||
|
||||
// Make creates a new heap.
|
||||
func Make[I constraints.Ordered, E any](cap int) H[I, E] {
|
||||
heap := make([]I, cap)
|
||||
elems := make([]E, cap)
|
||||
h := H[I, E]{heap: heap, elems: elems}
|
||||
return h
|
||||
}
|
||||
|
||||
// Capacity returns the total capacity of the heap.
|
||||
func (h *H[I, E]) Capacity() int {
|
||||
return cap(h.heap)
|
||||
}
|
||||
|
||||
// Len returns the number of items in the heap.
|
||||
func (h *H[I, E]) Len() int {
|
||||
return h.len
|
||||
}
|
||||
|
||||
// CanExtract returns true if the heap has any item, otherwise false.
|
||||
func (h *H[I, E]) CanExtract() bool {
|
||||
return h.len != 0
|
||||
}
|
||||
|
||||
// CanInsert returns true if the heap has unused capacity, otherwise false.
|
||||
func (h *H[I, E]) CanInsert() bool {
|
||||
return cap(h.heap)-h.len != 0
|
||||
}
|
||||
|
||||
// Extract returns the current heap root, then performs a heap-down pass.
|
||||
//
|
||||
// If the heap is empty, it panics.
|
||||
func (h *H[I, E]) Extract() (I, E) {
|
||||
if !h.CanExtract() {
|
||||
panic("heap is empty")
|
||||
}
|
||||
|
||||
id := h.heap[0]
|
||||
elem := h.elems[0]
|
||||
var emptyId I
|
||||
var emptyElem E
|
||||
h.heap[0] = h.heap[h.len-1]
|
||||
h.elems[0] = h.elems[h.len-1]
|
||||
h.heap[h.len-1] = emptyId
|
||||
h.elems[h.len-1] = emptyElem
|
||||
h.len--
|
||||
idx := 0
|
||||
for {
|
||||
left := idx*2 + 1
|
||||
right := idx*2 + 2
|
||||
largest := idx
|
||||
if left < h.len && h.heap[left] > h.heap[largest] {
|
||||
largest = left
|
||||
}
|
||||
if right < h.len && h.heap[right] > h.heap[largest] {
|
||||
largest = right
|
||||
}
|
||||
if largest == idx {
|
||||
break
|
||||
}
|
||||
h.heap[idx], h.heap[largest] = h.heap[largest], h.heap[idx]
|
||||
h.elems[idx], h.elems[largest] = h.elems[largest], h.elems[idx]
|
||||
idx = largest
|
||||
}
|
||||
|
||||
return id, elem
|
||||
}
|
||||
|
||||
// Insert adds an item to the heap, then performs a heap-up pass.
|
||||
//
|
||||
// If the heap is full, it panics.
|
||||
func (h *H[I, E]) Insert(id I, elem E) {
|
||||
if !h.CanInsert() {
|
||||
panic("heap is full")
|
||||
}
|
||||
|
||||
idx := h.len
|
||||
h.heap[idx] = id
|
||||
h.elems[idx] = elem
|
||||
h.len++
|
||||
for {
|
||||
parent := (idx - 1) / 2
|
||||
if parent == idx || h.heap[parent] >= h.heap[idx] {
|
||||
break
|
||||
}
|
||||
h.heap[parent], h.heap[idx] = h.heap[idx], h.heap[parent]
|
||||
h.elems[parent], h.elems[idx] = h.elems[idx], h.elems[parent]
|
||||
idx = parent
|
||||
}
|
||||
}
|
84
binheap/lib_test.go
Normal file
84
binheap/lib_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package binheap_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"gogs.humancabbage.net/sam/priorityq/binheap"
|
||||
)
|
||||
|
||||
func TestSmoke(t *testing.T) {
|
||||
h := binheap.Make[int, int](10)
|
||||
if h.Capacity() != 10 {
|
||||
t.Errorf("expected heap capacity to be 10")
|
||||
}
|
||||
h.Insert(1, 1)
|
||||
h.Insert(2, 2)
|
||||
h.Insert(3, 3)
|
||||
h.Insert(4, 4)
|
||||
if h.Len() != 4 {
|
||||
t.Errorf("expected heap length to be 4")
|
||||
}
|
||||
checkExtract := func(n int) {
|
||||
_, extracted := h.Extract()
|
||||
if extracted != n {
|
||||
t.Errorf("expected to extract %d, got %d", n, extracted)
|
||||
}
|
||||
}
|
||||
checkExtract(4)
|
||||
checkExtract(3)
|
||||
checkExtract(2)
|
||||
checkExtract(1)
|
||||
}
|
||||
|
||||
func TestInsertFullPanic(t *testing.T) {
|
||||
h := binheap.Make[int, int](4)
|
||||
h.Insert(1, 1)
|
||||
h.Insert(2, 2)
|
||||
h.Insert(3, 3)
|
||||
h.Insert(4, 4)
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("expected final insert to panic")
|
||||
}
|
||||
}()
|
||||
h.Insert(5, 5)
|
||||
}
|
||||
|
||||
func TestExtractEmptyPanic(t *testing.T) {
|
||||
h := binheap.Make[int, int](4)
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("expected extract to panic")
|
||||
}
|
||||
}()
|
||||
h.Extract()
|
||||
}
|
||||
|
||||
func TestRandomized(t *testing.T) {
|
||||
h := binheap.Make[int, int](8192)
|
||||
rs := rand.NewSource(0)
|
||||
r := rand.New(rs)
|
||||
// insert a bunch of random integers
|
||||
for i := 0; i < h.Capacity(); i++ {
|
||||
n := r.Int()
|
||||
h.Insert(n, n)
|
||||
}
|
||||
// ensure that each extracted integer is <= the last extracted integer
|
||||
var extracted []int
|
||||
for h.CanExtract() {
|
||||
id, item := h.Extract()
|
||||
if id != item {
|
||||
t.Errorf("id / item mismatch: %d %d", id, item)
|
||||
}
|
||||
lastIdx := len(extracted) - 1
|
||||
extracted = append(extracted, item)
|
||||
if lastIdx < 0 {
|
||||
continue
|
||||
}
|
||||
if item > extracted[lastIdx] {
|
||||
t.Errorf("newly extracted %d is greater than %d",
|
||||
item, extracted[lastIdx])
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user