Skip to content

cs_survival_kit.data_structures

Data structures, each written by hand for study.

GrowthPolicy

GrowthPolicy = Callable[[int], int]

Maps the current capacity to the new capacity after an array resize.

A valid policy must return a value strictly greater than its input. DynamicArray.append raises ValueError if a policy does not.

AbstractList

Bases: ABC

An ordered collection of elements, each at a position from 0 upward.

This class defines the operations every list implementation provides. It stores nothing and implements nothing itself: a subclass supplies the storage and all six operations, and may add operations of its own that suit its storage, such as an O(1) prepend on a linked list.

A subclass that leaves out any of these operations cannot be instantiated.

Complexity

The interface fixes behaviour only. What each operation costs is set by the implementation, and comparing those costs is what the implementations are for. Each one documents its own.

Operation Required behaviour
len(a) the number of elements
iteration every element, in position order
a[i] the element at position i
item in a whether any element equals item
append add an element after the current last one
repr(a) the class name and the elements, in order

Examples:

The base class cannot be instantiated; an implementation can.

>>> AbstractList()
Traceback (most recent call last):
    ...
TypeError: Can't instantiate abstract class AbstractList...
>>> from cs_survival_kit.data_structures import DynamicArray
>>> isinstance(DynamicArray[int](), AbstractList)
True

__len__ abstractmethod

__len__() -> int

Return the number of elements in the list.

Returns:

Type Description
int

The number of elements, which is 0 for an empty list.

Source code in lib/cs_survival_kit/data_structures/abstract_list.py
@abstractmethod
def __len__(self) -> int:
    """Return the number of elements in the list.

    Returns:
        The number of elements, which is 0 for an empty list.
    """
    raise NotImplementedError

__iter__ abstractmethod

__iter__() -> Iterator[T]

Iterate over the elements in position order.

Yields:

Type Description
T

Each element, starting with the one at position 0.

Source code in lib/cs_survival_kit/data_structures/abstract_list.py
@abstractmethod
def __iter__(self) -> Iterator[T]:
    """Iterate over the elements in position order.

    Yields:
        Each element, starting with the one at position 0.
    """
    raise NotImplementedError

__getitem__ abstractmethod

__getitem__(index: int) -> T

Return the element at position index.

Parameters:

Name Type Description Default
index int

The position of the element. Every implementation accepts 0 <= index < len(self); whether it accepts anything else, such as a negative index, is up to the implementation.

required

Returns:

Type Description
T

The element at index.

Raises:

Type Description
IndexError

If there is no element at index.

Source code in lib/cs_survival_kit/data_structures/abstract_list.py
@abstractmethod
def __getitem__(self, index: int) -> T:
    """Return the element at position `index`.

    Args:
        index: The position of the element. Every implementation accepts
            `0 <= index < len(self)`; whether it accepts anything else,
            such as a negative index, is up to the implementation.

    Returns:
        The element at `index`.

    Raises:
        IndexError: If there is no element at `index`.
    """
    raise NotImplementedError

__contains__ abstractmethod

__contains__(item: object) -> bool

Return whether item is in the list.

Parameters:

Name Type Description Default
item object

The value to look for.

required

Returns:

Type Description
bool

True if some element is item or equals it, otherwise False.

Source code in lib/cs_survival_kit/data_structures/abstract_list.py
@abstractmethod
def __contains__(self, item: object) -> bool:
    """Return whether `item` is in the list.

    Args:
        item: The value to look for.

    Returns:
        `True` if some element is `item` or equals it, otherwise `False`.
    """
    raise NotImplementedError

__repr__ abstractmethod

__repr__() -> str

Return a string showing the class name and the elements in order.

Returns:

Type Description
str

A string such as DynamicArray([1, 2, 3]).

Source code in lib/cs_survival_kit/data_structures/abstract_list.py
@abstractmethod
def __repr__(self) -> str:
    """Return a string showing the class name and the elements in order.

    Returns:
        A string such as `DynamicArray([1, 2, 3])`.
    """
    raise NotImplementedError

append abstractmethod

append(item: T) -> None

Add item to the end of the list.

Afterwards item is the last element and len(self) is one greater.

Parameters:

Name Type Description Default
item T

The element to add.

required
Source code in lib/cs_survival_kit/data_structures/abstract_list.py
@abstractmethod
def append(self, item: T) -> None:
    """Add `item` to the end of the list.

    Afterwards `item` is the last element and `len(self)` is one greater.

    Args:
        item: The element to add.
    """
    raise NotImplementedError

DynamicArray

DynamicArray(capacity: int = 4, growth: GrowthPolicy = doubling)

Bases: AbstractList[T]

A resizable array backed by fixed-capacity storage.

Elements are stored in a fixed-size backing list of capacity slots, where only the first len(self) slots are in use. When an append finds no free slot, the array first resizes: it allocates a larger backing list, sized by the growth policy, and copies every element into it. The backing storage never shrinks, so popping elements leaves capacity unchanged.

Unlike list, indexing accepts only non-negative indices in the range 0 <= index < len(self). Negative indices and slices are not supported.

Complexity
Operation Time Space
append O(1) amortized, O(n) worst O(1) amortized, O(n) worst
pop O(1) O(1)
a[i], a[i] = x O(1) O(1)
len(a) O(1) O(1)
item in a O(n) O(1)
iteration O(n) O(1)
resize O(n) O(n)

The append bounds assume a geometric growth policy such as the default doubling. With an additive policy, append is amortized O(n). Total storage is O(capacity). For an array built by appends alone, doubling keeps capacity below 2n once the array has grown past its initial capacity.

Parameters:

Name Type Description Default
capacity int

The number of slots to allocate up front. Must be at least 1.

4
growth GrowthPolicy

The growth policy that computes the new capacity on each resize. Must return a value greater than its input. Defaults to doubling.

doubling

Raises:

Type Description
ValueError

If capacity is less than 1.

Examples:

>>> a = DynamicArray[int](capacity=2)
>>> a.append(1)
>>> a.append(2)
>>> len(a), a.capacity
(2, 2)
>>> a.append(3)  # no free slot, so the array doubles first
>>> len(a), a.capacity
(3, 4)
>>> a
DynamicArray([1, 2, 3])
>>> a[0] = 10
>>> a.pop()
3
>>> list(a)
[10, 2]
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def __init__(self, capacity: int = 4, growth: GrowthPolicy = doubling) -> None:
    if capacity <= 0:
        raise ValueError("capacity must be greater than 0")

    self._items: list[T | None] = [None] * capacity
    self._capacity: int = capacity
    self._size: int = 0
    self._growth: GrowthPolicy = growth

capacity property

capacity: int

The number of allocated slots, used or not.

Always at least len(self). The two are equal when the array is full, and the next append will trigger a resize.

Complexity
  • Time: O(1)
  • Space: O(1)

__len__

__len__() -> int

Return the number of elements in the array.

Returns:

Type Description
int

The number of elements in the array.

Complexity
  • Time: O(1)
  • Space: O(1)
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def __len__(self) -> int:
    """Return the number of elements in the array.

    Returns:
        The number of elements in the array.

    Complexity:
        - Time: O(1)
        - Space: O(1)
    """
    return self._size

__getitem__

__getitem__(index: int) -> T

Return the element at index.

Parameters:

Name Type Description Default
index int

The position of the element. Must satisfy 0 <= index < len(self).

required

Returns:

Type Description
T

The element at index.

Raises:

Type Description
IndexError

If index is negative or not less than len(self).

Complexity
  • Time: O(1)
  • Space: O(1)
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def __getitem__(self, index: int) -> T:
    """Return the element at `index`.

    Args:
        index: The position of the element. Must satisfy
            `0 <= index < len(self)`.

    Returns:
        The element at `index`.

    Raises:
        IndexError: If `index` is negative or not less than `len(self)`.

    Complexity:
        - Time: O(1)
        - Space: O(1)
    """
    if index < 0 or index >= self._size:
        raise IndexError("index out of range")

    return typing.cast(T, self._items[index])

__setitem__

__setitem__(index: int, item: T) -> None

Replace the element at index with item.

Only overwrites an existing element. Use append to add one.

Parameters:

Name Type Description Default
index int

The position of the element to replace. Must satisfy 0 <= index < len(self).

required
item T

The new element.

required

Raises:

Type Description
IndexError

If index is negative or not less than len(self).

Complexity
  • Time: O(1)
  • Space: O(1)
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def __setitem__(self, index: int, item: T) -> None:
    """Replace the element at `index` with `item`.

    Only overwrites an existing element. Use `append` to add one.

    Args:
        index: The position of the element to replace. Must satisfy
            `0 <= index < len(self)`.
        item: The new element.

    Raises:
        IndexError: If `index` is negative or not less than `len(self)`.

    Complexity:
        - Time: O(1)
        - Space: O(1)
    """
    if index < 0 or index >= self._size:
        raise IndexError("index out of range")

    self._items[index] = item

__iter__

__iter__() -> Iterator[T]

Iterate over the elements from index 0 to len(self) - 1.

Yields:

Type Description
T

Each element, in index order.

Complexity
  • Time: O(n) to exhaust the iterator
  • Space: O(1)
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def __iter__(self) -> Iterator[T]:
    """Iterate over the elements from index 0 to `len(self) - 1`.

    Yields:
        Each element, in index order.

    Complexity:
        - Time: O(n) to exhaust the iterator
        - Space: O(1)
    """
    for i in range(self._size):
        yield typing.cast(T, self._items[i])

__contains__

__contains__(item: object) -> bool

Return whether item is in the array.

Checks the elements in index order and stops at the first match. An element matches if it is item or equals it, the same rule that list uses. Unused slots are never examined, so None in a is True only if None was actually stored.

Parameters:

Name Type Description Default
item object

The value to look for.

required

Returns:

Type Description
bool

True if some element is item or equals it, otherwise False.

Complexity
  • Time: O(n), since the elements are not ordered and each one may have to be checked; O(1) if the first element matches
  • Space: O(1)

Examples:

>>> a = DynamicArray[int]()
>>> a.append(1)
>>> 1 in a, 2 in a
(True, False)
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def __contains__(self, item: object) -> bool:
    """Return whether `item` is in the array.

    Checks the elements in index order and stops at the first match. An
    element matches if it is `item` or equals it, the same rule that
    `list` uses. Unused slots are never examined, so `None in a` is
    `True` only if `None` was actually stored.

    Args:
        item: The value to look for.

    Returns:
        `True` if some element is `item` or equals it, otherwise `False`.

    Complexity:
        - Time: O(n), since the elements are not ordered and each one may
          have to be checked; O(1) if the first element matches
        - Space: O(1)

    Examples:
        >>> a = DynamicArray[int]()
        >>> a.append(1)
        >>> 1 in a, 2 in a
        (True, False)
    """
    for i in range(self._size):
        element = self._items[i]
        if element is item or element == item:
            return True

    return False

__repr__

__repr__() -> str

Return a string showing the class name and the elements, like list.

Unused slots and the capacity are not shown. Use capacity to inspect the backing storage.

Returns:

Type Description
str

A string such as DynamicArray([1, 2, 3]).

Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def __repr__(self) -> str:
    """Return a string showing the class name and the elements, like `list`.

    Unused slots and the capacity are not shown. Use `capacity` to inspect
    the backing storage.

    Returns:
        A string such as `DynamicArray([1, 2, 3])`.
    """
    return f"{type(self).__name__}({list(self)})"

append

append(item: T) -> None

Add item to the end of the array.

If the array is full, it first resizes to the capacity returned by the growth policy, copying every element, and then stores the item in the first free slot. Resizing only when there is no room means every allocated slot gets used before the array grows.

The growth policy's result is checked before anything changes. If it is not greater than the current capacity, ValueError is raised and the array is left exactly as it was.

Parameters:

Name Type Description Default
item T

The element to add.

required

Raises:

Type Description
ValueError

If the array is full and the growth policy returns a capacity that is not greater than the current capacity.

Complexity
  • Time: O(1) amortized with a geometric growth policy; O(n) for the append that triggers a resize
  • Space: O(1) amortized; O(n) for the append that triggers a resize

Examples:

>>> a = DynamicArray[str](capacity=2)
>>> a.append("x")
>>> a.append("y")  # fills the last free slot; no resize yet
>>> a.capacity
2
>>> a.append("z")  # no free slot, so the array grows first
>>> a.capacity
4

A growth policy that doesn't grow is rejected:

>>> stuck = DynamicArray[int](capacity=1, growth=lambda c: c)
>>> stuck.append(1)
>>> stuck.append(2)
Traceback (most recent call last):
    ...
ValueError: growth policy must increase capacity (1 -> 1)
>>> list(stuck), stuck.capacity
([1], 1)
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def append(self, item: T) -> None:
    """Add `item` to the end of the array.

    If the array is full, it first resizes to the capacity returned by the
    growth policy, copying every element, and then stores the item in the
    first free slot. Resizing only when there is no room means every
    allocated slot gets used before the array grows.

    The growth policy's result is checked before anything changes. If it
    is not greater than the current capacity, `ValueError` is raised and
    the array is left exactly as it was.

    Args:
        item: The element to add.

    Raises:
        ValueError: If the array is full and the growth policy returns a
            capacity that is not greater than the current capacity.

    Complexity:
        - Time: O(1) amortized with a geometric growth policy; O(n) for
          the append that triggers a resize
        - Space: O(1) amortized; O(n) for the append that triggers a resize

    Examples:
        >>> a = DynamicArray[str](capacity=2)
        >>> a.append("x")
        >>> a.append("y")  # fills the last free slot; no resize yet
        >>> a.capacity
        2
        >>> a.append("z")  # no free slot, so the array grows first
        >>> a.capacity
        4

        A growth policy that doesn't grow is rejected:

        >>> stuck = DynamicArray[int](capacity=1, growth=lambda c: c)
        >>> stuck.append(1)
        >>> stuck.append(2)
        Traceback (most recent call last):
            ...
        ValueError: growth policy must increase capacity (1 -> 1)
        >>> list(stuck), stuck.capacity
        ([1], 1)
    """
    # if the array is full, resize it according to the growth policy before
    # inserting the new item
    if self._size == self._capacity:
        new_capacity: int = self._growth(self._capacity)

        # validate the growth policy's result before mutating any state so a
        # bad policy leaves the array unchanged
        if new_capacity <= self._capacity:
            raise ValueError(
                "growth policy must increase capacity "
                f"({self._capacity} -> {new_capacity})"
            )

        self._resize(new_capacity)

    # insert the item into the array by its index in the first open position
    self._items[self._size] = item

    # update the size of the array
    self._size = self._size + 1

pop

pop() -> T

Remove and return the last element.

The freed slot is cleared, so the array no longer holds a reference to the element. Capacity is unchanged.

Returns:

Type Description
T

The element that was at index len(self) - 1.

Raises:

Type Description
IndexError

If the array is empty.

Complexity
  • Time: O(1)
  • Space: O(1)

Examples:

>>> a = DynamicArray[int]()
>>> a.append(1)
>>> a.pop()
1
>>> a.pop()
Traceback (most recent call last):
    ...
IndexError: pop from empty array
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def pop(self) -> T:
    """Remove and return the last element.

    The freed slot is cleared, so the array no longer holds a reference to
    the element. Capacity is unchanged.

    Returns:
        The element that was at index `len(self) - 1`.

    Raises:
        IndexError: If the array is empty.

    Complexity:
        - Time: O(1)
        - Space: O(1)

    Examples:
        >>> a = DynamicArray[int]()
        >>> a.append(1)
        >>> a.pop()
        1
        >>> a.pop()
        Traceback (most recent call last):
            ...
        IndexError: pop from empty array
    """
    if self._size == 0:
        raise IndexError("pop from empty array")

    pos: int = self._size - 1
    item: T = typing.cast(T, self._items[pos])
    self._items[pos] = None
    self._size = pos

    return item

SinglyLinkedList

SinglyLinkedList(items: Iterable[T] = ())

Bases: AbstractList[T]

A sequence of nodes, each linked to the next, with head and tail references.

Each element lives in its own node, and each node links only forward to the next one. The list keeps references to the first node (head) and the last node (tail), plus a running size count:

  • head makes prepend and pop_front O(1).
  • tail makes append O(1). Without it, appending would mean walking the whole chain to find the last node.
  • The size count makes len O(1) instead of a full traversal.

Links only point forward, so there is no fast way to reach a node's predecessor. Removing the last element would require walking from the head to the second-to-last node, so the list offers no O(1) pop_back. That limitation is what a doubly linked list removes.

Compared with DynamicArray, adding at the front is O(1) instead of O(n), but indexing is O(n) instead of O(1), and every element pays for an extra node object and link.

Unlike list, indexing accepts only non-negative indices in the range 0 <= index < len(self). Negative indices and slices are not supported.

Complexity
Operation Time Space
prepend O(1) O(1)
append O(1) O(1)
pop_front O(1) O(1)
a[i] O(n) O(1)
item in a O(n) O(1)
remove O(n) O(1)
reverse O(n) O(1)
len(a) O(1) O(1)
iteration O(n) O(1)

Total storage is O(n): one node per element.

Parameters:

Name Type Description Default
items Iterable[T]

Elements to add to the new list, in order. Defaults to empty.

()

Examples:

>>> a = SinglyLinkedList[int]([2, 3])
>>> a.prepend(1)
>>> a.append(4)
>>> a
SinglyLinkedList([1, 2, 3, 4])
>>> a.pop_front()
1
>>> a[1], 3 in a, len(a)
(3, True, 3)
>>> a.reverse()
>>> list(a)
[4, 3, 2]
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def __init__(self, items: Iterable[T] = ()) -> None:
    # initialize the empty state for a singly-linked list
    self._head: _Node[T] | None = None
    self._tail: _Node[T] | None = None
    self._size: int = 0

    # iterate over the given items and append them to the
    # end of the singly-linked list
    for item in items:
        self.append(item)

__len__

__len__() -> int

Return the number of elements in the list.

Returns:

Type Description
int

The number of elements in the list.

Complexity
  • Time: O(1), from the stored size count
  • Space: O(1)
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def __len__(self) -> int:
    """Return the number of elements in the list.

    Returns:
        The number of elements in the list.

    Complexity:
        - Time: O(1), from the stored size count
        - Space: O(1)
    """
    return self._size

__iter__

__iter__() -> Iterator[T]

Iterate over the elements from head to tail.

Yields:

Type Description
T

Each element, in list order.

Complexity
  • Time: O(n) to exhaust the iterator
  • Space: O(1)
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def __iter__(self) -> Iterator[T]:
    """Iterate over the elements from head to tail.

    Yields:
        Each element, in list order.

    Complexity:
        - Time: O(n) to exhaust the iterator
        - Space: O(1)
    """
    # establish a node pointer to traverse the linked list
    current_node: _Node[T] | None = self._head

    # while the node pointer points to valid nodes, continue
    # traversing and yielding the current node as you progress
    while current_node is not None:
        yield current_node.item
        current_node = current_node.next

__getitem__

__getitem__(index: int) -> T

Return the element at index.

Nodes are scattered in memory and each one only knows where the next node is, so there is no way to compute where element index lives. The lookup walks index links from the head. An array can jump straight there because its elements sit at evenly spaced positions.

Parameters:

Name Type Description Default
index int

The position of the element. Must satisfy 0 <= index < len(self).

required

Returns:

Type Description
T

The element at index.

Raises:

Type Description
IndexError

If index is negative or not less than len(self).

Complexity
  • Time: O(n); O(index) links are followed
  • Space: O(1)
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def __getitem__(self, index: int) -> T:
    """Return the element at `index`.

    Nodes are scattered in memory and each one only knows where the next
    node is, so there is no way to compute where element `index` lives.
    The lookup walks `index` links from the head. An array can jump
    straight there because its elements sit at evenly spaced positions.

    Args:
        index: The position of the element. Must satisfy
            `0 <= index < len(self)`.

    Returns:
        The element at `index`.

    Raises:
        IndexError: If `index` is negative or not less than `len(self)`.

    Complexity:
        - Time: O(n); O(index) links are followed
        - Space: O(1)
    """
    # verify that the given index is within the valid range
    if index < 0 or index >= self._size:
        raise IndexError("index out of range")

    # walk forward from the head one link at a time until reaching the
    # target position; the bounds check above guarantees the node exists
    current_node = self._head
    for _ in range(index):
        assert current_node is not None
        current_node = current_node.next

    assert current_node is not None
    return current_node.item

__contains__

__contains__(item: object) -> bool

Return whether item is in the list.

Checks the elements from the head onward and stops at the first match. An element matches if it is item or equals it, the same rule that list uses.

Parameters:

Name Type Description Default
item object

The value to look for.

required

Returns:

Type Description
bool

True if some element is item or equals it, otherwise False.

Complexity
  • Time: O(n)
  • Space: O(1)
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def __contains__(self, item: object) -> bool:
    """Return whether `item` is in the list.

    Checks the elements from the head onward and stops at the first match.
    An element matches if it is `item` or equals it, the same rule that
    `list` uses.

    Args:
        item: The value to look for.

    Returns:
        `True` if some element is `item` or equals it, otherwise `False`.

    Complexity:
        - Time: O(n)
        - Space: O(1)
    """
    for element in self:
        if element is item or element == item:
            return True
    return False

__repr__

__repr__() -> str

Return a string showing the class name and the elements, like list.

Returns:

Type Description
str

A string such as SinglyLinkedList([1, 2, 3]).

Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def __repr__(self) -> str:
    """Return a string showing the class name and the elements, like `list`.

    Returns:
        A string such as `SinglyLinkedList([1, 2, 3])`.
    """
    return f"{type(self).__name__}({list(self)})"

prepend

prepend(item: T) -> None

Add item to the front of the list.

The new node links to the current head and becomes the new head. No existing element moves. An array must shift every element one slot to the right to make room at index 0, which costs O(n).

Parameters:

Name Type Description Default
item T

The element to add.

required
Complexity
  • Time: O(1)
  • Space: O(1) for the new node

Examples:

>>> a = SinglyLinkedList[str](["b"])
>>> a.prepend("a")
>>> a
SinglyLinkedList(['a', 'b'])
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def prepend(self, item: T) -> None:
    """Add `item` to the front of the list.

    The new node links to the current head and becomes the new head. No
    existing element moves. An array must shift every element one slot to
    the right to make room at index 0, which costs O(n).

    Args:
        item: The element to add.

    Complexity:
        - Time: O(1)
        - Space: O(1) for the new node

    Examples:
        >>> a = SinglyLinkedList[str](["b"])
        >>> a.prepend("a")
        >>> a
        SinglyLinkedList(['a', 'b'])
    """
    node = _Node(item)

    # an empty list's new node is both the head and the tail
    if self._head is None or self._tail is None:
        self._head = node
        self._tail = node
    else:
        node.next = self._head
        self._head = node

    self._size += 1

append

append(item: T) -> None

Add item to the end of the list.

The tail reference points straight at the last node, so the new node is linked after it and becomes the new tail. Without a tail reference, finding the last node would mean walking the whole chain, making append O(n).

Parameters:

Name Type Description Default
item T

The element to add.

required
Complexity
  • Time: O(1)
  • Space: O(1) for the new node

Examples:

>>> a = SinglyLinkedList[str](["a"])
>>> a.append("b")
>>> a
SinglyLinkedList(['a', 'b'])
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def append(self, item: T) -> None:
    """Add `item` to the end of the list.

    The tail reference points straight at the last node, so the new node
    is linked after it and becomes the new tail. Without a tail reference,
    finding the last node would mean walking the whole chain, making
    `append` O(n).

    Args:
        item: The element to add.

    Complexity:
        - Time: O(1)
        - Space: O(1) for the new node

    Examples:
        >>> a = SinglyLinkedList[str](["a"])
        >>> a.append("b")
        >>> a
        SinglyLinkedList(['a', 'b'])
    """
    node = _Node(item)

    # if the head or tail pointer is not set, then the list is empty.
    # So, set the head and tail pointers to the new node that is to be
    # appended.
    #
    # a useful invariant here is the head pointer will only be not set when
    # the tail pointer is not set which implies the list is empty.
    if self._head is None or self._tail is None:
        self._head = node
        self._tail = node
    else:
        self._tail.next = node
        self._tail = node

    self._size += 1

pop_front

pop_front() -> T

Remove and return the first element.

The head moves to the second node. If that empties the list, the tail is cleared too.

Returns:

Type Description
T

The element that was at the front of the list.

Raises:

Type Description
IndexError

If the list is empty.

Complexity
  • Time: O(1)
  • Space: O(1)

Examples:

>>> a = SinglyLinkedList[int]([1])
>>> a.pop_front()
1
>>> a.pop_front()
Traceback (most recent call last):
    ...
IndexError: pop_front from empty list
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def pop_front(self) -> T:
    """Remove and return the first element.

    The head moves to the second node. If that empties the list, the tail
    is cleared too.

    Returns:
        The element that was at the front of the list.

    Raises:
        IndexError: If the list is empty.

    Complexity:
        - Time: O(1)
        - Space: O(1)

    Examples:
        >>> a = SinglyLinkedList[int]([1])
        >>> a.pop_front()
        1
        >>> a.pop_front()
        Traceback (most recent call last):
            ...
        IndexError: pop_front from empty list
    """
    if self._head is None:
        raise IndexError("pop_front from empty list")

    front_node: _Node[T] = self._head
    self._head = front_node.next

    # maintain the invariant that the head pointer is only not set
    # when the tail pointer is not set as well
    #
    # this if statement is required for when the last remaining element
    # is popped and the head and tail pointers have to be cleared
    if self._head is None:
        self._tail = None

    self._size -= 1

    return front_node.item

remove

remove(item: T) -> None

Remove the first element that matches item.

Only the first match, scanning from the head, is removed. An element matches if it is item or equals it, as in item in a. Unlinking a node means pointing its predecessor's next past it, and nodes have no backward link, so the scan carries a reference to the previous node as it goes. The head and tail references are updated when the removed node is at either end.

Parameters:

Name Type Description Default
item T

The value to remove.

required

Raises:

Type Description
ValueError

If no element matches item.

Complexity
  • Time: O(n)
  • Space: O(1)

Examples:

>>> a = SinglyLinkedList[int]([1, 2, 1])
>>> a.remove(1)
>>> a
SinglyLinkedList([2, 1])
>>> a.remove(5)
Traceback (most recent call last):
    ...
ValueError: item not in list
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def remove(self, item: T) -> None:
    """Remove the first element that matches `item`.

    Only the first match, scanning from the head, is removed. An element
    matches if it is `item` or equals it, as in `item in a`. Unlinking a
    node means pointing its predecessor's `next` past it, and nodes have no
    backward link, so the scan carries a reference to the previous node as
    it goes. The head and tail references are updated when the removed node
    is at either end.

    Args:
        item: The value to remove.

    Raises:
        ValueError: If no element matches `item`.

    Complexity:
        - Time: O(n)
        - Space: O(1)

    Examples:
        >>> a = SinglyLinkedList[int]([1, 2, 1])
        >>> a.remove(1)
        >>> a
        SinglyLinkedList([2, 1])
        >>> a.remove(5)
        Traceback (most recent call last):
            ...
        ValueError: item not in list
    """
    previous_node: _Node[T] | None = None
    current_node: _Node[T] | None = self._head

    while current_node is not None:
        if current_node.item is item or current_node.item == item:
            # if the target node has a predecessor, link it past the
            # target; otherwise the target is the head, so advance the head
            if previous_node is not None:
                previous_node.next = current_node.next
            else:
                self._head = current_node.next

            # if the target was the last node, its predecessor is the new
            # tail (None when the list is now empty)
            if current_node is self._tail:
                self._tail = previous_node

            self._size -= 1
            return

        # continue searching through the linked list while remembering
        # the predecessor nodes in case the target node is found
        previous_node = current_node
        current_node = current_node.next

    raise ValueError("item not in list")

reverse

reverse() -> None

Reverse the list in place.

Walks the list once, re-pointing each node's next at the node before it. Three references track progress: the previous node, the current node, and the next node (saved before its link is overwritten). When the walk ends, the old tail is the new head and the old head is the new tail. No nodes are allocated or copied.

Complexity
  • Time: O(n)
  • Space: O(1)

Examples:

>>> a = SinglyLinkedList[int]([1, 2, 3])
>>> a.reverse()
>>> a
SinglyLinkedList([3, 2, 1])
>>> a.append(0)  # the tail reference was updated too
>>> a
SinglyLinkedList([3, 2, 1, 0])
Source code in lib/cs_survival_kit/data_structures/singly_linked_list.py
def reverse(self) -> None:
    """Reverse the list in place.

    Walks the list once, re-pointing each node's `next` at the node before
    it. Three references track progress: the previous node, the current
    node, and the next node (saved before its link is overwritten). When
    the walk ends, the old tail is the new head and the old head is the new
    tail. No nodes are allocated or copied.

    Complexity:
        - Time: O(n)
        - Space: O(1)

    Examples:
        >>> a = SinglyLinkedList[int]([1, 2, 3])
        >>> a.reverse()
        >>> a
        SinglyLinkedList([3, 2, 1])
        >>> a.append(0)  # the tail reference was updated too
        >>> a
        SinglyLinkedList([3, 2, 1, 0])
    """
    # the current head will be the tail once every link is reversed
    self._tail = self._head

    previous_node: _Node[T] | None = None
    current_node: _Node[T] | None = self._head

    while current_node is not None:
        # save the rest of the list before overwriting the forward link
        next_node = current_node.next

        # point the current node backward, then advance both references
        current_node.next = previous_node
        previous_node = current_node
        current_node = next_node

    # the last node visited, the old tail, is the new head
    self._head = previous_node

additive

additive(step: int) -> GrowthPolicy

Build a growth policy that adds a fixed number of slots on each resize.

This policy is a counterexample to geometric growth. A resize happens every step appends and copies every element, so n appends perform about n² / (2 · step) copies in total. That is amortized O(n) per append, and O(n²) to build an array of n elements. In exchange, a resize never leaves more than step unused slots.

Parameters:

Name Type Description Default
step int

The number of slots to add on each resize. Must be at least 1.

required

Returns:

Type Description
GrowthPolicy

A growth policy that returns capacity + step.

Raises:

Type Description
ValueError

If step is less than 1.

Examples:

>>> additive(16)(4)
20
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def additive(step: int) -> GrowthPolicy:
    """Build a growth policy that adds a fixed number of slots on each resize.

    This policy is a counterexample to geometric growth. A resize happens
    every `step` appends and copies every element, so n appends perform about
    n² / (2 · step) copies in total. That is amortized O(n) per append, and
    O(n²) to build an array of n elements. In exchange, a resize never leaves
    more than `step` unused slots.

    Args:
        step: The number of slots to add on each resize. Must be at least 1.

    Returns:
        A growth policy that returns `capacity + step`.

    Raises:
        ValueError: If `step` is less than 1.

    Examples:
        >>> additive(16)(4)
        20
    """
    # verify the growth step size is valid; if a step size less than 1 is used,
    # the array will not grow
    if step <= 0:
        raise ValueError("step must be greater than 0")

    def add(curr_capacity: int) -> int:
        return curr_capacity + step

    return add

doubling

doubling(curr_capacity: int) -> int

Double the capacity on each resize.

The default growth policy for DynamicArray. Equivalent to geometric(2.0).

Parameters:

Name Type Description Default
curr_capacity int

The current capacity of the array.

required

Returns:

Type Description
int

Twice the current capacity.

Examples:

>>> doubling(4)
8
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def doubling(curr_capacity: int) -> int:
    """Double the capacity on each resize.

    The default growth policy for `DynamicArray`. Equivalent to
    `geometric(2.0)`.

    Args:
        curr_capacity: The current capacity of the array.

    Returns:
        Twice the current capacity.

    Examples:
        >>> doubling(4)
        8
    """
    return curr_capacity * 2

geometric

geometric(factor: float) -> GrowthPolicy

Build a growth policy that multiplies the capacity by a constant factor.

Any factor greater than 1 gives amortized O(1) appends. The factor trades memory for copying: a resize copies every element, so a larger factor means fewer resizes, but right after a resize the capacity is about factor times the number of elements. CPython's list over-allocates by roughly ⅛, choosing low memory overhead over fewer resizes.

Because the new capacity is truncated to an integer, a small factor applied to a small capacity could round back down to the current capacity. The returned policy always grows the capacity by at least 1 to prevent that.

Parameters:

Name Type Description Default
factor float

The multiplier applied to the current capacity. Must be finite and greater than 1.

required

Returns:

Type Description
GrowthPolicy

A growth policy that returns max(capacity + 1, int(capacity * factor)).

Raises:

Type Description
ValueError

If factor is not finite or is not greater than 1.

Examples:

>>> geometric(1.5)(4)
6
>>> geometric(1.1)(4)  # int(4.4) == 4, so the policy grows by 1 instead
5
Source code in lib/cs_survival_kit/data_structures/dynamic_array.py
def geometric(factor: float) -> GrowthPolicy:
    """Build a growth policy that multiplies the capacity by a constant factor.

    Any factor greater than 1 gives amortized O(1) appends. The factor trades
    memory for copying: a resize copies every element, so a larger factor means
    fewer resizes, but right after a resize the capacity is about `factor` times
    the number of elements. CPython's `list` over-allocates by roughly 1/8,
    choosing low memory overhead over fewer resizes.

    Because the new capacity is truncated to an integer, a small factor applied
    to a small capacity could round back down to the current capacity. The
    returned policy always grows the capacity by at least 1 to prevent that.

    Args:
        factor: The multiplier applied to the current capacity. Must be finite
            and greater than 1.

    Returns:
        A growth policy that returns `max(capacity + 1, int(capacity * factor))`.

    Raises:
        ValueError: If `factor` is not finite or is not greater than 1.

    Examples:
        >>> geometric(1.5)(4)
        6
        >>> geometric(1.1)(4)  # int(4.4) == 4, so the policy grows by 1 instead
        5
    """
    # verify the geometric growth factor is valid for a strictly increasing
    # geometric growth policy
    if not math.isfinite(factor) or factor <= 1:
        raise ValueError("factor must be finite and greater than 1")

    def growth(curr_capacity: int) -> int:
        # compute the new capacity given the current capacity and the geometric growth
        # factor
        new_geometric_capacity: int = int(curr_capacity * factor)

        # the new capacity must be at least 1 greater than the current capacity
        # this max() statement ensures the growth policy generates a sequence that
        # is strictly increasing even for small geometric factor values and small
        # capacities
        return max(curr_capacity + 1, new_geometric_capacity)

    return growth