Skip to content

Commit f203c61

Browse files
feat: add static and dynamic array implementations
Introduce StaticArray (fixed-capacity) and DynamicArray (auto-resizing) with full IArray interface including append, pop, insert, remove, and clear. Back ArrayStack with DynamicArray instead of built-in list. Remove main.py entry point.
1 parent 3c71519 commit f203c61

8 files changed

Lines changed: 623 additions & 13 deletions

File tree

README.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A collection of classic algorithms and data structures implemented in Python.
88

99
| Implementation | Description |
1010
|---|---|
11-
| `ArrayStack` | Stack backed by a dynamic array (`list`). O(1) amortised push/pop/peek. |
11+
| `ArrayStack` | Stack backed by a `DynamicArray`. O(1) amortised push/pop/peek. |
1212
| `MinStack` | Extends `ArrayStack` with O(1) `get_min()` tracking via an auxiliary stack. |
1313

1414
```python
@@ -29,6 +29,33 @@ ms.push(7)
2929
ms.get_min() # 3
3030
```
3131

32+
### Arrays
33+
34+
| Implementation | Description |
35+
|---|---|
36+
| `StaticArray` | Fixed-capacity array. O(1) access, O(n) insert/remove. Raises `OverflowError` when full. |
37+
| `DynamicArray` | Resizable array that doubles on overflow and halves when sparse. O(1) amortised append/pop. |
38+
39+
```python
40+
from data_structures.array.static_array import StaticArray
41+
from data_structures.array.dynamic_array import DynamicArray
42+
43+
sa = StaticArray(3)
44+
sa.append(1)
45+
sa.append(2)
46+
sa.insert(0, 0)
47+
list(sa) # [0, 1, 2]
48+
sa.pop() # 2
49+
sa.remove(0) # 0
50+
51+
da = DynamicArray()
52+
for i in range(10):
53+
da.append(i)
54+
len(da) # 10
55+
da.pop() # 9
56+
da.capacity # 16
57+
```
58+
3259
### Linked Lists
3360

3461
| Implementation | Description |
@@ -87,15 +114,20 @@ uv run pre-commit run --all-files
87114

88115
```
89116
data_structures/
117+
array/
118+
base.py # IArray ABC
119+
static_array.py # StaticArray implementation
120+
dynamic_array.py # DynamicArray implementation
90121
stack/
91-
base.py # Stack ABC and StackEmptyError
92-
array_stack.py # ArrayStack implementation
93-
min_stack.py # MinStack implementation
122+
base.py # IStack ABC and StackEmptyError
123+
array_stack.py # ArrayStack implementation
124+
min_stack.py # MinStack implementation
94125
linked_list/
95-
base.py # SinglyLinkedList & DoublyLinkedList ABCs
126+
base.py # ISinglyLinkedList & IDoublyLinkedList ABCs
96127
singly_linked_list.py # SinglyLinkedList
97128
doubly_linked_list.py # DoublyLinkedList
98129
tests/
130+
test_arrays.py # pytest suite for array implementations
99131
test_stacks.py # pytest suite for stack implementations
100132
test_linked_lists.py # pytest suite for linked list implementations
101133
.pre-commit-config.yaml # pre-commit hooks (ruff, ty)

data_structures/array/__init__.py

Whitespace-only changes.

data_structures/array/base.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Iterator
3+
4+
5+
class IArray[T](ABC):
6+
"""Abstract Array interface (random-access, indexed collection).
7+
8+
>>> a = StaticArray(4)
9+
>>> a.append(10); a.append(20); a.append(30)
10+
>>> a[1]
11+
20
12+
>>> a.remove(0)
13+
10
14+
>>> list(a)
15+
[20, 30]
16+
"""
17+
18+
@abstractmethod
19+
def append(self, value: T) -> None:
20+
"""Append *value* after the last element. O(1)."""
21+
22+
@abstractmethod
23+
def pop(self) -> T:
24+
"""Remove and return the last element. O(1)."""
25+
26+
@abstractmethod
27+
def remove(self, index: int) -> T:
28+
"""Remove and return the element at *index*, shifting successors left. O(n)."""
29+
30+
@abstractmethod
31+
def insert(self, index: int, value: T) -> None:
32+
"""Insert *value* at *index*, shifting successors right. O(n)."""
33+
34+
@abstractmethod
35+
def clear(self) -> None:
36+
"""Remove all elements. O(n)."""
37+
38+
@abstractmethod
39+
def is_empty(self) -> bool:
40+
"""Return ``True`` if the array contains no elements. O(1)."""
41+
42+
@abstractmethod
43+
def __getitem__(self, index: int) -> T:
44+
"""Return the element at *index*. O(1)."""
45+
46+
@abstractmethod
47+
def __setitem__(self, index: int, value: T) -> None:
48+
"""Replace the element at *index* with *value*. O(1)."""
49+
50+
@abstractmethod
51+
def __len__(self) -> int:
52+
"""Return the number of elements. O(1)."""
53+
54+
@abstractmethod
55+
def __iter__(self) -> Iterator[T]:
56+
"""Iterate over all elements from first to last."""
57+
58+
@abstractmethod
59+
def __contains__(self, item: object) -> bool:
60+
"""Return ``True`` if *item* is in the array. O(n)."""
61+
62+
@abstractmethod
63+
def __repr__(self) -> str:
64+
"""Return a string representation."""
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from typing import Iterator
2+
3+
from .base import IArray
4+
5+
_DEFAULT_CAPACITY = 4
6+
_GROWTH_FACTOR = 2
7+
_SHRINK_THRESHOLD = 4 # shrink when size <= capacity // SHRINK_THRESHOLD
8+
9+
10+
class DynamicArray[T](IArray[T]):
11+
"""Resizable array that doubles capacity when full and halves when sparse.
12+
13+
Starts with a default capacity of 4. Grows by a factor of 2 on overflow
14+
and shrinks by half when the size drops to a quarter of the capacity.
15+
"""
16+
17+
def __init__(self) -> None:
18+
"""Create an empty dynamic array."""
19+
self._capacity: int = _DEFAULT_CAPACITY
20+
self._size: int = 0
21+
self._data: list[T | None] = [None] * self._capacity
22+
23+
def append(self, value: T) -> None:
24+
"""Append *value* after the last element. O(1) amortised."""
25+
if self._size >= self._capacity:
26+
self._resize(self._capacity * _GROWTH_FACTOR)
27+
self._data[self._size] = value
28+
self._size += 1
29+
30+
def insert(self, index: int, value: T) -> None:
31+
"""Insert *value* at *index*, shifting successors right. O(n).
32+
33+
Allows *index* equal to size (equivalent to ``append``).
34+
"""
35+
if index < 0:
36+
index = self._size + index
37+
if not 0 <= index <= self._size:
38+
raise IndexError("Index out of range")
39+
if self._size >= self._capacity:
40+
self._resize(self._capacity * _GROWTH_FACTOR)
41+
for i in range(self._size, index, -1):
42+
self._data[i] = self._data[i - 1]
43+
self._data[index] = value
44+
self._size += 1
45+
46+
def pop(self) -> T:
47+
"""Remove and return the last element. O(1) amortised."""
48+
if self._size == 0:
49+
raise IndexError("Pop from empty array")
50+
self._size -= 1
51+
value = self._data[self._size]
52+
self._data[self._size] = None
53+
self._try_shrink()
54+
return value # type: ignore[return-value]
55+
56+
def clear(self) -> None:
57+
"""Remove all elements and reset capacity. O(1)."""
58+
self._capacity = _DEFAULT_CAPACITY
59+
self._data = [None] * self._capacity
60+
self._size = 0
61+
62+
def remove(self, index: int) -> T:
63+
"""Remove and return the element at *index*, shifting successors left. O(n)."""
64+
index = self._normalize_index(index)
65+
value = self._data[index]
66+
for i in range(index, self._size - 1):
67+
self._data[i] = self._data[i + 1]
68+
self._size -= 1
69+
self._data[self._size] = None
70+
self._try_shrink()
71+
return value # type: ignore[return-value]
72+
73+
def is_empty(self) -> bool:
74+
"""Return ``True`` if the array contains no elements. O(1)."""
75+
return self._size == 0
76+
77+
@property
78+
def capacity(self) -> int:
79+
"""Return the current internal capacity."""
80+
return self._capacity
81+
82+
def __getitem__(self, index: int) -> T:
83+
"""Return the element at *index*. O(1)."""
84+
index = self._normalize_index(index)
85+
return self._data[index] # type: ignore[return-value]
86+
87+
def __setitem__(self, index: int, value: T) -> None:
88+
"""Replace the element at *index* with *value*. O(1)."""
89+
index = self._normalize_index(index)
90+
self._data[index] = value
91+
92+
def __len__(self) -> int:
93+
"""Return the number of elements. O(1)."""
94+
return self._size
95+
96+
def __iter__(self) -> Iterator[T]:
97+
"""Iterate over all elements from first to last."""
98+
for i in range(self._size):
99+
yield self._data[i] # type: ignore[misc]
100+
101+
def __contains__(self, item: object) -> bool:
102+
"""Return ``True`` if *item* is in the array. O(n)."""
103+
return any(v == item for v in self)
104+
105+
def __repr__(self) -> str:
106+
"""Return a string representation."""
107+
return f"{type(self).__name__}({list(self)})"
108+
109+
def _normalize_index(self, index: int) -> int:
110+
"""Translate negative indices and validate bounds."""
111+
if index < 0:
112+
index = self._size + index
113+
if not 0 <= index < self._size:
114+
raise IndexError("Index out of range")
115+
return index
116+
117+
def _resize(self, new_capacity: int) -> None:
118+
"""Allocate a new backing list and copy elements over."""
119+
new_data: list[T | None] = [None] * new_capacity
120+
for i in range(self._size):
121+
new_data[i] = self._data[i]
122+
self._data = new_data
123+
self._capacity = new_capacity
124+
125+
def _try_shrink(self) -> None:
126+
"""Halve capacity when size drops to a quarter, but never below default."""
127+
new_capacity = self._capacity // _GROWTH_FACTOR
128+
if (
129+
new_capacity >= _DEFAULT_CAPACITY
130+
and self._size <= self._capacity // _SHRINK_THRESHOLD
131+
):
132+
self._resize(new_capacity)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from typing import Iterator
2+
3+
from .base import IArray
4+
5+
6+
class StaticArray[T](IArray[T]):
7+
"""Fixed-capacity array backed by a Python list.
8+
9+
Capacity is set at construction time and cannot grow.
10+
Raises ``OverflowError`` when appending to a full array.
11+
"""
12+
13+
def __init__(self, capacity: int) -> None:
14+
"""Create an empty array that can hold at most *capacity* elements."""
15+
if capacity <= 0:
16+
raise ValueError("Capacity must be positive")
17+
self._capacity: int = capacity
18+
self._size: int = 0
19+
self._data: list[T | None] = [None] * capacity
20+
21+
def append(self, value: T) -> None:
22+
"""Append *value* after the last element. O(1).
23+
24+
Raises ``OverflowError`` if the array is already at capacity.
25+
"""
26+
if self._size >= self._capacity:
27+
raise OverflowError("Static array is full")
28+
self._data[self._size] = value
29+
self._size += 1
30+
31+
def insert(self, index: int, value: T) -> None:
32+
"""Insert *value* at *index*, shifting successors right. O(n).
33+
34+
Allows *index* equal to size (equivalent to ``append``).
35+
Raises ``OverflowError`` if the array is already at capacity.
36+
"""
37+
if self._size >= self._capacity:
38+
raise OverflowError("Static array is full")
39+
if index < 0:
40+
index = self._size + index
41+
if not 0 <= index <= self._size:
42+
raise IndexError("Index out of range")
43+
for i in range(self._size, index, -1):
44+
self._data[i] = self._data[i - 1]
45+
self._data[index] = value
46+
self._size += 1
47+
48+
def pop(self) -> T:
49+
"""Remove and return the last element. O(1)."""
50+
if self._size == 0:
51+
raise IndexError("Pop from empty array")
52+
self._size -= 1
53+
value = self._data[self._size]
54+
self._data[self._size] = None
55+
return value # type: ignore[return-value]
56+
57+
def clear(self) -> None:
58+
"""Remove all elements. O(n)."""
59+
for i in range(self._size):
60+
self._data[i] = None
61+
self._size = 0
62+
63+
def remove(self, index: int) -> T:
64+
"""Remove and return the element at *index*, shifting successors left. O(n)."""
65+
index = self._normalize_index(index)
66+
value = self._data[index]
67+
for i in range(index, self._size - 1):
68+
self._data[i] = self._data[i + 1]
69+
self._size -= 1
70+
self._data[self._size] = None
71+
return value # type: ignore[return-value]
72+
73+
def is_empty(self) -> bool:
74+
"""Return ``True`` if the array contains no elements. O(1)."""
75+
return self._size == 0
76+
77+
@property
78+
def capacity(self) -> int:
79+
"""Return the maximum number of elements the array can hold."""
80+
return self._capacity
81+
82+
def __getitem__(self, index: int) -> T:
83+
"""Return the element at *index*. O(1)."""
84+
index = self._normalize_index(index)
85+
return self._data[index] # type: ignore[return-value]
86+
87+
def __setitem__(self, index: int, value: T) -> None:
88+
"""Replace the element at *index* with *value*. O(1)."""
89+
index = self._normalize_index(index)
90+
self._data[index] = value
91+
92+
def __len__(self) -> int:
93+
"""Return the number of elements. O(1)."""
94+
return self._size
95+
96+
def __iter__(self) -> Iterator[T]:
97+
"""Iterate over all elements from first to last."""
98+
for i in range(self._size):
99+
yield self._data[i] # type: ignore[misc]
100+
101+
def __contains__(self, item: object) -> bool:
102+
"""Return ``True`` if *item* is in the array. O(n)."""
103+
return any(v == item for v in self)
104+
105+
def __repr__(self) -> str:
106+
"""Return a string representation."""
107+
return f"{type(self).__name__}({list(self)})"
108+
109+
def _normalize_index(self, index: int) -> int:
110+
"""Translate negative indices and validate bounds."""
111+
if index < 0:
112+
index = self._size + index
113+
if not 0 <= index < self._size:
114+
raise IndexError("Index out of range")
115+
return index

data_structures/stack/array_stack.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
from data_structures.array.dynamic_array import DynamicArray
2+
13
from .base import IStack, StackEmptyError
24

35

46
class ArrayStack[T](IStack):
5-
"""Stack backed by a dynamic array (``list``)."""
7+
"""Stack backed by a ``DynamicArray``."""
68

79
def __init__(self) -> None:
8-
self._items: list[T] = []
10+
self._items: DynamicArray[T] = DynamicArray()
911

1012
def push(self, item: T) -> None:
1113
self._items.append(item)

0 commit comments

Comments
 (0)