cs_survival_kit.data_structures ¶
Data structures, each written by hand for study.
GrowthPolicy ¶
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.
The contract has two layers.
Six primitives are abstract. An implementation must supply all of
them, and they are the only places its storage is touched: len(a),
iteration, a[i], a[i] = x, insert and pop. A subclass that leaves
out any primitive cannot be instantiated.
Nine defaults are inherited. Each is written here once, in terms of
the primitives: append is insert at the end, prepend is insert at
0, pop_front and pop_back are pop at either end, remove is a scan
followed by pop, item in a and repr(a) are scans, reversed(a) is
one scan buffered and yielded backward, and reverse swaps a[i] with
a[n - 1 - i] inward from both ends. An implementation may override a
default, but it rarely needs to: the cost of each default is simply the
cost of the primitive it calls at that position, and an implementation
whose insert is O(1) at index 0 gets an O(1) prepend for free.
Two defaults deserve a closer look. reverse is only as cheap as
indexing, so a structure that cannot index in O(1) overrides it with an
algorithm suited to its storage. reversed(a) could have been written
the same way, as a[i] from the last position down, but that is
quadratic on a linked list. Instead it spends O(n) auxiliary space on a
buffer so that it is linear on every implementation. A structure that
can walk backward without the buffer, such as an array by index or a
doubly linked list by its backward links, overrides it to get the space
back.
Every index is a non-negative position. a[i], a[i] = x and pop
accept 0 <= index < len(a); insert also accepts index == len(a),
which adds at the end. Negative indices and slices are not part of the
contract.
Complexity
The interface fixes behaviour only. What each primitive costs is set by the implementation, and comparing those costs is what the implementations are for. Each one documents its own. The defaults cost whatever the primitive they call costs at that index:
| Operation | Defined in terms of |
|---|---|
append |
insert(len(a), item) |
prepend |
insert(0, item) |
pop_front |
pop(0) |
pop_back |
pop(len(a) - 1) |
remove |
one iteration to find the index, then pop |
item in a |
one iteration |
repr(a) |
one iteration |
reversed(a) |
one iteration into a buffer, yielded backward |
reverse |
n / 2 swaps, each two a[i] reads and two writes |
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
The defaults come from the base class, so an implementation that writes only the six primitives supports every operation:
>>> a = DynamicArray[int]()
>>> a.append(2)
>>> a.prepend(1)
>>> a.append(3)
>>> a
DynamicArray([1, 2, 3])
>>> a.pop_front(), a.pop_back()
(1, 3)
>>> a.remove(2)
>>> len(a)
0
reversed walks the elements backward without changing the list;
reverse changes the list:
>>> a.append(1)
>>> a.append(2)
>>> list(reversed(a)), list(a)
([2, 1], [1, 2])
>>> a.reverse()
>>> list(a)
[2, 1]
- Data Structures
- Reference (cs-survival-kit 0.8.0) cs_survival_kit data_structures
__len__
abstractmethod
¶
Return the number of elements in the list.
Returns:
| Type | Description |
|---|---|
int
|
The number of elements, which is 0 for an empty list. |
__iter__
abstractmethod
¶
Iterate over the elements in position order.
Yields:
| Type | Description |
|---|---|
T
|
Each element, starting with the one at position 0. |
__getitem__
abstractmethod
¶
Return the element at position index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position of the element. Must satisfy
|
required |
Returns:
| Type | Description |
|---|---|
T
|
The element at |
Raises:
| Type | Description |
|---|---|
IndexError
|
If there is no element at |
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
__setitem__
abstractmethod
¶
Replace the element at position index with item.
Only overwrites an existing element; the length does not change. Use
insert to add one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position of the element to replace. Must satisfy
|
required |
item
|
T
|
The new element. |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If there is no element at |
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
insert
abstractmethod
¶
Add item at position index, moving later elements along by one.
Afterwards self[index] is item and len(self) is one greater.
index == len(self) adds the item after the current last one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position the new element will occupy. Must satisfy
|
required |
item
|
T
|
The element to add. |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
pop
abstractmethod
¶
Remove and return the element at position index.
Later elements move back by one, so afterwards len(self) is one
less and the element that was at index + 1 is at index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position of the element to remove. Must satisfy
|
required |
Returns:
| Type | Description |
|---|---|
T
|
The element that was at |
Raises:
| Type | Description |
|---|---|
IndexError
|
If there is no element at |
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
__contains__ ¶
Return whether item is in the list.
Checks the elements in position order 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
|
|
Complexity
- Time: one iteration, stopping at the first match
- Space: O(1)
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
__repr__ ¶
Return a string showing the class name and the elements in order.
Returns:
| Type | Description |
|---|---|
str
|
A string such as |
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
__reversed__ ¶
Iterate over the elements from the last position down to 0.
This is what the built-in reversed(a) calls. The list is not
changed; compare reverse, which some structures offer to rewire
themselves in place.
The default iterates forward once into a buffer, then yields the
buffer from its end. The buffer costs O(n) auxiliary space, and that
is a deliberate trade. The alternative, reading a[i] from the last
position down, needs no extra memory but is only as cheap as
indexing, which is quadratic on a linked list that walks to each
index. A linear default that every implementation can inherit beats
a constant-space default that some cannot afford. A structure that
can walk backward in O(1) space overrides this: an array reads by
index, and a doubly linked list follows its backward links.
The buffer is filled when iteration starts, not when reversed(a)
is called, and it is a snapshot: changes made to the list after the
first element has been yielded do not affect the rest.
Yields:
| Type | Description |
|---|---|
T
|
Each element, starting with the one at position |
Complexity
- Time: O(n), one iteration plus one pass over the buffer
- Space: O(n) for the buffer
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
reverse ¶
Reverse the order of the elements in place.
Afterwards the element that was last is at position 0, and so on.
Compare reversed(a), which visits the elements backward and changes
nothing.
The default is the array algorithm: swap the first element with the
last, the second with the second-to-last, and so on, meeting in the
middle after n // 2 swaps. It needs nothing but indexing, so on an
array it is O(n) with O(1) extra space. A linked list cannot index
cheaply, so it overrides this with an algorithm suited to links: a
singly linked list re-points every next at the node before it, and
a doubly linked list swaps prev and next on every node.
Complexity
- Time:
n // 2swaps, each two reads and two writes ofa[i]at that index's cost - Space: O(1)
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
append ¶
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 |
Complexity
- Time: the cost of
insertat indexlen(self) - Space: the cost of
insertat indexlen(self)
- Data Structures
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
prepend ¶
Add item to the front of the list.
Afterwards item is at position 0, every other element is one
position later, and len(self) is one greater.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
item
|
T
|
The element to add. |
required |
Complexity
- Time: the cost of
insertat index 0 - Space: the cost of
insertat index 0
- Data Structures
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
pop_front ¶
Remove and return the first element.
Returns:
| Type | Description |
|---|---|
T
|
The element that was at position 0. |
Raises:
| Type | Description |
|---|---|
IndexError
|
If the list is empty. |
Complexity
- Time: the cost of
popat index 0 - Space: the cost of
popat index 0
- Data Structures
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
pop_back ¶
Remove and return the last element.
Returns:
| Type | Description |
|---|---|
T
|
The element that was at position |
Raises:
| Type | Description |
|---|---|
IndexError
|
If the list is empty. |
Complexity
- Time: the cost of
popat indexlen(self) - 1 - Space: the cost of
popat indexlen(self) - 1
- Data Structures Doubly Linked List Working at the end
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
remove ¶
Remove the first element that matches item.
Only the first match, scanning from position 0, is removed. An
element matches if it is item or equals it, as in item in a.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
item
|
T
|
The value to remove. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no element matches |
Complexity
- Time: one iteration to find the match, plus the cost of
popat that index - Space: O(1)
- Data Structures Singly Linked List Finding an element
Source code in cs_survival_kit/data_structures/abstract_list.py
·
View on GitHub
List ¶
Bases: DynamicArray[T]
The default list: a DynamicArray under a name that says "just a list".
List adds nothing to DynamicArray; it is the name. Every operation
and every cost is DynamicArray's, and the full table is in its
docstring. The name exists so that code can say which it means: a
DynamicArray because it wants an array, or a List because it wants
the default. If a better general-purpose list is ever added to the kit,
this is the name that will point at it.
Complexity
The same as DynamicArray. The operations that make it the default:
| Operation | Time |
|---|---|
a[i], a[i] = x |
O(1) |
append |
O(1) amortized, O(n) worst |
pop_back |
O(1) |
len(a) |
O(1) |
| iteration | O(n) |
And the ones that are not its strength: prepend, pop_front and
insert or pop away from the end are O(n), because the elements
after the position shift. Constructing from items is O(n).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
Iterable[T]
|
Elements to add to the new list, in order. Defaults to empty. |
()
|
capacity
|
int
|
The number of slots to allocate up front, as for
|
4
|
growth
|
GrowthPolicy
|
The growth policy, as for |
doubling
|
Examples:
>>> from cs_survival_kit import List
>>> a = List([3, 1, 2])
>>> a
List([3, 1, 2])
>>> a.append(4)
>>> a[0], a[3], len(a)
(3, 4, 4)
>>> a.pop_back()
4
It is a DynamicArray, so anything written for one accepts it:
>>> from cs_survival_kit.data_structures import AbstractList, DynamicArray
>>> isinstance(a, DynamicArray), isinstance(a, AbstractList)
(True, True)
And it satisfies the sorting contract, like every list in the kit:
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
DoublyLinkedList ¶
Bases: AbstractList[T]
A generic sequence implemented as a doubly linked list.
Elements are stored in nodes connected by prev and next references.
The list maintains references to both the head and tail nodes as well as
its current size. This permits O(1) insertion and removal at either end
and O(1) length queries.
Indexed access requires traversal because nodes are not stored
contiguously. Traversal begins at the head or tail depending on which is
closer to the requested index, requiring
O(min(i, n - 1 - i)) time for index i and O(n) time in the worst case.
Compared with a singly linked list, the backward reference enables direct traversal toward the head and O(1) removal from the tail. The tradeoff is an additional reference per node and additional pointer maintenance. Compared with a dynamic array, this structure provides O(1) insertion and removal at either end without shifting elements, but does not provide O(1) random access and has greater per-element storage overhead.
Complexity
| Operation | Time | Space |
|---|---|---|
a[i], a[i] = x |
O(min(i, n - 1 - i)), O(n) worst | O(1) |
insert(i, x) |
O(min(i, n - i)), O(n) worst | O(1) |
pop(i) |
O(min(i, n - 1 - i)), O(n) worst | O(1) |
prepend |
O(1) | O(1) |
append |
O(1) | O(1) |
pop_front |
O(1) | O(1) |
pop_back |
O(1) | O(1) |
remove |
O(n) | O(1) |
reverse |
O(n) | O(1) |
len(a) |
O(1) | O(1) |
item in a |
O(n) | O(1) |
| iteration | O(n) | O(1) |
| reversed iteration | O(n) | O(1) |
The list requires O(n) total storage. Each element is stored in a separate node containing the element and two node references.
The space bounds above describe auxiliary space used by each operation, excluding storage for newly inserted nodes and iterator objects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
Iterable[T]
|
Elements used to initialize the list, in iteration order. |
()
|
Examples:
>>> values = DoublyLinkedList([1, 2, 3])
>>> list(values)
[1, 2, 3]
>>> list(reversed(values))
[3, 2, 1]
>>> values.insert(1, 4)
>>> list(values)
[1, 4, 2, 3]
- Data Structures Doubly Linked List
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
__len__ ¶
Return the number of elements in the list.
Returns:
| Type | Description |
|---|---|
int
|
The number of elements currently stored. |
Complexity
- Time: O(1)
- Space: O(1)
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
__iter__ ¶
Iterate over the elements from head to tail.
Yields:
| Type | Description |
|---|---|
T
|
Each element in list order. |
Complexity
- Time: O(n) for complete iteration
- Space: O(1) auxiliary space
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
__reversed__ ¶
Iterate over the elements from tail to head.
Each node stores a reference to its predecessor, allowing traversal backward from the tail without first reversing the list or repeatedly searching from the head. A singly linked list does not have these backward references.
Yields:
| Type | Description |
|---|---|
T
|
Each element in reverse list order. |
Complexity
- Time: O(n) for complete iteration
- Space: O(1) auxiliary space
Examples:
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
__getitem__ ¶
Return the element at the given index.
Traversal begins at whichever end of the list is closer to index.
This reduces the number of nodes visited for positions near the tail,
although indexed access remains O(n) in the worst case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Zero-based index of the element to retrieve. |
required |
Returns:
| Type | Description |
|---|---|
T
|
The element stored at |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(min(index, n - 1 - index)); O(n) worst case
- Space: O(1)
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
__setitem__ ¶
Replace the element at the given index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Zero-based index of the element to replace. |
required |
item
|
T
|
New element to store at |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(min(index, n - 1 - index)); O(n) worst case
- Space: O(1)
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
insert ¶
Insert an element at the given index.
Insertion at the head or one position past the tail requires no
traversal and runs in O(1) time. For an interior insertion, the node
currently at index is located by traversing from the nearer end.
Its prev reference identifies the predecessor, after which the four
links surrounding the new node are updated in O(1) time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Zero-based position at which to insert the element. An index equal to the current size appends the element. |
required |
item
|
T
|
Element to insert. |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(min(index, n - index)); O(n) worst case
- Space: O(1) auxiliary space
Examples:
- Data Structures Doubly Linked List Walking from the nearer end
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
pop ¶
Remove and return the element at the given index.
Removing the head or tail requires only pointer updates and therefore
runs in O(1) time. In particular, the tail's prev reference gives
direct access to the new tail, whereas a singly linked list must
traverse from the head to locate the tail's predecessor.
Removing an interior element first locates its node by traversing from
the nearer end. The node's prev and next references then provide
direct access to both neighbors so it can be unlinked in O(1) time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Zero-based index of the element to remove. |
required |
Returns:
| Type | Description |
|---|---|
T
|
The element removed from the list. |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(min(index, n - 1 - index)); O(n) worst case
- Space: O(1)
Examples:
- Data Structures Doubly Linked List Walking from the nearer end
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | |
reverse ¶
Reverse the list in place.
Each node's prev and next references are swapped. After a node's
links are swapped, its former next node is reachable through prev,
which allows traversal to continue through the original list order.
Once every node has been updated, the head and tail references are
swapped to complete the reversal.
Complexity
- Time: O(n)
- Space: O(1)
Examples:
- Data Structures Doubly Linked List Reversing
Source code in cs_survival_kit/data_structures/doubly_linked_list.py
·
View on GitHub
DynamicArray ¶
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 insert 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.
Elements sit at evenly spaced positions, so reaching any index is one
arithmetic step. The price is paid on insertion and removal anywhere but
the end: every later element has to shift by one slot to keep the block
contiguous. The inherited append and pop_back work at the end and
shift nothing; the inherited prepend and pop_front work at index 0 and
shift everything.
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 |
|---|---|---|
a[i], a[i] = x |
O(1) | O(1) |
insert |
O(n) amortized | O(1) amortized, O(n) worst |
pop |
O(n) | O(1) |
append |
O(1) amortized, O(n) worst | O(1) amortized, O(n) worst |
pop_back |
O(1) | O(1) |
prepend |
O(n) | O(1) amortized, O(n) worst |
pop_front |
O(n) | O(1) |
remove |
O(n) | O(1) |
len(a) |
O(1) | O(1) |
item in a |
O(n) | O(1) |
| iteration | O(n) | O(1) |
reversed(a) |
O(n) | O(1) |
reverse |
O(n) | O(1) |
| resize | O(n) | O(n) |
insert at index i shifts n - i elements, so it is O(1) at the
end and O(n) at the front; pop likewise. reverse is the inherited
default, swapping inward from both ends, which an array can do
because both ends are O(1) away. reversed(a) overrides the inherited
default, which buffers a forward pass in O(n) space, with a backward
walk by index: a[i] is O(1), so the buffer would buy nothing. The
amortized 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 |
|---|---|---|---|
items
|
Iterable[T]
|
Elements to add to the new array, in order. Defaults to empty. Each is appended, so the array grows by the policy as it fills. |
()
|
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
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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_back()
3
>>> a.insert(1, 15)
>>> list(a)
[10, 15, 2]
- Data Structures Dynamic Array
- Reference (cs-survival-kit 0.8.0) cs_survival_kit
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
capacity
property
¶
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 insert will trigger a resize.
Complexity
- Time: O(1)
- Space: O(1)
__len__ ¶
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 cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
__getitem__ ¶
Return the element at index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position of the element. Must satisfy
|
required |
Returns:
| Type | Description |
|---|---|
T
|
The element at |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(1)
- Space: O(1)
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
__setitem__ ¶
Replace the element at index with item.
Only overwrites an existing element. Use insert or append to add
one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position of the element to replace. Must satisfy
|
required |
item
|
T
|
The new element. |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(1)
- Space: O(1)
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
__iter__ ¶
Iterate over the elements from index 0 to len(self) - 1.
Unused slots are never visited, so None in a is True only if
None was actually stored.
Yields:
| Type | Description |
|---|---|
T
|
Each element, in index order. |
Complexity
- Time: O(n) to exhaust the iterator
- Space: O(1)
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
__reversed__ ¶
Iterate over the elements from index len(self) - 1 down to 0.
Overrides the inherited default, which buffers one forward pass in O(n) auxiliary space so that it stays linear on structures that cannot index cheaply. An array indexes in O(1), so it walks backward by index instead and needs no buffer.
Yields:
| Type | Description |
|---|---|
T
|
Each element, starting with the one at index |
Complexity
- Time: O(n) to exhaust the iterator
- Space: O(1)
Examples:
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
insert ¶
Add item at index, shifting every later element one slot right.
If the array is full, it first resizes to the capacity returned by the
growth policy, copying every element, and only then makes room.
Resizing only when there is no free slot means every allocated slot
gets used before the array grows. The shift then runs from the last
element down to index, so no element is overwritten before it has
been moved.
Inserting at len(self) shifts nothing, which is why the inherited
append is O(1) amortized. Inserting at 0 shifts every element, which
is why the inherited prepend is O(n).
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 |
|---|---|---|---|
index
|
int
|
The position the new element will occupy. Must satisfy
|
required |
item
|
T
|
The element to add. |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
ValueError
|
If the array is full and the growth policy returns a capacity that is not greater than the current capacity. |
Complexity
- Time: O(n - index) to shift, plus O(n) for the insert that triggers a resize; O(1) amortized at the end with a geometric growth policy
- Space: O(1) amortized; O(n) for the insert that triggers a resize
Examples:
>>> a = DynamicArray[str](capacity=2)
>>> a.append("x")
>>> a.append("z") # fills the last free slot; no resize yet
>>> a.capacity
2
>>> a.insert(1, "y") # no free slot, so the array grows first
>>> a, a.capacity
(DynamicArray(['x', 'y', 'z']), 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 cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | |
pop ¶
Remove and return the element at index, shifting later ones left.
The shift runs from index + 1 up to the last element, so each slot
is overwritten only after its element has moved. The slot freed at
the end is cleared, so the array no longer holds a reference to the
element. Capacity is unchanged.
Popping at len(self) - 1 shifts nothing, which is why the inherited
pop_back is O(1). Popping at 0 shifts every remaining element, which
is why the inherited pop_front is O(n).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position of the element to remove. Must satisfy
|
required |
Returns:
| Type | Description |
|---|---|
T
|
The element that was at |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(n - index) to shift; O(1) at the end
- Space: O(1)
Examples:
>>> a = DynamicArray[int]()
>>> for item in (1, 2, 3):
... a.append(item)
>>> a.pop(0)
1
>>> a
DynamicArray([2, 3])
>>> a.pop(2)
Traceback (most recent call last):
...
IndexError: index out of range
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
SinglyLinkedList ¶
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:
headmakesinsertandpopat index 0 O(1), and so the inheritedprependandpop_front.tailmakesinsertat indexlen(self)O(1), and so the inheritedappend. Without it, appending would mean walking the whole chain to find the last node.- The size count makes
lenO(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 means walking from the head to the
second-to-last node, so the inherited pop_back is O(n) even though the
tail reference finds the last node instantly. That limitation is what a
doubly linked list removes.
Compared with DynamicArray, adding or removing 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 |
|---|---|---|
a[i], a[i] = x |
O(n) | O(1) |
insert |
O(n) | O(1) |
pop |
O(n) | O(1) |
prepend |
O(1) | O(1) |
append |
O(1) | O(1) |
pop_front |
O(1) | O(1) |
pop_back |
O(n) | O(1) |
remove |
O(n) | O(1) |
reverse |
O(n) | O(1) |
len(a) |
O(1) | O(1) |
item in a |
O(n) | O(1) |
| iteration | O(n) | O(1) |
reversed(a) |
O(n) | O(n) |
insert and pop at index i follow O(i) links, so they are O(1)
at the front. insert is also O(1) at the end, thanks to the tail
reference. reversed(a) is the inherited default, which iterates
once into a buffer and yields it backward. The buffer is O(n) extra
space, and it is the price of linear time: nodes have no backward
link to follow, so reading each index from the last one down would
walk from the head every time and cost O(n²). 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]
- Data Structures Singly Linked List
Source code in cs_survival_kit/data_structures/singly_linked_list.py
·
View on GitHub
__len__ ¶
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 cs_survival_kit/data_structures/singly_linked_list.py
·
View on GitHub
__iter__ ¶
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 cs_survival_kit/data_structures/singly_linked_list.py
·
View on GitHub
__getitem__ ¶
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
|
required |
Returns:
| Type | Description |
|---|---|
T
|
The element at |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(n); O(index) links are followed
- Space: O(1)
Source code in cs_survival_kit/data_structures/singly_linked_list.py
·
View on GitHub
__setitem__ ¶
Replace the element at index with item.
Walks index links from the head, exactly as a[i] does, and
overwrites the element in the node it reaches. No node is added or
unlinked. Use insert to add an element.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position of the element to replace. Must satisfy
|
required |
item
|
T
|
The new element. |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(n); O(index) links are followed
- Space: O(1)
Source code in cs_survival_kit/data_structures/singly_linked_list.py
·
View on GitHub
insert ¶
Add item at index, after walking to the node before it.
A new node is linked in; no existing element moves. An array must
shift every element after index one slot to the right to make room,
which costs O(n) however far the walk is.
The walk is what costs. Three cases avoid it entirely:
index == 0: the new node links to the current head and becomes the new head. This is the inheritedprepend.index == len(self): the tail reference points straight at the last node, so the new node is linked after it and becomes the new tail. This is the inheritedappend.- Both at once, when the list is empty: the new node is both head and tail.
Anywhere else, the walk follows index - 1 links to the predecessor,
and the new node is linked between it and its successor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position the new element will occupy. Must satisfy
|
required |
item
|
T
|
The element to add. |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(n); O(index) links are followed, so O(1) at either end
- Space: O(1) for the new node
Examples:
>>> a = SinglyLinkedList[str](["a", "c"])
>>> a.insert(1, "b")
>>> a
SinglyLinkedList(['a', 'b', 'c'])
>>> a.prepend("_") # insert at 0: no walk
>>> a.append("d") # insert at len(a): no walk, thanks to the tail
>>> a
SinglyLinkedList(['_', 'a', 'b', 'c', 'd'])
Source code in cs_survival_kit/data_structures/singly_linked_list.py
·
View on GitHub
pop ¶
Remove and return the element at index, unlinking its node.
Unlinking a node means pointing its predecessor's next past it. At
index == 0 there is no predecessor: the head simply moves to the
second node, which is why the inherited pop_front is O(1). Anywhere
else, the walk follows index - 1 links to reach the predecessor,
because nodes carry no backward link.
That walk is why the inherited pop_back is O(n). The tail reference
finds the last node instantly, but unlinking it needs the node before
it, and only a walk from the head can find that. If the removed node
was the tail, the predecessor becomes the new tail. If it was the only
node, both references are cleared.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The position of the element to remove. Must satisfy
|
required |
Returns:
| Type | Description |
|---|---|
T
|
The element that was at |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
Complexity
- Time: O(n); O(index) links are followed, so O(1) at the front
- Space: O(1)
Examples:
>>> a = SinglyLinkedList[int]([1, 2, 3])
>>> a.pop(1)
2
>>> a.pop_front() # pop at 0: no walk
1
>>> a.pop_back() # pop at len(a) - 1: walks the whole chain
3
>>> a.pop(0)
Traceback (most recent call last):
...
IndexError: index out of range
Source code in cs_survival_kit/data_structures/singly_linked_list.py
·
View on GitHub
reverse ¶
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.
This overrides the inherited default, which swaps a[i] with its
mirror from both ends inward. That algorithm suits an array, where
every index is O(1) away, but here each index is a walk from the head,
so the default would be O(n²). Rewiring the links is O(n).
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])
- Data Structures Singly Linked List Reversing
Source code in cs_survival_kit/data_structures/singly_linked_list.py
·
View on GitHub
additive ¶
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
- Data Structures Dynamic Array Growth policies
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
doubling ¶
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:
- Data Structures Dynamic Array Growth policies
- Reference (cs-survival-kit 0.8.0) cs_survival_kit
Source code in cs_survival_kit/data_structures/dynamic_array.py
·
View on GitHub
geometric ¶
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
- Data Structures Dynamic Array Growth policies