From b01cbddfdf266be6ae52f48f8790996ebd721403 Mon Sep 17 00:00:00 2001 From: Danny Whalen Date: Sat, 4 Jun 2022 16:11:10 -0700 Subject: [PATCH] Add v2 with generics (#2) * Add version 2 with generics * Add another benchmark inspired by the original * Remove New in favor of variable initialization * Add golang version constraint in readme * Update README example --- .github/workflows/go.yml | 7 +- README.md | 49 ++++--- README_v1.md | 37 ++++++ go.sum | 0 v2/flatqueue.go | 92 +++++++++++++ v2/flatqueue_test.go | 271 +++++++++++++++++++++++++++++++++++++++ v2/go.mod | 5 + v2/go.sum | 2 + 8 files changed, 441 insertions(+), 22 deletions(-) create mode 100644 README_v1.md create mode 100644 go.sum create mode 100644 v2/flatqueue.go create mode 100644 v2/flatqueue_test.go create mode 100644 v2/go.mod create mode 100644 v2/go.sum diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5824ae0..60c9712 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -9,6 +9,11 @@ jobs: name: Run tests runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v3 + with: + go-version: '>=1.18.0' - uses: actions/checkout@v2 - run: go test -v -bench . + - run: | + cd v2 + go test -v -bench . diff --git a/README.md b/README.md index edd7a3a..5e91271 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,44 @@ ## flatqueue-go [![Tests](https://github.com/invisiblefunnel/flatqueue-go/actions/workflows/go.yml/badge.svg)](https://github.com/invisiblefunnel/flatqueue-go/actions/workflows/go.yml) -A Go port of the [flatqueue](https://github.com/mourner/flatqueue) priority queue library. Push items by identifier (`int`) and value (`float64`). +A Go 1.18+ port of the [flatqueue](https://github.com/mourner/flatqueue) priority queue library using generics. `Peek`, `PeekValue`, and `Pop` will panic if called on an empty queue. You must check `Len` accordingly. ```go -q := flatqueue.New() +package main -for i, item := range items { - q.Push(i, item.Value) +import "github.com/invisiblefunnel/flatqueue-go/v2" + +type Item struct { + Name string + Value int } -var ( - id int - value float64 -) +func main() { + items := []Item{ + {"X", 5}, + {"Y", 2}, + {"Z", 3}, + } -id = q.Peek() // top item index -value = q.PeekValue() // top item value -id = q.Pop() // remove and return the top item index + var q flatqueue.FlatQueue[Item, int] -// loop while queue is not empty -for q.Len() > 0 { - q.Pop() -} -``` + for _, item := range items { + q.Push(item, item.Value) + } -Specifying an initial capacity for the underlying slices may improve the performance of `Push` if you know, or can estimate, the maximum length of the queue. This does not limit the length of the queue. + var ( + item Item + value int + ) -```go -q := flatqueue.NewWithCapacity(len(items)) + item = q.Peek() // top item + value = q.PeekValue() // top item value + item = q.Pop() // remove and return the top item -for i, item := range items { - q.Push(i, item.Value) + // loop while the queue is not empty + for q.Len() > 0 { + q.Pop() + } } ``` diff --git a/README_v1.md b/README_v1.md new file mode 100644 index 0000000..edd7a3a --- /dev/null +++ b/README_v1.md @@ -0,0 +1,37 @@ +## flatqueue-go [![Tests](https://github.com/invisiblefunnel/flatqueue-go/actions/workflows/go.yml/badge.svg)](https://github.com/invisiblefunnel/flatqueue-go/actions/workflows/go.yml) + +A Go port of the [flatqueue](https://github.com/mourner/flatqueue) priority queue library. Push items by identifier (`int`) and value (`float64`). + +`Peek`, `PeekValue`, and `Pop` will panic if called on an empty queue. You must check `Len` accordingly. + +```go +q := flatqueue.New() + +for i, item := range items { + q.Push(i, item.Value) +} + +var ( + id int + value float64 +) + +id = q.Peek() // top item index +value = q.PeekValue() // top item value +id = q.Pop() // remove and return the top item index + +// loop while queue is not empty +for q.Len() > 0 { + q.Pop() +} +``` + +Specifying an initial capacity for the underlying slices may improve the performance of `Push` if you know, or can estimate, the maximum length of the queue. This does not limit the length of the queue. + +```go +q := flatqueue.NewWithCapacity(len(items)) + +for i, item := range items { + q.Push(i, item.Value) +} +``` diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/v2/flatqueue.go b/v2/flatqueue.go new file mode 100644 index 0000000..cf1e2c8 --- /dev/null +++ b/v2/flatqueue.go @@ -0,0 +1,92 @@ +package flatqueue + +import ( + "golang.org/x/exp/constraints" +) + +type FlatQueue[T any, V constraints.Ordered] struct { + items []T + values []V + length int +} + +func (q *FlatQueue[_, _]) Len() int { + return q.length +} + +func (q *FlatQueue[T, V]) Push(item T, value V) { + pos := q.length + q.length++ + + q.items = append(q.items, item) + q.values = append(q.values, value) + + for pos > 0 { + parent := (pos - 1) >> 1 + parentValue := q.values[parent] + + if value > parentValue { + break + } + + q.items[pos] = q.items[parent] + q.values[pos] = parentValue + + pos = parent + } + + q.items[pos] = item + q.values[pos] = value +} + +func (q *FlatQueue[T, _]) Pop() T { + top := q.items[0] + q.length-- + + if q.length > 0 { + id := q.items[q.length] + value := q.values[q.length] + + q.items[0] = id + q.values[0] = value + + halfLength := q.length >> 1 + pos := 0 + + for pos < halfLength { + left := (pos << 1) + 1 + right := left + 1 + + bestIndex := q.items[left] + bestValue := q.values[left] + rightValue := q.values[right] + + if right < q.length && rightValue < bestValue { + left = right + bestIndex = q.items[right] + bestValue = rightValue + } + + if bestValue > value { + break + } + + q.items[pos] = bestIndex + q.values[pos] = bestValue + pos = left + } + + q.items[pos] = id + q.values[pos] = value + } + + return top +} + +func (q *FlatQueue[T, _]) Peek() T { + return q.items[0] +} + +func (q *FlatQueue[_, V]) PeekValue() V { + return q.values[0] +} diff --git a/v2/flatqueue_test.go b/v2/flatqueue_test.go new file mode 100644 index 0000000..7230353 --- /dev/null +++ b/v2/flatqueue_test.go @@ -0,0 +1,271 @@ +package flatqueue + +import ( + "math/rand" + "reflect" + "sort" + "testing" +) + +const ( + benchN int = 100_000 + benchK int = benchN / 1_000 +) + +func TestMaintainsPriorityQueue(t *testing.T) { + n := 10000 + var q FlatQueue[int, float64] + + data := make([]float64, n) + sorted := make([]float64, n) + for i := 0; i < n; i++ { + data[i] = rand.Float64() + sorted[i] = data[i] + } + + sort.Float64s(sorted) + + for i, v := range data { + q.Push(i, v) + } + + if q.PeekValue() != sorted[0] { + t.Fatal() + } + + if data[q.Peek()] != sorted[0] { + t.Fatal() + } + + result := make([]float64, n) + i := 0 + for q.Len() > 0 { + result[i] = data[q.Pop()] + i++ + } + + if !reflect.DeepEqual(sorted, result) { + t.Fatal() + } +} + +func TestLen(t *testing.T) { + var q FlatQueue[int, float64] + + if q.Len() != 0 { + t.Fatal() + } + + if q.Push(0, 0); q.Len() != 1 { + t.Fatal() + } + + if q.Push(1, 1); q.Len() != 2 { + t.Fatal() + } + + if q.Pop(); q.Len() != 1 { + t.Fatal() + } + + if q.Pop(); q.Len() != 0 { + t.Fatal() + } +} + +func TestPop(t *testing.T) { + var q FlatQueue[int, float64] + + q.Push(1, 10) + q.Push(2, 11) + + if q.Pop() != 1 { + t.Fatal() + } + + if q.Pop() != 2 { + t.Fatal() + } +} + +func TestPeek(t *testing.T) { + var q FlatQueue[int, float64] + + q.Push(1, 10) + + if q.Peek() != 1 { + t.Fatal() + } + + q.Push(2, 11) + + if q.Peek() != 1 { + t.Fatal() + } + + q.Push(3, 9) + + if q.Peek() != 3 { + t.Fatal() + } + + q.Pop() + + if q.Peek() != 1 { + t.Fatal() + } + + q.Pop() + q.Pop() +} + +func TestPeekValue(t *testing.T) { + var q FlatQueue[int, float64] + + q.Push(1, 10) + + if q.PeekValue() != float64(10) { + t.Fatal() + } + + q.Push(2, 11) + + if q.PeekValue() != float64(10) { + t.Fatal() + } + + q.Push(3, 9) + + if q.PeekValue() != float64(9) { + t.Fatal() + } + + q.Pop() + + if q.PeekValue() != float64(10) { + t.Fatal() + } + + q.Pop() + q.Pop() +} + +func TestEdgeCasesWithFewElements(t *testing.T) { + var q FlatQueue[int, float64] + + q.Push(0, 2) + q.Push(1, 1) + q.Pop() + q.Pop() + q.Push(2, 2) + q.Push(3, 1) + + if q.Pop() != 3 { + t.Fatal() + } + + if q.Pop() != 2 { + t.Fatal() + } +} + +func TestPeekEmpty(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal() + } + }() + var q FlatQueue[int, float64] + q.Peek() +} + +func TestPeekValueEmpty(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal() + } + }() + var q FlatQueue[int, float64] + q.PeekValue() +} + +func TestPopEmpty(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal() + } + }() + var q FlatQueue[int, float64] + q.Pop() +} + +func BenchmarkPush(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + b.StopTimer() + + var q FlatQueue[int, float64] + + values := make([]float64, benchN) + for j := 0; j < benchN; j++ { + values[j] = rand.Float64() + } + + b.StartTimer() + + // Fill the queue + for j, value := range values { + q.Push(j, value) + } + } +} + +func BenchmarkPop(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + b.StopTimer() + + var q FlatQueue[int, float64] + for j := 0; j < benchN; j++ { + q.Push(j, rand.Float64()) + } + + b.StartTimer() + + // Empty the queue + for q.Len() > 0 { + q.Pop() + } + } +} + +func BenchmarkPushPop(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + b.StopTimer() + + var q FlatQueue[int, float64] + + values := make([]float64, benchN) + for i := 0; i < benchN; i++ { + values[i] = rand.Float64() + } + + b.StartTimer() + + for j := 0; j < benchN; j += benchK { + for k := 0; k < benchK; k++ { + q.Push(j+k, values[j+k]) + } + for k := 0; k < benchK; k++ { + q.Pop() + } + } + } +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..599a444 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,5 @@ +module github.com/invisiblefunnel/flatqueue-go/v2 + +go 1.18 + +require golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..caddb82 --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f h1:KK6mxegmt5hGJRcAnEDjSNLxIRhZxDcgwMbcO/lMCRM= +golang.org/x/exp v0.0.0-20220602145555-4a0574d9293f/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys=