defmodule PairingHeap do @readme "README.md" @external_resource @readme @moduledoc_readme @readme |> File.read!() @moduledoc """ #{@moduledoc_readme} """ defstruct root: :empty, size: 0, mode: nil, ordered?: nil alias PairingHeap.Node @type key :: any @type item :: any @type pair :: {key, item} @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-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 """ @spec new(mode, [pair]) :: t def new(mode, []), do: new(mode) def new(mode, [{_, _} | _] = list) do Enum.reduce(list, new(mode), fn {key, item}, heap -> put(heap, key, item) 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{data: {key1, _}}, %Node{data: {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}), do: true def empty?(%PairingHeap{root: %PairingHeap.Node{}}), do: false @doc """ Insert a new key and item to the heap and return the updated heap. ## Examples iex> PairingHeap.new(:min) |> PairingHeap.put(1, :a) #PairingHeap """ @spec put(t, key, item) :: t def put(%PairingHeap{root: :empty, size: 0} = heap, key, item) do %{heap | root: Node.new({key, item}, []), size: 1} end def put( %PairingHeap{root: %Node{} = node, size: size, ordered?: ordered?} = heap, key, item ) do %{ heap | root: Node.merge(node, Node.new({key, item}, []), ordered?), size: size + 1 } end @doc """ 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 peek(t) :: {:ok, pair} | :error def peek(%PairingHeap{root: :empty}), do: :error def peek(%PairingHeap{root: %Node{data: {key, item}}}), do: {:ok, {key, item}} @doc """ 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 """ @spec pop(t) :: {:ok, pair, t} | :error def pop(%PairingHeap{root: :empty}), do: :error def pop( %PairingHeap{ root: %Node{data: {key, item}, 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, item}, 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}), do: size @doc """ 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 """ @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, item}, heap} -> pull(heap, n - 1, [{key, item} | 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]), 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-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 member?(t, pair) :: boolean def member?(%PairingHeap{root: :empty}, _pair), do: false def member?(%PairingHeap{root: %Node{} = node, ordered?: ordered?}, {_, _} = pair) do Node.member?(node, pair, ordered?) end end