Skip to content

Commit 622a661

Browse files
committed
Initial commit
0 parents  commit 622a661

18 files changed

+1040
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.idea
2+
testdata/

.golangci.yml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
run:
2+
concurrency: 4
3+
timeout: 1m
4+
issue-exit-code: 1
5+
tests: false
6+
7+
output:
8+
format: colored-line-number # use 'golangci-lint run --out-format junit-xml' to have Jenkins-compatible results
9+
print-issued-lines: true
10+
print-linter-name: true
11+
uniq-by-line: true
12+
sort-results: true
13+
14+
linters:
15+
enable-all: false
16+
presets:
17+
- bugs
18+
- comment
19+
- error
20+
- format
21+
- import
22+
- performance
23+
- style
24+
- metalinter
25+
disable:
26+
- scopelint # deprecated
27+
- interfacer # deprecated
28+
- golint # deprecated, 'style' preset uses Revive
29+
- maligned # deprecated, replaced with govet fieldalignment
30+
- varnamelen
31+
- wsl
32+
- gomnd
33+
- ireturn
34+
- nlreturn
35+
36+
linters-settings:
37+
govet:
38+
check-shadowing: true
39+
enable:
40+
- asmdecl
41+
- assign
42+
- atomic
43+
- atomicalign
44+
- bools
45+
- buildtag
46+
- cgocall
47+
- composites
48+
- copylocks
49+
- deepequalerrors
50+
- errorsas
51+
- fieldalignment
52+
- findcall
53+
- framepointer
54+
- httpresponse
55+
- ifaceassert
56+
- loopclosure
57+
- lostcancel
58+
- nilfunc
59+
- nilness
60+
- printf
61+
- reflectvaluecompare
62+
- shadow
63+
- shift
64+
- sigchanyzer
65+
- sortslice
66+
- stdmethods
67+
- stringintconv
68+
- structtag
69+
- testinggoroutine
70+
- tests
71+
- unmarshal
72+
- unreachable
73+
- unsafeptr
74+
- unusedresult
75+
- unusedwrite
76+
tagliatelle:
77+
case:
78+
rules:
79+
json: snake
80+
yaml: snake

LICENCE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 lispad.me
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
test: fuzz
2+
go test -cover github.com/lispad/go-generics-tools/binheap
3+
4+
fuzz:
5+
go test -fuzz=FuzzEmptyMinHeap -fuzztime=10s github.com/lispad/go-generics-tools/binheap
6+
go test -fuzz=FuzzEmptyMaxHeap -fuzztime=10s github.com/lispad/go-generics-tools/binheap
7+
go test -fuzz=FuzzTopN$$ -fuzztime=10s github.com/lispad/go-generics-tools/binheap
8+
go test -fuzz=FuzzTopNImmutable -fuzztime=10s github.com/lispad/go-generics-tools/binheap
9+
10+
lint:
11+
golangci-lint run binheap

README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Heap structure, using go generics
2+
3+
Introduction
4+
------------
5+
6+
The Heap package contains simple [binary heap](https://en.wikipedia.org/wiki/Binary_heap) implementation, using Golang
7+
generics. There are several heap implementations
8+
9+
- generic Heap implementation, that could be used for `any` type,
10+
- `ComparableHeap` for [comparable](https://go.dev/ref/spec#Comparison_operators) types. Additional `Search`
11+
and `Delete` are implemented,
12+
- for [`constraints.Ordered`](https://pkg.go.dev/golang.org/x/exp/constraints#Ordered) there are syntax sugar
13+
constructors for min, max heaps;
14+
15+
Also use-cases provided:
16+
17+
- `TopN` that allows getting N top elements from slice.
18+
`TopN` swaps top N elements to first N elements of slice, no additional allocations are done. All slice elements are
19+
kept, only order is changed.
20+
- `TopNHeap` allows to get N top, pushing elements from stream without allocation slice for all elements. Only O(N)
21+
memory is used.
22+
- `TopNImmutable` allocated new slice for heap, input slice is not mutated.
23+
24+
Both TopN and TopNImmutable has methods for creating min and max tops for `constraints.Ordered`.
25+
26+
Usage Example
27+
-----------------
28+
29+
package main
30+
31+
import (
32+
"fmt"
33+
34+
"github.com/lispad/go-generics-heap/binheap"
35+
)
36+
37+
func main() {
38+
someData := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
39+
mins := binheap.MinN[float64](someData, 3)
40+
fmt.Printf("--- top 3 min elements: %v\n", mins)
41+
maxs := binheap.MaxN[float64](someData, 3)
42+
fmt.Printf("--- top 3 max elements: %v\n\n", maxs)
43+
44+
heap := binheap.EmptyMaxHeap[string]()
45+
heap.Push("foo")
46+
heap.Push("zzz")
47+
heap.Push("bar")
48+
heap.Push("baz")
49+
heap.Push("foobar")
50+
heap.Push("foobaz")
51+
fmt.Printf("--- heap has %d elements, max element:\n%s\n\n", heap.Len(), heap.Peak())
52+
}
53+
54+
A bit more examples could be found in `examples` directory
55+
56+
Benchmark
57+
-----------------
58+
Theoretical complexity for getting TopN from slice with size M, N <= M: O(N*ln(M)). When N << M, the heap-based TopN
59+
could be much faster than sorting slice and getting top. E.g. For top-3 from 10k elements approach is ln(10^5)/ln(3) ~=
60+
8.38 times faster.
61+
62+
#### Benchmark
63+
64+
BenchmarkSortedMaxN-8 10303648 136.0 ns/op 0 B/op 0 allocs/op
65+
BenchmarkMaxNImmutable-8 398996316 3.029 ns/op 0 B/op 0 allocs/op
66+
BenchmarkMaxN-8 804041455 1.819 ns/op 0 B/op 0 allocs/op
67+
68+
Compatibility
69+
-------------
70+
Minimal Golang version is 1.18. Generics and fuzz testing are used.
71+
72+
Installation
73+
----------------------
74+
75+
To install package, run:
76+
77+
go get github.com/lispad/go-generics-heap/binheap
78+
79+
License
80+
-------
81+
82+
The binheap package is licensed under the MIT license. Please see the LICENSE file for details.

binheap/generic.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Package binheap provides implementation of binary heap for any type
2+
// with use of golang generics, and top-N heap usecases.
3+
package binheap
4+
5+
// Heap provides basic methods: Push, Peak, Pop, Replace top element and PushPop.
6+
type Heap[T any] struct {
7+
comparator func(x, y T) bool
8+
data []T
9+
}
10+
11+
// EmptyHeap creates empty heap with provided comparator.
12+
func EmptyHeap[T any](comparator func(x, y T) bool) Heap[T] {
13+
return Heap[T]{
14+
comparator: comparator,
15+
}
16+
}
17+
18+
// FromSlice creates heap, based on provided slice.
19+
// Slice could be reordered.
20+
func FromSlice[T any](data []T, comparator func(x, y T) bool) Heap[T] {
21+
h := Heap[T]{
22+
data: data,
23+
comparator: comparator,
24+
}
25+
n := h.Len()
26+
for i := n/2 - 1; i >= 0; i-- {
27+
h.down(i, n)
28+
}
29+
30+
return h
31+
}
32+
33+
// Push inserts element to heap.
34+
func (h *Heap[T]) Push(x T) {
35+
h.data = append(h.data, x)
36+
h.up(h.Len() - 1)
37+
}
38+
39+
// Len returns count of elements in heap.
40+
func (h *Heap[T]) Len() int {
41+
return len(h.data)
42+
}
43+
44+
// Peak returns top element without deleting.
45+
func (h *Heap[T]) Peak() T {
46+
return h.data[0]
47+
}
48+
49+
// Pop returns top element with removing it.
50+
func (h *Heap[T]) Pop() T {
51+
n := h.Len() - 1
52+
h.swap(0, n)
53+
h.down(0, n)
54+
result := h.data[n]
55+
h.data = h.data[0:n]
56+
57+
return result
58+
}
59+
60+
// PushPop pushes x to the heap and then pops top element.
61+
func (h *Heap[T]) PushPop(x T) T {
62+
if h.Len() > 0 && h.comparator(h.data[0], x) {
63+
x, h.data[0] = h.data[0], x
64+
h.down(0, h.Len())
65+
}
66+
67+
return x
68+
}
69+
70+
// Replace extracts the root of the heap, and push a new item.
71+
func (h *Heap[T]) Replace(x T) (result T) {
72+
result, h.data[0] = h.data[0], x
73+
h.fix(0)
74+
return
75+
}
76+
77+
func (h *Heap[T]) fix(i int) (result T) {
78+
if !h.down(i, h.Len()) {
79+
h.up(i)
80+
}
81+
82+
return result
83+
}
84+
85+
func (h *Heap[T]) swap(i, j int) {
86+
h.data[i], h.data[j] = h.data[j], h.data[i]
87+
}
88+
89+
func (h *Heap[T]) up(j int) {
90+
for {
91+
i := (j - 1) / 2 // parent
92+
if i == j || !h.comparator(h.data[j], h.data[i]) {
93+
break
94+
}
95+
h.swap(i, j)
96+
j = i
97+
}
98+
}
99+
100+
func (h *Heap[T]) down(i0, n int) bool {
101+
i := i0
102+
for {
103+
j1 := 2*i + 1
104+
if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
105+
break
106+
}
107+
j := j1 // left child
108+
if j2 := j1 + 1; j2 < n && h.comparator(h.data[j2], h.data[j1]) {
109+
j = j2 // = 2*i + 2 // right child
110+
}
111+
if !h.comparator(h.data[j], h.data[i]) {
112+
break
113+
}
114+
h.swap(i, j)
115+
i = j
116+
}
117+
118+
return i > i0
119+
}

binheap/generic_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package binheap_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"github.com/lispad/go-generics-tools/binheap"
9+
)
10+
11+
func TestEmptyHeap(t *testing.T) {
12+
h := binheap.EmptyHeap[string](func(a, b string) bool { return len(a) > len(b) })
13+
assert.Equal(t, 0, h.Len())
14+
h.Push("1")
15+
assert.Equal(t, 1, h.Len())
16+
h.Push("22")
17+
assert.Equal(t, 2, h.Len())
18+
h.Push("4444")
19+
assert.Equal(t, 3, h.Len())
20+
h.Push("88888888")
21+
assert.Equal(t, 4, h.Len())
22+
h.Push("")
23+
assert.Equal(t, 5, h.Len())
24+
h.Push("55555")
25+
assert.Equal(t, 6, h.Len())
26+
h.Push("1")
27+
assert.Equal(t, 7, h.Len())
28+
h.Push("7777777")
29+
assert.Equal(t, 8, h.Len())
30+
h.Push("999999999")
31+
assert.Equal(t, 9, h.Len())
32+
h.Push("333")
33+
assert.Equal(t, 10, h.Len())
34+
35+
assert.Equal(t, "999999999", h.Peak())
36+
assert.Equal(t, 10, h.Len())
37+
assert.Equal(t, "999999999", h.PushPop("4444")) // push less than max value, max will be returned
38+
assert.Equal(t, 10, h.Len())
39+
assert.Equal(t, "88888888", h.Peak())
40+
assert.Equal(t, 10, h.Len())
41+
assert.Equal(t, "0000000000", h.PushPop("0000000000")) // value will be returned, heap is unchanged
42+
assert.Equal(t, 10, h.Len())
43+
assert.Equal(t, "88888888", h.Peak())
44+
assert.Equal(t, 10, h.Len())
45+
46+
assert.Equal(t, "88888888", h.Replace("22"))
47+
assert.Equal(t, 10, h.Len())
48+
assert.Equal(t, "7777777", h.Pop())
49+
assert.Equal(t, 9, h.Len())
50+
assert.Equal(t, "55555", h.Pop())
51+
assert.Equal(t, 8, h.Len())
52+
assert.Equal(t, "4444", h.Pop())
53+
assert.Equal(t, 7, h.Len())
54+
assert.Equal(t, "4444", h.Pop())
55+
assert.Equal(t, 6, h.Len())
56+
assert.Equal(t, "333", h.Pop())
57+
assert.Equal(t, 5, h.Len())
58+
h.Push("999999999")
59+
assert.Equal(t, 6, h.Len())
60+
assert.Equal(t, "999999999", h.Pop())
61+
assert.Equal(t, 5, h.Len())
62+
assert.Equal(t, "22", h.Pop())
63+
assert.Equal(t, 4, h.Len())
64+
assert.Equal(t, "22", h.Pop())
65+
assert.Equal(t, 3, h.Len())
66+
assert.Equal(t, "1", h.Pop())
67+
assert.Equal(t, 2, h.Len())
68+
assert.Equal(t, "1", h.Pop())
69+
assert.Equal(t, 1, h.Len())
70+
assert.Equal(t, "", h.Pop())
71+
assert.Equal(t, 0, h.Len())
72+
}

0 commit comments

Comments
 (0)