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
- :mapper_fun (optional, &Function.identity/1 by default) - maps data entries to application data
"""
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)
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: Keyword.get(opts, :mode),
## 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)
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, @terminator, _step, _idx) do
nil
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(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(%{mode: mode} = list, idx) when is_integer(idx) and idx > 0 do
if idx > size(list) do
{:error, {:no_index, idx}}
else
deallocate(list, idx)
# Removing first element of the list
if idx == 1 do
head = head(list)
next_head = next(list, head)
set_head(list, next_head)
if mode == @doubly_linked_mode && next_head != @terminator do
set_previous(list, next_head, @terminator)
end
else
pointer = get_pointer(list, idx - 1)
pointer_to_delete = next(list, pointer)
pointer_next = next(list, pointer_to_delete)
set_next(list, pointer, pointer_next)
if mode == @doubly_linked_mode do
if pointer_to_delete == tail(list) do
## Last element
set_tail(list, pointer)
else
set_previous(list, pointer_next, pointer)
end
end
end
end
end
def to_list(list) do
to_list(list, head(list), [])
end
defp to_list(_list, @terminator, acc) do
Enum.reverse(acc)
end
defp to_list(list, pointer, acc) do
to_list(list, next(list, pointer), [data(list, pointer) | acc])
end
def empty?(list) do
head(list) == @terminator
end
def size(%{capacity: capacity, free: free} = _list) do
capacity - Stack.size(free)
end
def default_opts() do
[
mode: @singly_linked_mode,
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
##
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
:noop
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
:noop
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
:noop
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
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
:noop
end
def data(%{refs: refs, mapper_fun: mapper} = _list, pointer) do
mapper.(Array.get(refs, pointer))
end
def next(_list, @terminator) do
nil
end
def next(%{next: pointers} = _list, pointer) do
Array.get(pointers, pointer)
end
def prev(_list, @terminator) do
nil
end
def prev(%{prev: pointers} = _list, pointer) do
Array.get(pointers, pointer)
end
def allocate(%{free: free} = _list) do
Stack.pop(free) || throw(:list_over_capacity)
end
def deallocate(%{free: free} = _list, idx) when is_integer(idx) and idx > 0 do
Stack.push(free, idx)
end
end