Dynamic array in Python
Array that reallocates and copies when full, giving amortized O(1) append.
- File
dynamic_array.py, 71 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dynamic-array/. The authors are listed in prov/9C8NGV.jsonl.
from typing import Generic, Iterator, TypeVar
T = TypeVar("T")
class DynamicArray(Generic[T]):
"""An array that doubles when full and halves when a quarter full.
get and set cost O(1) in the worst case. append costs O(1) amortized
(at most 3 units) and pop O(1) amortized (at most 2 units), counting
one unit per item written, read or copied. The capacity is a power of
two and is less than 4 * max(n, 1).
"""
def __init__(self) -> None:
self.cap = 1
self.n = 0
self.a: list[T | None] = [None]
self.copies = 0 # the number of executions of line L10, for tests
def __len__(self) -> int:
return self.n
def get(self, i: int) -> T:
"""Return the item at index i. Precondition: 0 <= i < n. Cost O(1)."""
if not 0 <= i < self.n:
raise IndexError("index out of range")
return self.a[i] # type: ignore[return-value]
def set(self, i: int, x: T) -> None:
"""Replace the item at index i. Precondition: 0 <= i < n. Cost O(1)."""
if not 0 <= i < self.n:
raise IndexError("index out of range")
self.a[i] = x
def append(self, x: T) -> None:
"""Add x at the end. Cost O(1) amortized, O(n) worst case."""
if self.n == self.cap: # @line L1
self._resize(2 * self.cap)
self.a[self.n] = x # @line L2
self.n += 1 # @line L3
def pop(self) -> T:
"""Remove and return the last item. Precondition: n > 0. Cost O(1) amortized."""
if self.n == 0:
raise IndexError("pop from an empty array")
self.n -= 1 # @line L4
x = self.a[self.n] # @line L5
self.a[self.n] = None # drop the reference so that the item can be freed
if self.n > 0 and 4 * self.n == self.cap: # @line L6
self._resize(self.cap // 2)
return x # type: ignore[return-value] # @line L7
def _resize(self, c: int) -> None:
b: list[T | None] = [None] * c # @line L8
for i in range(self.n): # @line L9
b[i] = self.a[i] # @line L10
self.copies += 1
self.a, self.cap = b, c # @line L11
def __iter__(self) -> Iterator[T]:
for i in range(self.n):
yield self.a[i] # type: ignore[misc]
def check_invariant(self) -> None:
assert 0 <= self.n <= self.cap == len(self.a)
assert self.cap & (self.cap - 1) == 0
assert self.cap < 4 * max(self.n, 1)
test_dynamic_array.py 95 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dynamic-array/. The authors are listed in prov/9C8NGV.jsonl.
import random
import pytest
from dynamic_array import DynamicArray
def phi(n: int, cap: int) -> float:
return 2 * n - cap if 2 * n >= cap else cap / 2 - n
def test_page_example() -> None:
d: DynamicArray[int] = DynamicArray()
rows = []
for x in [1, 2, 3, 4, 5]:
c0 = d.copies
d.append(x)
rows.append((d.n, d.cap, d.copies - c0))
for want in [5, 4, 3, 2, 1]:
c0 = d.copies
assert d.pop() == want
rows.append((d.n, d.cap, d.copies - c0))
assert rows == [
(1, 1, 0), (2, 2, 1), (3, 4, 2), (4, 4, 0), (5, 8, 4),
(4, 8, 0), (3, 8, 0), (2, 4, 2), (1, 2, 1), (0, 2, 0),
]
assert d.copies == 10
def test_edges() -> None:
d: DynamicArray[int] = DynamicArray()
with pytest.raises(IndexError):
d.pop()
with pytest.raises(IndexError):
d.get(0)
d.append(7)
d.set(0, 8)
assert d.get(0) == 8 and d.cap == 1
assert d.pop() == 8 and d.cap == 1
def test_no_thrashing_at_boundary() -> None:
d: DynamicArray[int] = DynamicArray()
for x in range(64):
d.append(x)
assert d.cap == 64
c0 = d.copies
for _ in range(100):
d.append(0)
d.pop()
assert d.copies - c0 == 64 # only the first append resizes
def test_amortized_bound() -> None:
rng = random.Random(77)
d: DynamicArray[int] = DynamicArray()
total = 0
m = 0
for _ in range(5000):
before = phi(d.n, d.cap)
c0 = d.copies
if d.n and rng.random() < 0.45:
d.pop()
bound = 2
else:
d.append(1)
bound = 3
cost = 1 + d.copies - c0
assert cost + phi(d.n, d.cap) - before <= bound
total += cost
m += 1
assert total <= 3 * m + 0.5
def test_random_against_list() -> None:
rng = random.Random(9821)
for _ in range(500):
d: DynamicArray[int] = DynamicArray()
model: list[int] = []
for _ in range(60):
op = rng.randrange(3)
if op == 0:
x = rng.randint(0, 9)
d.append(x)
model.append(x)
elif op == 1 and model:
assert d.pop() == model.pop()
elif model:
i = rng.randrange(len(model))
assert d.get(i) == model[i]
d.check_invariant()
assert list(d) == model
What the next level needs
- the tests did not run in this build