https://github.com/aalpar/deheap
Generic double-ended heap (min-max heap) for Go — O(1) min and max, O(log n) push/pop
https://github.com/aalpar/deheap
data-structure deque double-ended-queue generics go golang heap heaps min-max-heap priority-queue
Last synced: 16 days ago
JSON representation
Generic double-ended heap (min-max heap) for Go — O(1) min and max, O(log n) push/pop
- Host: GitHub
- URL: https://github.com/aalpar/deheap
- Owner: aalpar
- License: mit
- Created: 2019-04-22T18:44:48.000Z (almost 7 years ago)
- Default Branch: master
- Last Pushed: 2026-03-07T03:39:12.000Z (19 days ago)
- Last Synced: 2026-03-07T12:27:41.020Z (19 days ago)
- Topics: data-structure, deque, double-ended-queue, generics, go, golang, heap, heaps, min-max-heap, priority-queue
- Language: Go
- Size: 65.4 KB
- Stars: 15
- Watchers: 1
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-go - deheap - Doubly-ended heap (min-max heap) with O(log n) access to both minimum and maximum elements. (Data Structures and Algorithms / Queues)
- fucking-awesome-go - deheap - Doubly-ended heap (min-max heap) with O(log n) access to both minimum and maximum elements. (Data Structures and Algorithms / Queues)
- awesome-go-with-stars - deheap - ended heap (min-max heap) with O(log n) access to both minimum and maximum elements. | 2026-03-12 | (Data Integration Frameworks / Queues)
- awesome-go - deheap - Doubly-ended heap (min-max heap) with O(log n) access to both minimum and maximum elements. (Data Structures and Algorithms / Queues)
README
# deheap
[](https://pkg.go.dev/github.com/aalpar/deheap)
[](https://goreportcard.com/report/github.com/aalpar/deheap)
[](https://codecov.io/gh/aalpar/deheap)
A doubly-ended heap (min-max heap) for Go. Provides O(log n) access to both
the minimum and maximum elements of a collection through a single data
structure, with zero external dependencies.
```
go get github.com/aalpar/deheap
```
## Why a doubly-ended heap?
A standard binary heap gives you efficient access to one extremum — the
smallest or the largest element — but not both. Many practical problems need
both ends simultaneously:
**Scheduling and resource allocation.** Operating system schedulers and job
queues routinely need the highest-priority task (to run next) and the
lowest-priority task (to evict or age). A doubly-ended priority queue avoids
maintaining two separate heaps and the bookkeeping to keep them synchronized.
**Bounded-size caches and buffers.** When a priority queue has a capacity
limit, insertions must discard the least valuable element. With a min-max
heap, both the insertion (against the max) and the eviction (from the min, or
vice versa) are logarithmic — no linear scan required.
**Median maintenance and order statistics.** Streaming median algorithms
typically partition data into a max-heap of the lower half and a min-heap of
the upper half. A single min-max heap can serve double duty, simplifying the
implementation.
**Network packet scheduling.** Rate-controlled and deadline-aware packet
schedulers (e.g., in QoS systems) need to dequeue by earliest deadline and
drop by lowest priority, both efficiently.
**Search algorithms.** Algorithms like SMA\* (Simplified Memory-Bounded A\*)
maintain an open set where the node with the lowest f-cost is expanded next
and the node with the highest f-cost is pruned when memory is exhausted.
## API
`deheap` provides two API surfaces.
### Generic API (Go 1.21+)
For `cmp.Ordered` types — `int`, `float64`, `string`, and friends — use the
type-safe generic API directly:
```go
import "github.com/aalpar/deheap"
// Construct from existing elements.
h := deheap.From(5, 3, 8, 1, 9)
// Or build incrementally.
h := deheap.New[int]()
h.Push(5)
h.Push(3)
// O(1) access to both extrema.
fmt.Println(h.Peek()) // smallest
fmt.Println(h.PeekMax()) // largest
// O(log n) removal from either end.
min := h.Pop()
max := h.PopMax()
// Remove by index.
val := h.Remove(2)
// Update an element and restore heap order.
h.Push(10)
h.Push(20)
// ... mutate the element at index 0 ...
h.Fix(0)
// Check if the heap is valid.
fmt.Println(h.Verify()) // true
```
### Interface API
For custom types, implement `heap.Interface` and use the package-level
functions. This is the original v1 API and remains stable.
```go
import "github.com/aalpar/deheap"
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
h := &IntHeap{2, 1, 5, 6}
deheap.Init(h)
deheap.Push(h, 3)
min := deheap.Pop(h)
max := deheap.PopMax(h)
// Update an element and restore heap order.
(*h)[0] = 42
deheap.Fix(h, 0)
// Check if the heap is valid.
fmt.Println(deheap.Verify(h)) // true
```
## Implementation
The underlying data structure is a **min-max heap** stored in a flat slice
with no pointers and no additional bookkeeping. Nodes on even levels
(0, 2, 4, ...) satisfy the min-heap property with respect to their
descendants, and nodes on odd levels (1, 3, 5, ...) satisfy the max-heap
property. The root is always the minimum; the maximum is one of its two
children.
Level parity is determined by bit-length of the 1-based index, computed via
`math/bits.Len` — a single CPU instruction on most architectures. Insertions
bubble up through grandparent links; deletions bubble down through
grandchild links, with a secondary swap against the binary-tree parent when
the element crosses a level boundary.
The generic API implements the same algorithms directly on `[]T` with
native `<` comparisons, eliminating interface dispatch, adapter allocation,
and boxing overhead.
### Complexity
| Operation | Time | Space |
|-----------|----------|-------|
| `Push` | O(log n) | O(1) amortized |
| `Pop` | O(log n) | O(1) |
| `PopMax` | O(log n) | O(1) |
| `Remove` | O(log n) | O(1) |
| `Fix` | O(log n) | O(1) |
| `Peek` | O(1) | O(1) |
| `PeekMax` | O(1) | O(1) |
| `Init` | O(n) | O(1) |
| `Verify` | O(n) | O(1) |
Storage is a single contiguous slice — one element per slot, no child
pointers, no color bits, no auxiliary arrays. Memory overhead beyond the
elements themselves is the slice header (24 bytes on 64-bit systems).
### Benchmarks
Measured on Apple M4 Max (arm64), Go 1.23:
**Generic API** (`Deheap[T]` — direct `<` comparisons, no interface dispatch):
| Operation | ns/op | B/op | allocs/op |
|-------------|--------|------|-----------|
| Push | 12 | 45 | 0 |
| Pop | 225 | 0 | 0 |
| PopMax | 225 | 0 | 0 |
**Interface API** (v1 — `heap.Interface`):
| Operation | ns/op | B/op | allocs/op |
|-------------|--------|------|-----------|
| Push | 21 | 54 | 0 |
| Pop | 288 | 7 | 0 |
| PopMax | 282 | 7 | 0 |
**Standard library** (`container/heap`) for comparison:
| Operation | ns/op | B/op | allocs/op |
|-------------|--------|------|-----------|
| Push | 22 | 55 | 0 |
| Pop | 208 | 7 | 0 |
The generic API is faster than `container/heap` on Push. Pop is ~8% slower
due to examining up to four grandchildren per node (vs two children in a
binary heap), but provides O(log n) access to both extrema. The v1 interface
API pays the same `heap.Interface` dispatch cost as `container/heap`. All
operations are zero-allocation in steady state.
#### Benchmark descriptions
The benchmarks compare deheap's two API surfaces against each other and
against `container/heap`. Baseline measurements isolate the cost of slice
operations from heap logic. All benchmarks use a heap of 10,000 `int`
elements to ensure the working set fits in L1 cache while exercising the
full tree depth.
Sample run (Apple M4 Max, Go 1.23):
```
$ go test -bench . -benchmem
goos: darwin
goarch: arm64
pkg: github.com/aalpar/deheap
cpu: Apple M4 Max
BenchmarkMin4-16 261428533 4.784 ns/op 0 B/op 0 allocs/op
BenchmarkBaselinePush-16 1000000000 6.411 ns/op 42 B/op 0 allocs/op
BenchmarkPush-16 44383689 23.16 ns/op 50 B/op 0 allocs/op
BenchmarkPop-16 5666707 326.3 ns/op 7 B/op 0 allocs/op
BenchmarkPopMax-16 5644575 319.7 ns/op 7 B/op 0 allocs/op
BenchmarkPushPop-16 5098502 297.2 ns/op 65 B/op 1 allocs/op
BenchmarkHeapPushPop-16 6203408 256.5 ns/op 56 B/op 1 allocs/op
BenchmarkHeapPop-16 8475319 234.7 ns/op 7 B/op 0 allocs/op
BenchmarkHeapPush-16 46837188 21.57 ns/op 48 B/op 0 allocs/op
BenchmarkOrderedPush-16 100000000 12.38 ns/op 45 B/op 0 allocs/op
BenchmarkOrderedPop-16 8680756 249.4 ns/op 0 B/op 0 allocs/op
BenchmarkOrderedPopMax-16 10103798 237.3 ns/op 0 B/op 0 allocs/op
BenchmarkOrderedPushPop-16 8926204 233.2 ns/op 44 B/op 0 allocs/op
PASS
```
| Benchmark | What it measures |
|-----------|-----------------|
| `OrderedPush` | Generic `Deheap[int].Push`: append + bubble-up with direct `<` comparisons. |
| `OrderedPop` | Generic `Deheap[int].Pop`: remove minimum and bubble-down with direct `<`. |
| `OrderedPopMax` | Generic `Deheap[int].PopMax`: remove maximum and bubble-down with direct `<`. |
| `OrderedPushPop` | Generic push-then-drain throughput. |
| `Min4` | Cost of `min4`, the internal function that finds the extremum among up to 4 grandchildren during bubble-down. |
| `BaselinePush` | Raw slice append with no heap ordering — establishes the floor cost of memory allocation and copying. |
| `Push` | `deheap.Push` (v1): append + bubble-up via `heap.Interface`. |
| `Pop` | `deheap.Pop` (v1): remove the minimum element and bubble-down via `heap.Interface`. |
| `PopMax` | `deheap.PopMax` (v1): remove the maximum element and bubble-down via `heap.Interface`. |
| `PushPop` | v1 push-then-drain throughput. |
| `HeapPushPop` | Same push-then-drain pattern using `container/heap` for direct comparison. |
| `HeapPop` | `container/heap.Pop` in isolation, comparable to `Pop` above. |
| `HeapPush` | `container/heap.Push` in isolation, comparable to `Push` above. |
## Testing
The test suite includes 54 test functions covering internal helpers,
algorithmic correctness, edge cases (empty, single-element, two-element
heaps), and large-scale randomized validation. Six native Go fuzz targets
(`testing.F`) exercise both API surfaces under arbitrary input. Tests are run
against Go 1.21, 1.22, and 1.23 in CI.
```bash
go test ./... # run all tests
go test -bench . ./... # run benchmarks
go test -fuzz . # run fuzz tests
```
## Requirements
- Go 1.21 or later (generic API)
- Go 1.13 or later (interface API only)
- Zero external dependencies
## References
1. M.D. Atkinson, J.-R. Sack, N. Santoro, and T. Strothotte. "Min-Max
Heaps and Generalized Priority Queues." *Communications of the ACM*,
29(10):996–1000, October 1986.
https://doi.org/10.1145/6617.6621
2. J. van Leeuwen and D. Wood. "Interval Heaps." *The Computer Journal*,
36(3):209–216, 1993.
3. P. Brass. *Advanced Data Structures*. Cambridge University Press, 2008.
## License
MIT — see [LICENSE](LICENSE).