Packages

Skewheaps are fun, weird, priority queues that self-balance over time.

Current section

Files

Jump to
skewheap lib skewheap.ex
Raw

lib/skewheap.ex

defmodule Skewheap do
@moduledoc """
Skewheap - a mergable priority queue
Skewheaps are fun, weird, priority queues that self-balance over time.
Their structural depth is not guaranteed and individual operations may vary
in performance. That said, its _amortized_ performance is roughly O(log n)
([source](https://en.wikipedia.org/wiki/Skew_heap)).
Skewheaps' most interesting characteristic is that they can be _very_ quickly
merged together non-destructively, creating a new, balanced heap containing
all elements of the source heaps.
## Examples
iex> {_, items} = 1..10 |> Enum.shuffle() |> Enum.into(Skewheap.new()) |> Skewheap.drain()
...> items
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
iex> a = 1..3 |> Enum.shuffle() |> Enum.into(Skewheap.new())
...> b = 4..6 |> Enum.shuffle() |> Enum.into(Skewheap.new())
...> {_, items} = Skewheap.merge(a, b) |> Skewheap.drain()
...> items
[1, 2, 3, 4, 5, 6]
"""
@typep skewnode :: :leaf | {any(), skewnode, skewnode}
defmacrop skewnode(p, l \\ :leaf, r \\ :leaf), do: quote do: {unquote(p), unquote(l), unquote(r)}
defmacrop payload(node), do: quote do: elem((unquote node), 0)
defmacrop left(node), do: quote do: elem((unquote node), 1)
defmacrop right(node), do: quote do: elem((unquote node), 2)
defmacrop leaf?(node), do: quote do: (unquote node) == :leaf
@spec merge_nodes(skewheap, skewnode, skewnode) :: skewnode
defp merge_nodes(_, :leaf, :leaf), do: :leaf
defp merge_nodes(_, :leaf, b), do: b
defp merge_nodes(_, a, :leaf), do: a
defp merge_nodes(skew, a, b) do
if skew.sorter.(payload(b), payload(a)) do
merge_nodes(skew, b, a)
else
skewnode(payload(a), merge_nodes(skew, b, right(a)), left(a))
end
end
defstruct size: 0, root: :leaf, sorter: &<=/2
@typep sorter :: (any(), any() -> boolean())
@opaque skewheap :: %Skewheap{
size: non_neg_integer(),
root: skewnode,
sorter: sorter,
}
@doc """
Returns a new Skewheap.
## Examples
iex> s = Skewheap.new()
...> Skewheap.size(s)
0
"""
@spec new() :: skewheap
def new(), do: %Skewheap{}
@spec new(sorter) :: skewheap
def new(sorter), do: %Skewheap{sorter: sorter}
@doc """
True when the Skewheap has no items in it.
## Examples
iex> Skewheap.new() |> Skewheap.empty?()
true
iex> 1..10 |> Enum.shuffle() |> Enum.into(Skewheap.new()) |> Skewheap.empty?()
false
"""
defmacro empty?(skew) do
quote do
(unquote skew).size == 0
end
end
@doc """
Returns the number of items in the Skewheap.
## Examples
iex> Skewheap.new() |> Skewheap.size()
0
iex> 1..10 |> Enum.shuffle() |> Enum.into(Skewheap.new()) |> Skewheap.size()
10
"""
@spec size(skewheap) :: non_neg_integer()
def size(skew), do: skew.size
@doc """
Returns the top element of the heap without removing it or `:nothing` if empty.
## Examples
iex> 1..10 |> Enum.shuffle() |> Enum.into(Skewheap.new()) |> Skewheap.peek()
1
iex> Skewheap.new() |> Skewheap.peek()
:nothing
"""
@spec peek(skewheap) :: any()
def peek(skew) when empty?(skew), do: :nothing
def peek(skew), do: payload(skew.root)
@doc """
## Examples
iex> {_, items} = Skewheap.fill(Skewheap.new(), 1..5 |> Enum.shuffle()) |> Skewheap.drain()
...> items
[1, 2, 3, 4, 5]
"""
def fill(skew, items) do
case items do
[car | cdr] ->
skew = put(skew, car)
fill(skew, cdr)
[] ->
skew
end
end
@doc """
Adds a new element to the heap.
## Examples
iex> s = Skewheap.new()
...> s = Skewheap.put(s, 42)
...> Skewheap.put(s, "fnord")
...> Skewheap.peek(s)
42
"""
@spec put(skewheap, any()) :: skewheap
def put(skew, payload) when empty?(skew) do
%Skewheap{
size: 1,
root: skewnode(payload),
sorter: skew.sorter,
}
end
def put(skew, payload) do
%Skewheap{
size: skew.size + 1,
root: merge_nodes(skew, skew.root, skewnode(payload)),
sorter: skew.sorter
}
end
@doc """
Retrieves the top element from the heap or `:nothing` if empty.
## Examples
iex> {_, v} = Skewheap.new() |> Skewheap.take()
...> v
:nothing
iex> {s, v} = [1,2,3] |> Enum.shuffle() |> Enum.into(Skewheap.new()) |> Skewheap.take()
...> {v, Skewheap.size(s)}
{1, 2}
"""
@spec take(skewheap) :: {skewheap, any()}
def take(skew) when empty?(skew), do: {skew, :nothing}
def take(skew) do
{payload, _, _} = skew.root
{
%Skewheap{
size: skew.size - 1,
root: merge_nodes(skew, left(skew.root), right(skew.root)),
sorter: skew.sorter,
},
payload,
}
end
@doc """
Removes all elements from the heap and returns them as a list.
## Examples
iex> {_, items} = 1..10 |> Enum.shuffle() |> Enum.into(Skewheap.new()) |> Skewheap.drain()
...> items
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
"""
@spec drain(skewheap) :: {skewheap, [any()]}
def drain(skew) when empty?(skew), do: {skew, []}
def drain(skew) do
{skew, payload} = take(skew)
{skew, rest} = drain(skew)
{skew, [payload | rest]}
end
@doc """
Merges two skew heaps into a new heap.
## Examples
iex> a = 1..3 |> Enum.shuffle() |> Enum.into(Skewheap.new())
...> b = 4..6 |> Enum.shuffle() |> Enum.into(Skewheap.new())
...> {_, items} = Skewheap.merge(a, b) |> Skewheap.drain()
...> items
[1, 2, 3, 4, 5, 6]
"""
@spec merge(skewheap, skewheap) :: skewheap
def merge(a, b) do
%Skewheap{
size: a.size + b.size,
root: merge_nodes(a, a.root, b.root),
sorter: a.sorter,
}
end
end