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> 1..10 |> Enum.shuffle() |> Enum.into(Skewheap.new()) |> Skewheap.drain()
{%Skewheap{root: :leaf, size: 0, sorter: &:erlang."=<"/2}, [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())
...> Skewheap.merge(a, b) |> Skewheap.drain()
{%Skewheap{root: :leaf, size: 0, sorter: &:erlang."=<"/2}, [1, 2, 3, 4, 5, 6]}
"""
@type skewnode :: :leaf | {any(), skewnode, skewnode}
@spec skewnode(any(), skewnode, skewnode) :: skewnode
defp skewnode(p, l \\ :leaf, r \\ :leaf), do: {p, l, r}
defmacrop leaf?(node) do
quote do
(unquote node) == :leaf
end
end
defmacrop payload(node) do
quote do
elem((unquote node), 0)
end
end
defmacrop left(node) do
quote do
elem((unquote node), 1)
end
end
defmacrop right(node) do
quote do
elem((unquote node), 2)
end
end
@spec merge_nodes(skewheap, skewnode, skewnode) :: skewnode
defp merge_nodes(skew, a, b) do
cond do
leaf?(a) and leaf?(b) -> :leaf
leaf?(a) -> b
leaf?(b) -> a
skew.sorter.(payload(b), payload(a)) -> merge_nodes(skew, b, a)
true -> skewnode(payload(a), merge_nodes(skew, b, right(a)), left(a))
end
end
defstruct size: 0, root: :leaf, sorter: nil
@type sorter :: (any(), any() -> boolean())
@type skewheap :: %Skewheap{
size: non_neg_integer(),
root: skewnode,
sorter: sorter,
}
@doc """
Returns a new Skewheap.
## Examples
iex> Skewheap.new()
%Skewheap{size: 0, root: :leaf, sorter: &:erlang."=<"/2}
"""
@spec new() :: skewheap
def new(), do: %Skewheap{sorter: &<=/2}
@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 nil if empty.
## Examples
iex> 1..10 |> Enum.shuffle() |> Enum.into(Skewheap.new()) |> Skewheap.peek()
1
"""
@spec peek(skewheap) :: any()
def peek(skew) when empty?(skew), do: nil
def peek(skew), do: payload(skew.root)
@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 nil if empty.
## Examples
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, nil}
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