Packages
inplace
0.2.0
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Mutable data structures
Current section
Files
Jump to
Current section
Files
lib/adt/linked_list.ex
defmodule InPlace.LinkedList do
@moduledoc """
[Singly linked list](https://en.wikipedia.org/wiki/Linked_list#Singly_linked_list)
The data entries are stored as integers.
They will be interpreted (as references) by the calling application.
Note: indices are 1-based.
Options:
- `mode` :: :singly_linked | :doubly_linked
- `circular` :: boolean()
optional, `false` by default;
- `undo` :: boolean()
If true (`false` by default), the removed elements can be restored, one at time, in reverse order
(see `restore/1`). This can be used as a mechanism for backtracking
- `:mapper_fun` # maps data entries to application data
optional, &Function.identity/1 by default
"""
alias InPlace.{Array, Stack}
## List terminator
@terminator 0
@singly_linked_mode :singly_linked
@doubly_linked_mode :doubly_linked
def new(size, opts \\ []) when is_integer(size) and size > 0 do
opts = Keyword.merge(default_opts(), opts)
mode = Keyword.get(opts, :mode)
circular? = Keyword.get(opts, :circular)
undo? = Keyword.get(opts, :undo) && circular? && mode == @doubly_linked_mode
if mode not in [@singly_linked_mode, @doubly_linked_mode] do
throw({:error, {:unknown_mode, mode}})
end
%{
capacity: size,
## mode (:singly_linked or :doubly_linked)
mode: mode,
## circular?
circular: circular?,
## Option to "undo" removals; see restore/1
undo: undo?,
## holds the pointer to the first element of the list
handle: init_handle(mode),
# pointers (links) to the next element
next: init_links(size),
# keeps unused pointers
free: init_free(size),
# references to the data
refs: :atomics.new(size, signed: true),
# mapper `reference -> data`
mapper_fun: Keyword.get(opts, :mapper_fun)
}
|> then(fn state ->
if mode == @doubly_linked_mode do
Map.put(state, :prev, init_links(size))
else
state
end
end)
|> then(fn state ->
if undo? do
Map.put(state, :removed, Stack.new(size))
else
state
end
end)
end
def get(list, idx) when is_integer(idx) and idx > 0 do
case get_pointer(list, idx) do
nil -> nil
pointer -> data(list, pointer)
end
end
def get_pointer(list, idx) when is_integer(idx) and idx > 0 do
get_pointer(list, head(list), 0, idx)
end
defp get_pointer(_list, pointer, step, idx) when step == idx - 1 do
pointer
end
defp get_pointer(list, pointer, step, idx) do
get_pointer(list, next(list, pointer), step + 1, idx)
end
def add_first(list, data) when is_integer(data) do
## Store data in 'free' element of the list
allocated = allocate(list)
set_data(list, allocated, data)
head = head(list)
set_next(list, allocated, head)
add_first_mode(list, head, allocated)
## Allocated element becomes the new head
set_head(list, allocated)
end
defp add_first_mode(%{mode: @singly_linked_mode} = _list, _head, _allocated) do
:ok
end
defp add_first_mode(%{mode: @doubly_linked_mode} = list, head, allocated) do
set_previous(list, allocated, @terminator)
if head(list) == @terminator do
set_tail(list, allocated)
else
set_previous(list, head, allocated)
end
end
def insert(%{mode: mode} = list, idx, data)
when is_integer(data) and is_integer(idx) and idx > 0 do
cond do
idx > size(list) ->
{:error, {:no_index, idx}}
empty?(list) ->
add_first(list, data)
true ->
idx_pointer = get_pointer(list, idx)
next_pointer = next_pointer(list, idx_pointer)
allocated = allocate(list)
set_data(list, allocated, data)
set_next(list, allocated, next_pointer)
set_next(list, idx_pointer, allocated)
## Build `prev` links
## allocated.prev <- idx_pointer
if mode == @doubly_linked_mode do
set_previous(list, allocated, idx_pointer)
## allocated.next.prev <- allocated
if next_pointer == @terminator do
## insertion at the end - update the tail
set_tail(list, allocated)
else
## insertion in between - point the `prev` link of next pointer
## to newly allocated element
set_previous(list, next_pointer, allocated)
end
end
end
end
def add_last(list, data) when is_integer(data) do
if empty?(list) do
add_first(list, data)
else
insert(list, size(list), data)
end
end
def delete(list, idx) when is_integer(idx) and idx > 0 do
if idx > size(list) do
{:error, {:no_index, idx}}
else
# Removing first element of the list
if idx == 1 do
current_head = head(list)
delete_pointer(list, current_head)
{:removed, current_head}
else
pointer = get_pointer(list, idx - 1)
pointer_to_delete = next_pointer(list, pointer)
pointer_next = next_pointer(list, pointer_to_delete)
set_next(list, pointer, pointer_next)
delete_pointer(list, pointer_to_delete)
{:removed, pointer_to_delete}
end
end
end
def delete_pointer(_list, @terminator) do
:ok
end
def delete_pointer(%{mode: mode} = list, pointer) do
forget_or_hide(list, pointer)
cond do
pointer == head(list) ->
next_head = next_pointer(list, pointer)
set_head(list, next_head)
if mode == @doubly_linked_mode && next_head != @terminator do
set_previous(list, next_head, @terminator)
end
mode == @doubly_linked_mode ->
next_pointer = next_pointer(list, pointer)
if pointer == tail(list) do
## Last element
new_tail = prev(list, pointer)
set_tail(list, new_tail)
end
prev_pointer = prev(list, pointer)
set_next(list, prev_pointer, next_pointer)
set_previous(list, next_pointer, prev_pointer)
true ->
:ok
end
end
def to_list(list) do
reduce(list, []) |> Enum.reverse()
end
def reduce(list, initial_value, reducer \\ nil) do
iterate(list, action: (reducer || default_reducer(list)), initial_value: initial_value)
end
defp default_reducer(list) do
(fn p, acc ->
[data(list, p) | acc]
end)
end
def iterate(list, opts \\ [])
def iterate(list, opts) do
start = Keyword.get(opts, :start, head(list))
action = Keyword.get(opts, :action, default_reducer(list))
stop_on = Keyword.get(opts, :stop_on, fn next ->
last_pointer = list.circular && start || @terminator
next == last_pointer
end)
## If action is of arity 1, it's a "side-effect" function.
## The argument is a pointer.
## For instance, it might conditionally delete some entries,
## etc.
## The result of the action call would be ignored.
## If action is of arity 2, it's a "reducer" function
## The arguments are : pointer and accumulated value
## If {:halt, term()} is returned, the iteration is halted with term()
## If {:cont, term()} or term() is returned, the iteration continues.
##
cond do
is_function(action, 1) ->
iterate_impl(list, start, stop_on, action)
is_function(action, 2) ->
iterate_impl(list, start, stop_on, action, Keyword.get(opts, :initial_value))
true ->
throw({:error, :action_invalid_arity})
end
end
## Iteration with side-effects
defp iterate_impl(_list, @terminator, _stop_on, action) when is_function(action, 1) do
:ok
end
defp iterate_impl(list, current_pointer, stop_on, action) when is_function(action, 1) do
action.(current_pointer)
next_p = next(list, current_pointer)
if stop_on.(next_p) do
:ok
else
iterate_impl(list, next_p, stop_on, action)
end
end
## "Reducer" iteration
defp iterate_impl(_list, @terminator, _stop_on, action, acc) when is_function(action, 2) do
acc
end
defp iterate_impl(list, current_pointer, stop_on, action, acc) when is_function(action, 2) do
case action.(current_pointer, acc) do
{:halt, new_acc} ->
new_acc
result ->
new_acc = case result do
{:cont, r} ->
r
r -> r
end
next_p = next(list, current_pointer)
if stop_on.(next_p) do
new_acc
else
iterate_impl(list, next_p, stop_on, action, new_acc)
end
end
end
def empty?(list) do
head(list) == @terminator
end
def size(%{undo: undo?, capacity: capacity, free: free} = list) do
capacity - Stack.size(free) - (undo? && Stack.size(list.removed) || 0)
end
def default_opts() do
[
mode: @doubly_linked_mode,
circular: false,
undo: false,
mapper_fun: &Function.identity/1
]
end
## Helpers
##
## Initialize handle.
## First element - pointer to the head
## Second element (for :doubly_linked) is a tail
defp init_handle(mode) do
size =
if mode == @singly_linked_mode do
1
else
2
end
Array.new(size, @terminator)
end
## Allocate links (pointers to the next element)
defp init_links(size) do
:atomics.new(size, signed: false)
end
## The stack for tracking 'free' indices
## They can be reused after the element is removed from linked list
## (unless `undo=true`, which disables reuse).
defp init_free(size) when is_integer(size) do
ref = Stack.new(size)
Enum.each(1..size, fn idx -> Stack.push(ref, idx) end)
ref
end
def head(%{handle: handle} = _list) do
Array.get(handle, 1)
end
def tail(%{mode: @singly_linked_mode} = _list) do
nil
end
def tail(%{handle: handle, mode: @doubly_linked_mode}) do
Array.get(handle, 2)
end
def set_next(_list, @terminator, _next_pointer) do
:ok
end
def set_next(%{next: pointers} = _list, pointer, next_pointer) do
Array.put(pointers, pointer, next_pointer)
end
def set_previous(_list, @terminator, _next_pointer) do
:ok
end
def set_previous(%{mode: @doubly_linked_mode, prev: pointers} = _list, pointer, prev_pointer) do
Array.put(pointers, pointer, prev_pointer)
end
def set_previous(_list, _pointer, _prev_pointer) do
:ok
end
def set_data(%{refs: refs} = _list, pointer, data_ref) do
Array.put(refs, pointer, data_ref)
end
def set_head(%{handle: handle} = list, pointer) do
Array.put(handle, 1, pointer)
if pointer == @terminator || size(list) == 1 do
set_tail(list, pointer)
end
:ok
end
def set_tail(%{handle: handle, mode: @doubly_linked_mode} = _list, pointer) do
Array.put(handle, 2, pointer)
end
def set_tail(_list, _pointer) do
:ok
end
def data(%{refs: refs, mapper_fun: mapper} = _list, pointer) do
mapper.(Array.get(refs, pointer))
end
def next(%{circular: circular?} = list, pointer) do
case next_pointer(list, pointer) do
@terminator when circular? ->
head(list)
next_pointer ->
next_pointer
end
end
def prev(%{circular: circular?} = list, pointer) do
case prev_pointer(list, pointer) do
@terminator when circular? ->
tail(list)
next_pointer ->
next_pointer
end
end
## The `next_pointer/2` and `prev_pointer/2` functions ignore `circular` option,
## and act based on position of list terminator.
## We will use it when updating the list,
## so we can treat internal structure uniformly.
## The navigation over circular lists is implemented by next/2 and prev/2.
##
defp next_pointer(%{next: pointers} = _list, pointer) do
Array.get(pointers, pointer)
end
defp prev_pointer(%{prev: pointers} = _list, pointer) do
Array.get(pointers, pointer)
end
defp prev_pointer(_singly_linked, _pointer) do
nil
end
defp allocate(%{free: free} = _list) do
Stack.pop(free) || throw(:list_over_capacity)
end
## If `undo` is enabled, record the pointer to removed element.
## Could be restored later for circular doubly linked list
## See `restore/1
##
defp forget_or_hide(%{undo: undo?} = list, pointer) when is_integer(pointer) and pointer > 0 do
if undo? do
hide_pointer(list, pointer)
else
forget_pointer(list, pointer)
end
end
defp forget_pointer(%{free: free} = _list, pointer) do
Stack.push(free, pointer)
end
defp hide_pointer(%{removed: removed} = _list, pointer) do
Stack.push(removed, pointer)
end
def num_free_pointers(%{free: free} = _list) do
Stack.size(free)
end
def restore(%{undo: true, removed: removed} = list) do
case Stack.pop(removed) do
nil -> false
restored_pointer ->
restore_pointer(list, restored_pointer)
end
end
def restore(_list) do
false
end
defp restore_pointer(list, pointer) do
next_pointer = next_pointer(list, pointer)
## Special case: next_pointer for restored pointer
## is a current head
if next_pointer == head(list) do
## We replace head with the restored pointer
set_head(list, pointer)
else
prev_pointer = prev(list, pointer)
set_next(list, prev_pointer, pointer)
end
set_previous(list, next_pointer, pointer)
end
end