Initial push of Heap.

This commit is contained in:
James Harton 2016-03-02 20:29:16 +13:00
commit 9d9b1c6045
14 changed files with 494 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/_build
/cover
/deps
erl_crash.dump
*.ez

66
README.md Normal file
View file

@ -0,0 +1,66 @@
# Heap
A Heap is a very useful data structure, because it sorts, quickly, at insert time.
See also: https://en.wikipedia.org/wiki/Heap_(data_structure)
You can use it for things like:
- Help with scientific computing
- Quickly sorting
- Priority queues
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
1. Add heap to your list of dependencies in `mix.exs`:
def deps do
[{:heap, "~> 0.0.1"}]
end
## Examples
Create a min heap and use it to find the smallest element in a collection:
```elixir
1..500 |> Enum.shuffle |> Enum.into(Heap.min) |> Heap.root
# => 1
```
Likewise, for max heaps:
```elixir
1..500 |> Enum.shuffle |> Enum.into(Heap.max) |> Heap.root
# => 500
```
A priority queue:
Tuples are compared by their elements in order, so you can push tuples
of `{priority, term}` into a Heap for sorting by priority:
```elixir
Heap.new
|> Heap.push({4, :jam})
|> Heap.push({1, :milk})
|> Heap.push({2, :eggs})
|> Heap.push({1, :bread})
|> Heap.push({3, :butter})
|> Heap.push({2, :coffee})
|> Enum.map(fn {_, what} -> what end)
# => [:bread, :milk, :coffee, :eggs, :butter, :jam]
```
### Documentation
Full API documentation is available on (hexdocs.pm)[https://hexdocs.pm/heap]
## Contributing
1. Fork it ( https://github.com/Huia/Huia/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

30
config/config.exs Normal file
View file

@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.
# You can configure for your application as:
#
# config :heap, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:heap, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"

112
lib/heap.ex Normal file
View file

@ -0,0 +1,112 @@
defmodule Heap do
defstruct h: nil, n: 0, d: nil
@moduledoc """
A heap is a special tree data structure. Good for sorting and other magic.
See also: [Heap (data structure) on Wikipedia](https://en.wikipedia.org/wiki/Heap_(data_structure)).
"""
@type t :: %Heap{}
@doc """
Create an empty min heap.
A min heap is a heap tree which always has the smallest value at the root.
"""
@spec min() :: Heap.t
def min(), do: Heap.new(:>)
@doc """
Create an empty max heap.
A max heap is a heap tree which always has the largest value at the root.
"""
@spec max() :: Heap.t
def max(), do: Heap.new(:<)
@doc """
Create an empty heap.
"""
@spec new() :: Heap.t
@spec new(:> | :<) :: Heap.t
def new(direction \\ :>), do: %Heap{d: direction}
@doc """
Test if the heap is empty.
"""
@spec empty?(Heap.t) :: boolean()
def empty?(%Heap{h: nil, n: 0}), do: true
def empty?(%Heap{}), do: false
@doc """
Test it the heap contains the element <elem>
"""
@spec member?(Heap.t, any()) :: boolean()
def member?(%Heap{}=heap, value) do
root = Heap.root heap
heap = Heap.pop heap
has_member? heap, root, value
end
@doc """
Push a new element into the heap.
"""
@spec push(Heap.t, any()) :: Heap.t
def push(%Heap{h: h, n: n, d: d}, v), do: %Heap{h: meld(h, {v, []}, d), n: n + 1, d: d}
@doc """
Pop the root element off the heap and discard it.
"""
@spec pop(Heap.t) :: Heap.t
def pop(%Heap{h: nil, n: 0}), do: nil
def pop(%Heap{h: {_, q}, n: n, d: d}), do: %Heap{h: pair(q, d), n: n - 1, d: d}
@doc """
Return the element at the root of the heap.
"""
@spec root(Heap.t) :: any()
def root(%Heap{h: {v, _}}), do: v
def root(%Heap{h: nil, n: 0}), do: nil
@doc """
Return the number of elements in the heap.
"""
@spec size(Heap.t) :: non_neg_integer()
def size(%Heap{n: n}), do: n
@doc """
Quickly sort an enumerable with a heap.
"""
@spec sort(Enum.t) :: List.t
@spec sort(Enum.t, :< | :>) :: List.t
def sort(enum, direction \\ :>) do
enum
|> Enum.into(Heap.new(direction))
|> Enum.to_list
end
defp meld(nil, queue, _), do: queue
defp meld(queue, nil, _), do: queue
defp meld({k0,l0}, {k1,_}=r, :>) when k0 < k1, do: {k0, [r | l0]}
defp meld({_,_}=l, {k1,r0}, :>), do: {k1, [l | r0]}
defp meld({k0,l0}, {k1,_}=r, :<) when k0 > k1, do: {k0, [r | l0]}
defp meld({_,_}=l, {k1,r0}, :<), do: {k1, [l | r0]}
defp pair([], _), do: nil
defp pair([q], _), do: q
defp pair([q0, q1 | q], d) do
q2 = meld(q0, q1, d)
meld(q2, pair(q, d), d)
end
defp has_member?(_, previous, compare) when previous == compare, do: true
defp has_member?(nil, _, _), do: false
defp has_member?(heap, _, compare) do
previous = Heap.root heap
heap = Heap.pop heap
has_member? heap, previous, compare
end
end

11
lib/heap/collectable.ex Normal file
View file

@ -0,0 +1,11 @@
defimpl Collectable, for: Heap do
def into heap do
{heap, fn
(h, {:cont, v}) -> Heap.push h, v
(h, :done) -> h
(_, :halt) -> :ok
end}
end
end

23
lib/heap/enumerable.ex Normal file
View file

@ -0,0 +1,23 @@
defimpl Enumerable, for: Heap do
def count heap do
{:ok, Heap.size heap}
end
def member? heap, v do
{:ok, Heap.member?(heap, v)}
end
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(heap, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(heap, &1, fun)}
def reduce(heap, {:cont, acc}, fun) do
case Heap.root heap do
nil ->
{:done, acc}
root ->
heap = Heap.pop heap
reduce(heap, fun.(root, acc), fun)
end
end
end

11
lib/heap/inspect.ex Normal file
View file

@ -0,0 +1,11 @@
defimpl Inspect, for: Heap do
import Inspect.Algebra
def inspect heap, opts do
concat [
"#Heap<",
to_doc(Enum.to_list(heap), opts),
">"
]
end
end

43
mix.exs Normal file
View file

@ -0,0 +1,43 @@
defmodule Heap.Mixfile do
use Mix.Project
def project do
[app: :heap,
version: "0.0.1",
maintainers: [ "James Harton <james@messagerocket.co>" ],
licenses: [ "MIT" ],
links: %{
"GitHub" => "https://github.com/jamesotron/heap",
"Docs" => "https://hexdocs.pm/heap"
},
description: "Heap data structure and tools",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
preferred_cli_env: [espec: :test],
deps: deps]
end
# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
[applications: [:logger]]
end
# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type "mix help deps" for more examples and options
defp deps do
[
{:espec, "~> 0.8.11", only: :test},
{:ex_doc, "~> 0.11.4", only: :dev}
]
end
end

3
mix.lock Normal file
View file

@ -0,0 +1,3 @@
%{"espec": {:hex, :espec, "0.8.15"},
"ex_doc": {:hex, :ex_doc, "0.11.4"},
"meck": {:hex, :meck, "0.8.4"}}

View file

@ -0,0 +1,12 @@
defmodule HeapCollectableSpec do
use ESpec
describe "into" do
let :source, do: 1..500
subject do: source |> Enum.into(Heap.new)
it "inserts into the heap" do
expect(Heap.size subject) |> to(eq 500)
end
end
end

View file

@ -0,0 +1,34 @@
defmodule HeapEnumerableSpec do
use ESpec
describe "count" do
let :heap, do: Enum.into(1..500, Heap.new)
subject do: Enum.count heap
it do: is_expected |> to(eq 500)
end
describe "member?" do
let :heap, do: Heap.new
subject do: Enum.member? heap, 13
context "When the value is in the heap" do
let :heap, do: Heap.new |> Heap.push(13)
it do: is_expected |> to(be_true)
end
context "When the value is not in the heap" do
it do: is_expected |> to(be_false)
end
end
describe "reduce" do
let :heap, do: Enum.into(1..500, Heap.new)
subject do: Enum.filter heap, fn(i) -> rem(i, 2) == 0 end
it do: is_expected |> to(have_min 2)
it do: is_expected |> to(have_max 500)
it do: is_expected |> to(have_count 250)
end
end

View file

@ -0,0 +1,10 @@
defmodule HeapInspectSpec do
use ESpec
describe "inspect" do
let :heap, do: Enum.into(1..5, Heap.new)
subject do: inspect heap
it do: is_expected |> to(eq "#Heap<[1, 2, 3, 4, 5]>")
end
end

123
spec/heap_spec.exs Normal file
View file

@ -0,0 +1,123 @@
defmodule HeapSpec do
use ESpec
describe "new" do
subject do: Heap.new
it do: is_expected |> to(be_struct Heap)
it "is empty" do
expect(Heap.empty? subject) |> to(be_true)
end
end
describe "min" do
subject do: Heap.min
it do: is_expected |> to(be_struct Heap)
it "is empty" do
expect(Heap.empty? subject) |> to(be_true)
end
it "has the correct sort direction" do
expect(subject.d) |> to(eq :>)
end
end
describe "max" do
subject do: Heap.max
it do: is_expected |> to(be_struct Heap)
it "is empty" do
expect(Heap.empty? subject) |> to(be_true)
end
it "has the correct sort direction" do
expect(subject.d) |> to(eq :<)
end
end
describe "empty?" do
subject do: Heap.empty? heap
context "When the heap is empty" do
let :heap, do: Heap.new
it do: is_expected |> to(be_true)
end
context "When the heap contains elements" do
let :heap, do: Heap.new |> Heap.push(13)
it do: is_expected |> to(be_false)
end
end
describe "member?" do
subject do: heap |> Heap.member?(13)
context "When the heap contains the element" do
let :heap, do: Heap.new |> Heap.push(13)
it do: is_expected |> to(be_true)
end
context "When the heap does not contain the element" do
let :heap, do: Heap.new
it do: is_expected |> to(be_false)
end
end
describe "push" do
let :heap, do: Heap.new
subject do: heap |> Heap.push(13)
it "adds the element to the heap" do
expect(subject |> Heap.member?(13)) |> to(be_true)
end
end
describe "pop" do
let :heap, do: Heap.new |> Heap.push(13) |> Heap.push(14)
subject do: Heap.pop heap
it "pops the root element off the heap" do
expect(Heap.root heap) |> to(eq 13)
expect(Heap.root subject) |> to(eq 14)
end
end
describe "root" do
let :heap, do: Heap.new |> Heap.push(13)
subject do: Heap.root heap
it do: is_expected |> to(eq 13)
end
describe "size" do
let :heap, do: Heap.new |> Heap.push(13) |> Heap.push(14)
subject do: Heap.size heap
it do: is_expected |> to(eq 2)
end
describe "sort" do
let :source, do: Enum.shuffle(1..500)
subject do: Heap.sort source, direction
context "When the direction is `:>`" do
let :direction, do: :>
it do: is_expected |> to(eq Enum.to_list(1..500))
end
context "When the direction is `:<`" do
let :direction, do: :<
it do: is_expected |> to(eq Enum.to_list(1..500) |> Enum.reverse)
end
end
end

11
spec/spec_helper.exs Normal file
View file

@ -0,0 +1,11 @@
ESpec.start
ESpec.configure fn(config) ->
config.before fn ->
{:shared, hello: :world}
end
config.finally fn(_shared) ->
:ok
end
end