Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go: [ '1.15', 'stable' ]
go: [ '1.18', 'stable' ]
name: Tests on Go ${{ matrix.go }}
steps:
- name: Checkout Repo
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.16-alpine3.13 AS build-go
FROM golang:1.18-alpine AS build-go

ARG GIT_SSH_KEY
ARG KNOWN_HOSTS_CONTENT
Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ in that package.

A standard Fibonacci heap providing the usual operations. Can be useful in executing Dijkstra or Prim's algorithms in the theoretically minimal time. Also useful as a general-purpose priority queue. The special thing about Fibonacci heaps versus other heap variants is the cheap decrease-key operation. This heap has a constant complexity for find minimum, insert and merge of two heaps, an amortized constant complexity for decrease key and O(log(n)) complexity for a deletion or dequeue minimum. In practice the constant factors are large, so Fibonacci heaps could be slower than Pairing heaps, depending on usage. Benchmarks - in the project subfolder. The heap has not been designed for thread-safety.

#### Binary and D-ary Heaps

Generic, comparator-based heaps implemented with Go 1.18 generics.

- NewHeap[T](compare func(T, T) int) *Heap[T]
- NewDaryHeap[T](d int, compare func(T, T) int) *DaryHeap[T]
- NewHeapFromSlice[T](values []T, compare func(T, T) int) *Heap[T]
- NewDaryHeapFromSlice[T](d int, values []T, compare func(T, T) int) *DaryHeap[T]
- (h *Heap[T]) Peek() (value T, ok bool)
- (h *DaryHeap[T]) Peek() (value T, ok bool)
- (h *Heap[T]) Pop() (value T, ok bool)
- (h *DaryHeap[T]) Pop() (value T, ok bool)

Comparator contract: compare(a, b) should return -1 if a < b, 0 if a == b, and 1 if a > b. If the comparator orders values in ascending order, the heap behaves as a min-heap. To build a max-heap, invert the comparator (e.g., return -compare(a, b)).

Goroutine-safety: Both heaps are safe for concurrent use by multiple goroutines. Internally they use sync.RWMutex to guard state. The comparator must be pure/non-blocking and must not call back into the heap (reentrant use would deadlock).

Zero allocations: Push/Pop/Peek perform 0 allocations in steady state because the storage is a pre-allocated slice grown by append, and operations only mutate indices and swap elements in-place. Benchmarks use testing.B.ReportAllocs to validate 0 allocs/op.

The D-ary heap is a generalization of the binary heap using a branching factor d. Indexing uses parent=(i-1)/d and children in [d*i+1, d*i+d].

#### Range Tree

Useful to determine if n-dimensional points fall within an n-dimensional range.
Expand Down Expand Up @@ -149,7 +170,7 @@ interface and the most expensive operation in CPU profiling is the interface
method which in turn calls into runtime.assertI2T. We need generics.

#### Immutable B Tree
A btree based on two principles, immutability and concurrency.
A btree based on two principles, immutability and concurrency.
Somewhat slow for single value lookups and puts, it is very fast for bulk operations.
A persister can be injected to make this index persistent.

Expand Down Expand Up @@ -185,8 +206,8 @@ operations are O(n) as you would expect.

#### Simple Graph

A mutable, non-persistent undirected graph where parallel edges and self-loops are
not permitted. Operations to add an edge as well as retrieve the total number of
A mutable, non-persistent undirected graph where parallel edges and self-loops are
not permitted. Operations to add an edge as well as retrieve the total number of
vertices/edges are O(1) while the operation to retrieve the vertices adjacent to a
target is O(n). For more details see [wikipedia](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)#Simple_graph)

Expand Down
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
module github.com/Workiva/go-datastructures

go 1.15
go 1.18

require (
github.com/stretchr/testify v1.7.0
github.com/tinylib/msgp v1.1.5
)

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
130 changes: 130 additions & 0 deletions heap/binary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
MIT License

Copyright (c) 2021 Florimond Husquinet

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/* A generic implementation of a binary heap */
package heap

import "sync"

type Heap[T any] struct {
mu sync.RWMutex
data []T
compare func(T, T) int
}

// NewHeap constructs a heap using the provided comparator.
// The comparator should return -1 if a < b, 0 if a == b, and 1 if a > b.
// If compare orders values in ascending order, the heap behaves as a min-heap.
// To build a max-heap, invert the comparator (e.g., return -compare(a, b)).
func NewHeap[T any](compare func(T, T) int) *Heap[T] {
return &Heap[T]{
data: make([]T, 0),
compare: compare,
}
}

// NewHeapFromSlice builds a heap in O(n) from an initial slice.
func NewHeapFromSlice[T any](values []T, compare func(T, T) int) *Heap[T] {
h := &Heap[T]{
data: append([]T(nil), values...),
compare: compare,
}
// heapify bottom-up
for i := (len(h.data) / 2) - 1; i >= 0; i-- {
h.sinkDown(i)
}
return h
}

// Peek returns the top element without removing it.
func (h *Heap[T]) Peek() (value T, ok bool) {
h.mu.RLock()
defer h.mu.RUnlock()
if len(h.data) == 0 {
return value, false
}
return h.data[0], true
}

func (h *Heap[T]) Len() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.data)
}

func (h *Heap[T]) Push(value T) {
h.mu.Lock()
defer h.mu.Unlock()
h.data = append(h.data, value)
idx := len(h.data) - 1
h.bubbleUp(idx)
}

func (h *Heap[T]) Pop() (value T, ok bool) {
h.mu.Lock()
defer h.mu.Unlock()
n := len(h.data)
if n == 0 {
return value, false
}
top := h.data[0]
h.data[0] = h.data[n-1]
h.data = h.data[:n-1]
h.sinkDown(0)
return top, true
}

// Min heap: if a node is less than its parent, swap them.
func (h *Heap[T]) bubbleUp(index int) {
if index == 0 {
return
}
var parent = (index - 1) / 2
if h.compare(h.data[index], h.data[parent]) < 0 {
h.swap(index, parent)
h.bubbleUp(parent)
}
}

// Min heap: if a node is greater than its children, swap the node with the smallest child.
func (h *Heap[T]) sinkDown(index int) {
n := len(h.data)
left := index*2 + 1
right := index*2 + 2
smallest := index
if left < n && h.compare(h.data[left], h.data[smallest]) < 0 {
smallest = left
}
if right < n && h.compare(h.data[right], h.data[smallest]) < 0 {
smallest = right
}
if smallest != index {
h.swap(index, smallest)
h.sinkDown(smallest)
}
}

func (h *Heap[T]) swap(i, j int) {
h.data[i], h.data[j] = h.data[j], h.data[i]
}
Loading