In this post we will learn about heap data structures, what they are used for, and how to implement them in Go using the container/heap standard library

banner

If you just want to see the example code, you can view it on Github or run the live example on the Go playground

What Is a Heap Data Structure?

A heap is a tree-based data structure with a fixed relationship between each parent node and its child nodes.

For max heaps, this means that the value of any parent node must be greater than or equal to its child node. This means that the root node will always have the maximum value.

max heap example

Min heaps have the reverse property, an so the root node of a min heap will always have the least value.

Why Do We Use Heaps?

Normally, we use heaps by “heapifying” a set of values - which means converting a list of items into a heap:

heapification process

Some of the key properties which make heaps useful are:

  1. We can find the maximum or minimum value of a given set of “n” values in O(1) time. If we use an array based list, this would normally take O(n) time.
  2. Adding and removing values takes O(log(n)) time, while preserving the max or min heap property. This is used when sorting a list of items using heap sort, where each element is inserted into a heap one at a time.

Using the Heap Interface in Go

We can convert a regular list of items into a heap by implementing the heap interface for that type.

The heap interface has five methods and once we implement them we can use the functions provided by the container/heap library.

For example let’s see how we can implement the heap interface for a list of integers:

// we need to define a custom type instead of using the raw integer slice 
// since we need to define methods on the type to implement the heap interface
type intHeap []int

// Len is the number of elements in the collection.
func (h intHeap) Len() int {
	return len(h)
}

// Less reports whether the element with index i
// must sort before the element with index j.
// This will determine whether the heap is a min heap or a max heap
func (h intHeap) Less(i int, j int) bool {
	return h[i] < h[j]
}

// Swap swaps the elements with indexes i and j.
func (h intHeap) Swap(i int, j int) {
	h[i], h[j] = h[j], h[i]
}

// Push and Pop are used to append and remove the last element of the slice
func (h *intHeap) Push(x any) {
	*h = append(*h, x.(int))
}

func (h *intHeap) Pop() any {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

Now that our intHeap type implements the heap.Interface interface, we can use the functions provided by the container/heap package to perform various operations.

For example, let’s start by heapifying an intHeap instance:

package main

import (
	"container/heap"
	"fmt"
)

func main() {
  // create a new intHeap instance
	nums := &intHeap{3, 1, 4, 5, 1, 1, 2, 6}
  // The `Init` function reorders the numbers into a heap
	heap.Init(nums)
  // The slice is now reordered to conform to the heap property
	fmt.Println(nums)
}

Output:

&[1 1 1 5 3 4 2 6]

Note that the number is your are not necessarily in order. Rather they are arranged as a heap data structure which would look something like this:

example heap

Adding and Removing Elements From the Heap

We can use the heap.Pop function to get the current minimum (or maximum, for a max heap) element from a heap. This element is also removed from the heap, and the heap is restructured to maintain the heap property.

For example, if we want to remove the minimum element from nums:

min := heap.Pop(nums)
fmt.Println("min: ", min, " heap: ", *nums)

Output:

min:  1  heap:  [1 3 1 5 6 4 2]

We can see that the heap is now restructured with the minimum value (1) removed:

heap structure popped

To add an element to the heap we can use the heap.Push function along with the value of the element that we want to add:

heap.Push(nums, 5)
fmt.Println("heap: ", *nums)

Output:

heap:  [1 3 1 5 6 4 2 5]

heap structure pushed

Note that both adding and removing an element take place in O(log(n)) time, so using a heap is a good way to maintain a priority queue of elements that can be added and removed efficiently.

Heap Sort

Since the pop function always returns the minimum element, we can continuously call the pop function for all elements in the heap to give us a sorted list of numbers:

sortedNums := []int{}
for nums.Len() > 0 {
  sortedNums = append(sortedNums, heap.Pop(nums).(int))
}
fmt.Println("sorted values: ", sortedNums)

Output:

sorted values:  [1 1 2 3 4 5 5 6]

The sort operation has to pop each item for all items, so the overall time complexity is O(n*log(n)).

Creating a Heap with Custom Struct Types

So far, we used an int heap, but we can create heaps from lists of any type, as long as we know the ordering property we want.

For example, consider a list of structs with a string field and a date field:

// Each holiday has a name and a date
type Holiday struct {
	name string
	date time.Time
}

// for convenience, we can create a string representation to print the holiday instance
func (h Holiday) String() string {
	return "[" + h.name + ", " + h.date.Format("Jan 2") + "]"
}

// We can create a new type to represent a list of holidays
type Holidays []Holiday

Now there are two ways we can sort a list of Holiday instances:

  1. By their name, in alphabetical order
  2. By the calendar date

First, let’s implement the heap interface for the Holidays type to order by calendar date:

// Most of the changes here would be in the `Less` method
func (h Holidays) Less(i, j int) bool {
  // Here, we choose to order by comparing the calendar dates of
  // two holiday instances
	return h[i].date.Before(h[j].date)
}

// The remaining methods are more or less the same as the previous examples
func (h Holidays) Len() int { return len(h) }

func (h Holidays) Swap(i, j int) { h[i], h[j] = h[j], h[i] }

func (h *Holidays) Push(x interface{}) {
	*h = append(*h, x.(Holiday))
}

func (h *Holidays) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

Now we can use the heap methods to create our Holidays instance:

holidays := &Holidays{}
heap.Init(holidays)
heap.Push(holidays, Holiday{name: "Christmas", date: time.Date(2023, time.December, 25, 0, 0, 0, 0, time.Local)})
heap.Push(holidays, Holiday{name: "Labour Day", date: time.Date(2023, time.May, 1, 0, 0, 0, 0, time.Local)})
heap.Push(holidays, Holiday{name: "Diwali", date: time.Date(2023, time.November, 23, 0, 0, 0, 0, time.Local)})

fmt.Println("holidays: ", holidays)

Output:

holidays:  &[[Labour Day, May 1] [Christmas, Dec 25] [Diwali, Nov 23]]

holiday heap date

If we want to now order our holidays alphabetically, we need to change the Less method to use the name field for comparison:

// this will return true if holiday i is before holiday j in the dictionary
func (h Holidays) Less(i, j int) bool {
	return strings.Compare(h[i].name, h[j].name) < 0
}

If we run the same heap example, our new output will be:

holidays:  &[[Christmas, Dec 25] [Labour Day, May 1] [Diwali, Nov 23]]

holiday heap name

If you want to see the code for all the examples in this post, you can view it on Github or run the live example on the Go playground