defmodule PairingHeap do @readme "README.md" @external_resource @readme @moduledoc_readme @readme |> File.read!() |> String.split("") |> Enum.fetch!(1) |> String.trim() @moduledoc """ #{@moduledoc_readme} """ defstruct root: :empty, size: 0, mode: nil, ordered?: nil alias PairingHeap.Node @type key() :: any() @type value() :: any() @type pair() :: {key(), value()} @type mode() :: :min | :max | {:min, module()} | {:max, module()} @type t :: %PairingHeap{ root: :empty | Node.t(), size: non_neg_integer(), mode: mode(), ordered?: Node.ordered_fn() } @doc """ 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 """ @spec new(mode()) :: t() def new(mode) do %PairingHeap{ root: :empty, size: 0, mode: mode, ordered?: to_ordered_fn(mode) } end @doc """ Return a heap with the given `mode` and insert each key-value pair in the given `list`. 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, [{2, :b}, {1, :a}]) #PairingHeap """ @spec new(mode(), [pair()]) :: t def new(mode, []), do: new(mode) def new(mode, [{_, _} | _] = list) do Enum.reduce(list, new(mode), fn {key, value}, heap -> put(heap, key, value) end) end # Generates the `ordered?` function. If `ordered?.(key1, key2)` is true, # then the heap property is satisfied if a node with key `key1` is a parent of # a ndoe with key `key2`. @spec to_ordered_fn(mode()) :: Node.ordered_fn() defp to_ordered_fn(:min), do: to_node_fn(&<=/2) defp to_ordered_fn(:max), do: to_node_fn(&>=/2) defp to_ordered_fn({:min, module}) when is_atom(module), do: to_node_fn(&(module.compare(&1, &2) != :gt)) defp to_ordered_fn({:min, module}) when is_atom(module), do: to_node_fn(&(module.compare(&1, &2) != :lt)) # Lift a function of two key to a function of two Nodes. @spec to_node_fn((key(), key() -> boolean())) :: Node.ordered_fn() defp to_node_fn(key_fn) do fn %Node{item: {key1, _}}, %Node{item: {key2, _}} -> key_fn.(key1, key2) end end @doc """ Return `true` if the heap is empty, and `false` otherwise. ## Examples iex> PairingHeap.new(:min) |> PairingHeap.empty?() true """ @spec empty?(t()) :: boolean() def empty?(%PairingHeap{root: :empty} = _heap), do: true def empty?(%PairingHeap{root: %PairingHeap.Node{}} = _heap), do: false @doc """ Insert a new key and value to the heap and return the updated heap. ## Examples iex> PairingHeap.new(:min) |> PairingHeap.put(1, :a) #PairingHeap """ @spec put(t(), key(), value()) :: t() def put(%PairingHeap{root: :empty, size: 0} = heap, key, value) do %{heap | root: Node.new({key, value}, []), size: 1} end def put( %PairingHeap{root: %Node{} = node, size: size, ordered?: ordered?} = heap, key, value ) do %{ heap | root: Node.merge(Node.new({key, value}, []), node, ordered?), size: size + 1 } end @doc """ Return the root key-value pair in the heap without modifying the heap. If the heap is non-empty, this returns `{:ok, {key, value}}`. If the heap is empty, `:error` is returned. ## Examples iex> PairingHeap.new(:min, [{1, :a}]) |> PairingHeap.peek() {:ok, {1, :a}} """ @spec peek(t()) :: {:ok, pair()} | :error def peek(%PairingHeap{root: :empty} = _heap), do: :error def peek(%PairingHeap{root: %Node{item: {key, value}}} = _heap), do: {:ok, {key, value}} @doc """ Return the root key-value 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, value}, heap}`. If the heap is empty, `:error` is returned. ## Examples iex> {:ok, {key, value}, heap} = ...> PairingHeap.new(:min, [{1, :a}]) ...> |> PairingHeap.pop() iex> {key, value} {1, :a} iex> heap #PairingHeap """ @spec pop(t()) :: {:ok, pair(), t()} | :error def pop(%PairingHeap{root: :empty}), do: :error def pop( %PairingHeap{ root: %Node{item: {key, value}, children: children}, size: size, ordered?: ordered? } = heap ) do heap = case children do [] -> %{heap | root: :empty, size: 0} [_ | _] -> %{heap | root: Node.merge(children, ordered?), size: size - 1} end {:ok, {key, value}, heap} end @doc """ Return the size of the heap, the total number of nodes. ## Examples iex> PairingHeap.new(:min, [{1, :a}]) |> PairingHeap.size() 1 """ @spec size(t()) :: non_neg_integer() def size(%PairingHeap{size: size} = _heap), do: size @doc """ Return the first `n` key-value pairs from the heap and the final state of the heap. If the heap `size` is less than `n`, all key-value pairs are returned along with an empty heap. ## Examples iex> {pairs, heap} = ...> PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}]) ...> |> PairingHeap.pull(2) iex> pairs [{1, :a}, {2, :b}] iex> heap #PairingHeap """ @spec pull(t(), non_neg_integer()) :: {[pair()], t()} def pull(heap, n) when is_integer(n) and n >= 0 do {data, heap} = pull(heap, n, []) {Enum.reverse(data), heap} end defp pull(heap, 0, acc), do: {acc, heap} defp pull(%PairingHeap{} = heap, n, acc) do case pop(heap) do {:ok, {key, value}, heap} -> pull(heap, n - 1, [{key, value} | acc]) :error -> {acc, heap} end end @doc """ 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 """ @spec merge(t(), t()) :: t() def merge(%PairingHeap{mode: m1}, %PairingHeap{mode: m2}) when m1 != m2 do # TODO: Improve this error message raise ArgumentError, message: "when merging heaps, the modes must match" end def merge(%PairingHeap{root: :empty} = heap, %PairingHeap{root: :empty}), do: heap def merge(%PairingHeap{root: %Node{}} = heap, %PairingHeap{root: :emtpy}), do: heap def merge(%PairingHeap{root: :empty}, %PairingHeap{root: %Node{}} = heap), do: heap def merge( %PairingHeap{root: node1, size: size1, ordered?: ordered?} = heap, %PairingHeap{root: node2, size: size2} ) do %{heap | root: Node.merge([node1, node2], ordered?), size: size1 + size2} end @doc """ 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 """ @spec merge([t]) :: t def merge([heap] = _heaps), do: heap def merge([heap1, heap2]), do: merge(heap1, heap2) def merge([heap1, heap2 | rest]), do: merge(merge(heap1, heap2), merge(rest)) @doc """ Retrun `true` if the `heap` contains the key-value `pair`, and `false` otherwise. ## Examples iex> heap = PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}]) iex> heap |> PairingHeap.member?({2, :b}) true """ @spec member?(t(), pair()) :: boolean() def member?(%PairingHeap{root: :empty} = _heap, _pair), do: false def member?( %PairingHeap{root: %Node{} = node, ordered?: ordered?} = _heap, {_, _} = pair ) do Node.member?(node, pair, ordered?) end @doc """ Return the list of items in the heap. While the head of the list will be the item in the root node of the heap, there is not guarantee that the remaining items will be in key order. This funciton runs in `O(n)` time and is faster than repeatedly calling `pop/1`. ## Examples iex> heap = PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}]) iex> PairingHeap.dump(heap) [{1, :a}, {2, :b}, {3, :c}] """ @spec dump(t()) :: [pair()] def dump(%PairingHeap{root: :empty}), do: [] def dump(%PairingHeap{root: %Node{} = node}), do: Node.dump(node) end