lib/heap/heap.ex

defmodule Collections.Heap do
  defstruct data: nil, size: 0, comparator: nil

  @moduledoc """
  Leftist heap implementation in Elixir

  See also: [Leftist Tree](https://en.wikipedia.org/wiki/Leftist_tree)

  Time complexity

  * `&peek/2`    : O(1)
  * `&push/2`    : O(logn)
  * `&pop/2`     : O(logn)
  * `&size/1`    : O(1)
  * `&member?/2` : O(n)
  * `&empty?/1`  : O(1)
  """

  alias Collections.Heap

  @type data :: {non_neg_integer(), any(), data(), data()} | nil

  @type t :: %__MODULE__{
          data: data(),
          size: non_neg_integer(),
          comparator: (any(), any() -> boolean())
        }

  @leaf nil

  @compile {:min, :max, :new, :size, :peek}

  @doc """
  Create an empty min `heap` with default comparator `&</2`.

  A min heap is a heap tree which always has the smallest value at the top.

  ## Examples

      iex> 1..10
      ...>   |> Enum.shuffle()
      ...>   |> Enum.into(Heap.min())
      ...>   |> Heap.peek()
      1
  """
  @spec min() :: t
  def min, do: Heap.new(&</2)

  @doc """
  Create an empty max `heap` with default comparator `&>/2`.

  A max heap is a heap tree which always has the largest value at the top.

  ## Examples

      iex> 1..10
      ...>   |> Enum.shuffle()
      ...>   |> Enum.into(Heap.max())
      ...>   |> Heap.peek()
      10
  """
  @spec max() :: t
  def max, do: Heap.new(&>/2)

  @doc """
  Create an empty heap with the default comparator `&</2`.

  Behaves the same as `&Heap.min`
  """
  @spec new() :: t
  def new(), do: %Heap{comparator: &</2}

  @doc """
  Create an empty heap with a specific comparator.

  ## Examples

        iex> 1..10
        ...>   |> Enum.shuffle()
        ...>   |> Enum.into(Heap.new(&(&1 > &2)))
        ...>   |> Enum.to_list()
        [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

  The given function should compare two arguments, and return true if the first argument precedes the second one.
  """
  @spec new((any, any -> boolean)) :: t
  def new(comparator) when is_function(comparator, 2), do: %Heap{comparator: comparator}

  @doc """
  Test if the `heap` is empty

  ## Examples

      iex> Heap.new() |> Heap.empty?()
      true
      
      iex> Heap.new() |> Heap.push(10) |> Heap.empty?()
      false
  """
  @spec empty?(t) :: boolean
  def empty?(t), do: Heap.size(t) == 0

  @doc """
  Returns the number of elements in `heap`.

  ## Examples

      iex> 1..10
      ...>   |> Enum.into(Heap.new())
      ...>   |> Heap.size()
      10
  """
  @spec size(t) :: non_neg_integer()
  def size(%Heap{size: size}), do: size

  @doc """
  Push a new element into `heap`.

  ## Examples

      iex> Heap.new()
      ...>   |> Heap.push(10)
      ...>   |> Heap.peek()
      10
  """
  @spec push(t, any()) :: t
  def push(%Heap{data: data, size: size, comparator: cmp}, value) do
    %Heap{data: merge(data, {1, value, @leaf, @leaf}, cmp), size: size + 1, comparator: cmp}
  end

  @doc """
  Returns the element at the top of `heap`.

  If the `heap` is empty, `default` is returned

  If `default` is not provided, nil is used

  ## Examples

      iex> Heap.new()
      ...>   |> Heap.peek()
      nil

      iex> Heap.new()
      ...>   |> Heap.peek(10)
      10

      iex> 1..10
      ...>   |> Enum.shuffle()
      ...>   |> Enum.into(Heap.new())
      ...>   |> Heap.peek()
      1
  """
  @spec peek(t, default) :: any() | default when default: any()
  def peek(heap, default \\ nil)
  def peek(%Heap{data: nil}, default), do: default
  def peek(%Heap{data: {_, v, _, _}}, _default), do: v

  @doc """
  Removes the element at the top of the `heap` and returns the element and the updated heap.

  If the `heap` is empty, `default` is returned

  If `default` is not provided, nil is used

  ## Examples

      iex> {nil, _} = Heap.new()
      ...>   |> Heap.pop()

      iex> {10, _} = Heap.new()
      ...>   |> Heap.pop(10)

      iex> {1, rest_heap} = 1..10
      ...>   |> Enum.shuffle()
      ...>   |> Enum.into(Heap.new())
      ...>   |> Heap.pop()
      ...> {2, _} = Heap.pop(rest_heap)
      ...> Heap.size(rest_heap)
      9
  """
  @spec pop(t, default) :: {any(), updated_heap :: t} | {default, t} when default: any()
  def pop(heap, default \\ nil)
  def pop(%Heap{data: nil, size: 0} = heap, default), do: {default, heap}

  def pop(%Heap{data: {_, v, l, r}, size: size, comparator: cmp}, _default),
    do: {v, %Heap{data: merge(l, r, cmp), size: size - 1, comparator: cmp}}

  @doc """
  Test if the `heap` contains the `value`.

  ## Examples

      iex> heap = 1..10
      ...>   |> Enum.into(Heap.new())
      ...> Heap.member?(heap, 5)
      true
      ...> Heap.member?(heap, 20)
      false
  """
  @spec member?(t, any()) :: boolean()
  def member?(%Heap{data: data}, value), do: has_member?(data, value)

  @spec rank(data()) :: non_neg_integer()
  defp rank(@leaf), do: 0
  defp rank({r, _, _, _}), do: r

  @spec merge(data(), data(), (any(), any() -> boolean())) :: data()
  defp merge(@leaf, @leaf, _cmp), do: nil
  defp merge(@leaf, t, _cmp), do: t
  defp merge(t, @leaf, _com), do: t

  defp merge({_, lv, ll, lr} = t1, {_, rv, rl, rr} = t2, cmp) do
    case cmp.(lv, rv) do
      true -> swipe(lv, ll, merge(lr, t2, cmp))
      false -> swipe(rv, rl, merge(t1, rr, cmp))
      err -> raise("Comparator should return boolean, but returned '#{err}'.")
    end
  end

  @spec swipe(any(), data(), data()) :: data()
  defp swipe(v, left, right) do
    if rank(left) >= rank(right) do
      {rank(right) + 1, v, left, right}
    else
      {rank(left) + 1, v, right, left}
    end
  end

  @spec has_member?(data(), any()) :: boolean()
  defp has_member?(nil, _value), do: false

  defp has_member?({_, v, l, r}, value) do
    if v == value do
      true
    else
      has_member?(l, value) || has_member?(r, value)
    end
  end
end