prioqueue

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2021 License: BSD-3-Clause Imports: 0 Imported by: 0

README

Go Priority Queue 🚥

An efficient min and max priority queue using a binary heap encoded in an array.


Package prioqueue implements an efficient min and max priority queue using a binary heap encoded in a slice.

Example usage

package prioqueue_test

import (
	"fmt"
	"math/rand"

	"github.com/fgrosse/prioqueue"
)

func Example() {
	// The queue will be backed by a slice and knowing its size ahead of time
	// avoids unnecessary allocations. If you don't know in advance how many
	// items you want to push then you can set n to 0 or a negative number
	// instead. In this case the slice used by the queue will start with the
	// default slice capacity of Go.
	n := 10

	// We use a random number generator in this example to generate values.
	rng := rand.New(rand.NewSource(42))

	q := prioqueue.NewMaxHeap(n)
	for i := 0; i < n; i++ {
		// Every element we push and pop from the queue must have a unique identifier.
		// It is the callers responsibility to ensure this uniqueness.
		id := uint32(i)
		prio := rng.Float32()
		q.Push(id, prio)
	}

	// The queue will always return the highest priority element first.
	for q.Len() > 0 {
		id, prio := q.Pop()
		fmt.Printf("%.2f (id %d)\n", prio, id)
	}

	// Output:
	// 0.81 (id 6)
	// 0.65 (id 9)
	// 0.60 (id 2)
	// 0.38 (id 7)
	// 0.38 (id 5)
	// 0.38 (id 8)
	// 0.37 (id 0)
	// 0.21 (id 3)
	// 0.07 (id 1)
	// 0.04 (id 4)
}

How it works

One way to implement a priority queue is by using a binary heap. Such a heap can be visualized as a binary tree in which each node's value is smaller than the values of its children ("Min Heap"). Alternatively the value can be greater than all of its children ("Max Heap") as long as this property is applied consistently upon the tree. For the remaining description we will assume a Min heap.

Now to get the smallest value we can get it in constant time from the root of the tree. To add a new value (aka "push") we append it to one of the leaves of the tree and then "bubble" it up by swapping the inserted node with its parent until the heap property is satisfied again. Since we have a binary tree all the time, the time complexity of insertion is logarithmic. Removing the minimum element (aka "pop") can also be done in logarithmic time. To do this we remove the root node and replace it with one of the leave nodes. We then swap the new root node down until the heap property is satisfied again (i.e. all nodes must have smaller values than their children).

Such a binary tree can be encoded into an array where the root node is the first element. We then write all children on the same level one after another to the array until it contains all n nodes of tree. With this schema, the parent of node i will be at index (i-1)/2, and the children of i are found at indices (2*i)+1 and (2*i)+2 respectively.

The nice thing about encoding this in an array is that it is very space efficient without sacrificing any speed. Especially in Go, we can leverage the concept of slices, for instance by pre allocating the underlying array and re-slicing it when pushing and popping element. That is also what the standard library is doing in the container/heap implementation. However, the implementation here appears to be slightly faster, probably because it is working on a concrete scalar data type directly (i.e. float32) instead of having to go through an interface.

Heap

Benchmarks

This package provides a couple of benchmarks to understand the performance of the implementation. In order to compare it with a baseline, there is also a benchmark which uses the container/heap package from the standard library.

There are three different benchmarks that test the performance of the standard library implementation (BenchmarkStdlib*) and this package (BenchmarkMaxHeap*).

The *Push1* benchmarks pushing a single element onto the queue and with each iteration of this benchmark the queue is growing. The *Push200* benchmarks test how long it takes to push exactly 200 elements. Having a benchmark with a fixed amount of elements to push (and thus a fixed size for the resulting queue) is useful because it shows performance of a smaller queue (i.e. only 200 elements). In contrast, the *Push1* benchmarks show performance with queues which contain millions of elements. The *Empty vs *Preallocate variants test performance when the corresponding queue is not sized in advance (i.e. "Empty") vs allocating the memory for the queue in advance (i.e. "Preallocate").

Finally, the *Pop200* benchmarks test how long it takes to pop all elements from a queue which contains out of 200 elements.

$ go test -bench .
goos: linux
goarch: amd64
pkg: github.com/fgrosse/prioqueue
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkMaxHeap_Push1_Empty-8               11827210     93.54 ns/op       52 B/op       1 allocs/op
BenchmarkMaxHeap_Push1_Preallocate-8         30413618     37.83 ns/op        8 B/op       1 allocs/op
BenchmarkMaxHeap_Push200_Empty-8               140348      8185 ns/op     5688 B/op     209 allocs/op
BenchmarkMaxHeap_Push200_Preallocate-8         223698      5389 ns/op     1600 B/op     200 allocs/op
BenchmarkMaxHeap_Pop200-8                      193807      6115 ns/op        0 B/op       0 allocs/op

BenchmarkStdlib_Push1_Empty-8                10242606     98.41 ns/op       49 B/op       1 allocs/op
BenchmarkStdlib_Push1_Preallocate-8          21231261     50.39 ns/op        8 B/op       1 allocs/op
BenchmarkStdlib_Push200_Empty-8                 97664     12617 ns/op     5688 B/op     209 allocs/op
BenchmarkStdlib_Push200_Preallocate-8          117884      9834 ns/op     1600 B/op     200 allocs/op
BenchmarkStdlibHeap_Pop200-8                    59112     20256 ns/op        0 B/op       0 allocs/op
PASS
ok      github.com/fgrosse/prioqueue    30.705s

Documentation

Overview

Package prioqueue implements an efficient min and max priority queue using a binary heap encoded in a slice.

Example
package main

import (
	"fmt"
	"math/rand"

	"github.com/fgrosse/prioqueue"
)

func main() {
	// The queue will be backed by a slice and knowing its size ahead of time
	// avoids unnecessary allocations. If you don't know in advance how many
	// items you want to push then you can set n to 0 or a negative number
	// instead. In this case the slice used by the queue will start with the
	// default slice capacity of Go.
	n := 10

	// We use a random number generator in this example to generate values.
	rng := rand.New(rand.NewSource(42))

	q := prioqueue.NewMaxHeap(n)
	for i := 0; i < n; i++ {
		// Every element we push and pop from the queue must have a unique identifier.
		// It is the callers responsibility to ensure this uniqueness.
		id := uint32(i)
		prio := rng.Float32()
		q.Push(id, prio)
	}

	// The queue will always return the highest priority element first.
	for q.Len() > 0 {
		id, prio := q.Pop()
		fmt.Printf("%.2f (id %d)\n", prio, id)
	}

}
Output:

0.81 (id 6)
0.65 (id 9)
0.60 (id 2)
0.38 (id 7)
0.38 (id 5)
0.38 (id 8)
0.37 (id 0)
0.21 (id 3)
0.07 (id 1)
0.04 (id 4)

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Item

type Item struct {
	ID   uint32
	Prio float32
}

Item is an element in a priority queue.

type MaxHeap

type MaxHeap struct {
	// contains filtered or unexported fields
}

MaxHeap implements a priority queue which allows to retrieve the highest priority element using a heap. Since the heap is maintained in form of a binary tree, it can efficiently be represented in the form of a list.

The priority queue has the following properties:

  • items with high priority are dequeued before elements with lower priority
  • the item at the root of the tree is the maximum among all items present in the binary heap. The same property is recursively true for all nodes in the tree.

Array representation

The first element of the list is always the root node (R) of the binary tree. The two children of (R) are the next two elements in the list (A) & (B). (A) and (B) are immediately followed by the children of (A) and then the children of (B). This process continues for all nodes of the binary tree. Generally speaking, the parent of index i is at index (i-1)/2. The two children of index i are at (2*i)+1 and (2*i)+2.

Time Complexity

Push and Pop take O(log n) and Top() happens in constant time.

func NewMaxHeap

func NewMaxHeap(size int) *MaxHeap

NewMaxHeap returns a new MaxHeap instance which contains a pre-allocated backing array for the stored items. Usage of this function or setting a correct size is optional. If more items are inserted into the queue than there is space in the backing array, it grows automatically.

If you do not know the size in advance you can set the argument to 0 or a negative value.

func (*MaxHeap) Items added in v0.4.0

func (h *MaxHeap) Items() []*Item

Items returns all elements that are currently in the queue. The caller should ignore the order in which elements are returned since this only reflects how the queue stores its items internally.

func (*MaxHeap) Len

func (h *MaxHeap) Len() int

Len returns the amount of elements in the queue.

func (*MaxHeap) Pop

func (h *MaxHeap) Pop() (id uint32, priority float32)

Pop removes the item with the highest priority value from the queue and returns its ID and priority.

Note that while popping an element from the heap will also remove it from the queue but it will not release the memory in the backing array as long as the heap is still in use. See https://blog.golang.org/slices-intro#TOC_6.

func (*MaxHeap) PopAndPush added in v0.3.0

func (h *MaxHeap) PopAndPush(item *Item)

PopAndPush removes the item with the highest priority value and adds a new value to the heap in one operation. This is faster than two separate calls to Pop and Push.

func (*MaxHeap) PopItem

func (h *MaxHeap) PopItem() *Item

PopItem removes the item with the highest priority value from the queue.

func (*MaxHeap) Push

func (h *MaxHeap) Push(id uint32, prio float32)

Push the value item into the priority queue with provided priority.

func (*MaxHeap) PushItem

func (h *MaxHeap) PushItem(item *Item)

PushItem adds an Item to the queue.

func (*MaxHeap) Reset

func (h *MaxHeap) Reset()

Reset is a fast way to empty the queue. Note that the underlying array will still be used by the heap which means that this function will not free up any memory. If you need to release memory, you have to create a new instance and let this one be taken care of by the garbage collection.

func (*MaxHeap) Top

func (h *MaxHeap) Top() (uint32, float32)

Top returns the ID and priority of the item with the highest priority value in the queue without removing it.

func (*MaxHeap) TopItem

func (h *MaxHeap) TopItem() *Item

TopItem returns the item with the highest priority value in the queue without removing it.

type MinHeap

type MinHeap struct {
	// contains filtered or unexported fields
}

MinHeap implements a priority queue which allows to retrieve the lowest priority element using a heap. Since the heap is maintained in form of a binary tree, it can efficiently be represented in the form of a list.

The priority queue has the following properties:

  • items with low priority are dequeued before elements with higher priority
  • the item at the root of the tree is the minimum among all items present in the binary heap. The same property is recursively true for all nodes in the tree.

Array representation

The first element of the list is always the root node (R) of the binary tree. The two children of (R) are the next two elements in the list (A) & (B). (A) and (B) are immediately followed by the children of (A) and then the children of (B). This process continues for all nodes of the binary tree. Generally speaking, the parent of index i is at index (i-1)/2. The two children of index i are at (2*i)+1 and (2*i)+2.

Time Complexity

Push and Pop take O(log n) and Top() happens in constant time.

func NewMinHeap

func NewMinHeap(size int) *MinHeap

NewMinHeap returns a new MinHeap instance which contains a pre-allocated backing array for the stored items. Usage of this function or setting a correct size is optional. If more items are inserted into the queue than there is space in the backing array, it grows automatically.

If you do not know the size in advance you can set the argument to 0 or a negative value.

func (*MinHeap) Items added in v0.4.0

func (h *MinHeap) Items() []*Item

Items returns all elements that are currently in the queue. The caller should ignore the order in which elements are returned since this only reflects how the queue stores its items internally.

func (*MinHeap) Len

func (h *MinHeap) Len() int

Len returns the amount of elements in the queue.

func (*MinHeap) Pop

func (h *MinHeap) Pop() (id uint32, priority float32)

Pop removes the item with the lowest priority value from the queue and returns its ID and priority.

Note that while popping an element from the heap will also remove it from the queue but it will not release the memory in the backing array as long as the heap is still in use. See https://blog.golang.org/slices-intro#TOC_6.

func (*MinHeap) PopAndPush added in v0.3.0

func (h *MinHeap) PopAndPush(item *Item)

PopAndPush removes the item with the lowest priority value and adds a new value to the heap in one operation. This is faster than two separate calls to Pop and Push.

func (*MinHeap) PopItem

func (h *MinHeap) PopItem() *Item

PopItem removes the item with the lowest priority value from the queue.

func (*MinHeap) Push

func (h *MinHeap) Push(id uint32, priority float32)

Push the value item into the priority queue with provided priority.

func (*MinHeap) PushItem

func (h *MinHeap) PushItem(item *Item)

PushItem adds an Item to the queue.

func (*MinHeap) Reset

func (h *MinHeap) Reset()

Reset is a fast way to empty the queue. Note that the underlying array will still be used by the heap which means that this function will not free up any memory. If you need to release memory, you have to create a new instance and let this one be taken care of by the garbage collection.

func (*MinHeap) Top

func (h *MinHeap) Top() (id uint32, prio float32)

Top returns the ID and priority of the item with the lowest priority value in the queue without removing it.

func (*MinHeap) TopItem

func (h *MinHeap) TopItem() *Item

TopItem returns the item with the lowest priority value in the queue without removing it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL