View Source PairingHeap (pairing_heap v0.1.0)

Elixir implementation of a pairing heap.

Background

A heap is a tree data structure that efficiently maintains the order of its contents. The heap was first introduced in the context of efficient sorting (see heapsort), but it can also be used to implement a priority queue.

Each node of a heap has a key on which the data is ordered, as well as a collection of child nodes. Every heap node satisfies the heap property that the node key is either less than or equal to (a min-heap) or greater than or equal to (a max-heap) the keys of its children. In a min-heap, the root node has the smallest key in the tree, while for a max-heap, the root key is the largest.

A pairing heap is one of a handful of commonly-used heap data structures. It has excellent performance characteristics, with O(1) time complexity for an insert and for viewing the root node, and O(log n) amortized time complexity for removing the root node and rebuilding the heap. The algorithms for maintaining a pairing heap have especially simple implementations in a functional language, which is one reason for doing this in Elixir.

Usage

PairingHeap provides operations for the creation of a heap, for insertion, retrieval, and deletion of elements, and for merging multiple heaps.

To create an empty min-heap that compares keys with &<=/2, use PairingHeap.new/1:

iex> PairingHeap.new(:min)
#PairingHeap<root: :empty, size: 0, mode: :min>

An empty heap is indicated with root: :empty.

The PairingHeap struct is not intended to be used directly. Use the public API when working with an instance of PairingHeap.

The data contained in each node of a heap is a key-item pair expressed as the tuple {key, item}. A key is what is being ordered in the heap, while the item can be data of any type. Note that a heap is a not a set; duplicate keys, items, and key-items pairs can be present.

Multiple key-item pairs can be added to a heap at the time of creation with PairingHeap.new/2:

iex> PairingHeap.new(:min, [{2, :b}, {1, :a}])
#PairingHeap<root: {1, :a}, size: 2, mode: :min>

The first argument to PairingHeap.new/1 and PairingHeap.new/2 is the mode of the heap, which can be one of

  • :min - a min-heap, where keys are compared with &<=/2
  • :max - a max-heap, where keys are compared with &>=/2
  • {:min | :max, module} - a min- or max-heap, where the module must implement a compare function with signature (any, any -> :lt | :eq | :gt)

For example, a min-heap with Date keys would be initialized as

iex> PairingHeap.new({:min, Date}, [{~D[2023-09-01], :b}, {~D[2023-08-01], :a}])
#PairingHeap<root: {~D[2023-08-01], :a}, size: 2, mode: {:min, Date}>

To add a single key-item pair to a heap, use PairingHeap.put/3:

iex> PairingHeap.new(:min) |> PairingHeap.put(1, :a)
#PairingHeap<root: {1, :a}, size: 1, mode: :min>

PairingHeap.peek/1 is used to obtained the key-item pair of the root without modifying the heap:

iex> PairingHeap.new(:min, [{1, :a}]) |> PairingHeap.peek()
{:ok, {1, :a}}

Extraction and removal of the root node is accomplished with PairingHeap.pop/1:

iex> {:ok, {key, item}, heap} = 
...>   PairingHeap.new(:min, [{1, :a}]) 
...>   |> PairingHeap.pop()
iex> {key, item}
{1, :a}
iex> heap
#PairingHeap<root: :empty, size: 0, mode: :min>

Both PairingHeap.peek/1 and PairingHeap.pop/1 return :error if the heap is empty.

PairingHeap.pull/2 pops zero or more items from the heap and returns the updated heap:

iex> {items, heap} =
...>   PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}])
...>   |> PairingHeap.pull(2)
iex> items
[{1, :a}, {2, :b}]
iex> heap
#PairingHeap<root: {3, :c}, size: 1, mode: :min>

If the number in PairingHeap.pull/2 is greater than the heap size, all of the key-item pairs are returned, along with an empty heap.

Two heaps with the same mode can be merged with PairingHeap.merge/2:

iex> heap1 = PairingHeap.new(:min, [{2, :b}])
iex> heap2 = PairingHeap.new(:min, [{1, :a}])
iex> PairingHeap.merge(heap1, heap2)
#PairingHeap<root: {1, :a}, size: 2, mode: :min>

Similarly, one or more heaps can be merged with PairingHeap.merge/1, which takes a list of heaps as its sole argument:

iex> heap1 = PairingHeap.new(:min, [{2, :b}])
iex> heap2 = PairingHeap.new(:min, [{1, :a}])
iex> PairingHeap.merge([heap1, heap2])
#PairingHeap<root: {1, :a}, size: 2, mode: :min>

Enumerable and Collectable

PairingHeap implements the Enumerable and Collectable protocols, meaning that functions from Enum and Stream are available.

Enum.into is a simple way to add a batch of key-item pairs to a heap:

iex> heap = PairingHeap.new(:min, [{2, :b}])
iex> [{3, :c}, {1, :a}] |> Enum.into(heap)
#PairingHeap<root: {1, :a}, size: 3, mode: :min>

Use Enum.to_list to list the entire contents of the heap in key order:

iex> heap = PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}])
iex> heap |> Enum.to_list()
[{1, :a}, {2, :b}, {3, :c}]

map, filter, and reduce are also available for PairingHeap. But beware:

Any operation that pops k elements from a heap will have O(k log n) complexity on average.

Summary

Functions

Return true if the heap is empty, and false otherwise.

Retrun true if the heap contains the key-item pair, and false otherwise.

Merge a list of one or more heaps into a single heap.

Merge two heaps into a single heap.

Return an empty heap with the given mode.

Return a heap with the given mode and insert each key-item pair in the given list.

Return the root key-item pair in the heap without modifying the heap.

Return the root key-item pair in the heap, as well as the updated heap after the root is removed.

Return the first n key-item pairs from the heap and the final state of the heap.

Insert a new key and item to the heap and return the updated heap.

Return the size of the heap, the total number of nodes.

Types

@type item() :: any()
@type key() :: any()
@type mode() :: :min | :max | {:min, module()} | {:max, module()}
@type pair() :: {key(), item()}
@type t() :: %PairingHeap{
  mode: mode(),
  ordered?: PairingHeap.Node.ordered_fn(),
  root: :empty | PairingHeap.Node.t(),
  size: non_neg_integer()
}

Functions

@spec empty?(t()) :: boolean()

Return true if the heap is empty, and false otherwise.

Examples

iex> PairingHeap.new(:min) |> PairingHeap.empty?()
true
Link to this function

member?(pairing_heap, pair)

View Source
@spec member?(t(), pair()) :: boolean()

Retrun true if the heap contains the key-item pair, and false otherwise.

Examples

iex> heap = PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}])
iex> heap |> PairingHeap.member?({2, :b})
true
@spec merge([t()]) :: t()

Merge a list of one or more heaps into a single heap.

An exception will be raised if the modes of the heaps are not identical.

Examples

iex> PairingHeap.merge([
...>   PairingHeap.new(:min, [{1, :a}]),
...>   PairingHeap.new(:min, [{2, :b}])
...> ])
#PairingHeap<root: {1, :a}, size: 2, mode: :min>
@spec merge(t(), t()) :: t()

Merge two heaps into a single heap.

An exception will be raised if the modes of the two heaps are not identical.

Examples

iex> PairingHeap.merge(
...>   PairingHeap.new(:min, [{1, :a}]),
...>   PairingHeap.new(:min, [{2, :b}])
...> )
#PairingHeap<root: {1, :a}, size: 2, mode: :min>
@spec new(mode()) :: t()

Return an empty heap with the given mode.

The mode can be one of

  • :min - a min-heap, where keys are compared with &<=/2
  • :max - a max-heap, where keys are compared with &>=/2
  • {:min | :max, module} - a min or max heap, where the module must implement a compare funciton with signature (any, any -> :lt | :eq | :gt)

Examples

iex> PairingHeap.new(:min)
#PairingHeap<root: :empty, size: 0, mode: :min>
@spec new(mode(), [pair()]) :: t()

Return a heap with the given mode and insert each key-item pair in the given list.

The mode can be one

  • :min - a min-heap, where keys are compared with &<=/2
  • :max - a max-heap, where keys are compared with &>=/2
  • {:min | :max, module} - a min or max heap, where the module must implement a compare funciton with signature (any, any -> :lt | :eq | :gt)

Examples

iex> PairingHeap.new(:min, [{2, :b}, {1, :a}])
#PairingHeap<root: {1, :a}, size: 2, mode: :min>
@spec peek(t()) :: {:ok, pair()} | :error

Return the root key-item pair in the heap without modifying the heap.

If the heap is non-empty, this returns {:ok, {key, item}}. If the heap is empty, :error is returned.

Examples

iex> PairingHeap.new(:min, [{1, :a}]) |> PairingHeap.peek()
{:ok, {1, :a}}
@spec pop(t()) :: {:ok, pair(), t()} | :error

Return the root key-item pair in the heap, as well as the updated heap after the root is removed.

If the heap is non-empty, this returns {:ok, {key, item}, heap}. If the heap is empty, :error is returned.

Examples

iex> {:ok, {key, item}, heap} =
...>   PairingHeap.new(:min, [{1, :a}])
...>   |> PairingHeap.pop()
iex> {key, item}
{1, :a}
iex> heap
#PairingHeap<root: :empty, size: 0, mode: :min>
@spec pull(t(), non_neg_integer()) :: {[pair()], t()}

Return the first n key-item pairs from the heap and the final state of the heap.

If the heap size is less than n, all key-item pairs are returned, along with an empty heap.

Examples

iex> {items, heap} =
...>   PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}])
...>   |> PairingHeap.pull(2)
iex> items
[{1, :a}, {2, :b}]
iex> heap
#PairingHeap<root: {3, :c}, size: 1, mode: :min>
@spec put(t(), key(), item()) :: t()

Insert a new key and item to the heap and return the updated heap.

Examples

iex> PairingHeap.new(:min) |> PairingHeap.put(1, :a)
#PairingHeap<root: {1, :a}, size: 1, mode: :min>
@spec size(t()) :: non_neg_integer()

Return the size of the heap, the total number of nodes.

Examples

iex> PairingHeap.new(:min, [{1, :a}]) |> PairingHeap.size()
1