Current section

Files

Jump to
inplace lib adt linked_list.ex
Raw

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