Skip to content

Commit 3d6fee1

Browse files
feat: add binary search algorithm and iterable array constructors
Implement binary_search, lower_bound, and upper_bound for any indexable collection (list, StaticArray, DynamicArray). Add iterable constructors to both array types for convenient initialization. Add algorithms package to coverage config.
1 parent 6a895da commit 3d6fee1

8 files changed

Lines changed: 296 additions & 15 deletions

File tree

algorithms/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

algorithms/searching/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import Any
2+
3+
4+
def binary_search(arr: Any, target: Any) -> int:
5+
"""Return the index of *target* in a sorted sequence, or -1 if not found.
6+
7+
Time: O(log n). Space: O(1).
8+
9+
Accepts any indexable collection with ``__getitem__`` and ``__len__``
10+
(``list``, ``StaticArray``, ``DynamicArray``, etc.).
11+
The input must be sorted in ascending order.
12+
"""
13+
low = 0
14+
high = len(arr) - 1
15+
16+
while low <= high:
17+
mid = low + (high - low) // 2
18+
if arr[mid] == target:
19+
return mid
20+
if arr[mid] < target:
21+
low = mid + 1
22+
else:
23+
high = mid - 1
24+
25+
return -1
26+
27+
28+
def lower_bound(arr: Any, target: Any) -> int:
29+
"""Return the index of the first element >= *target*.
30+
31+
Time: O(log n). Space: O(1).
32+
33+
Returns ``len(arr)`` if all elements are less than *target*.
34+
"""
35+
low = 0
36+
high = len(arr)
37+
38+
while low < high:
39+
mid = low + (high - low) // 2
40+
if arr[mid] < target:
41+
low = mid + 1
42+
else:
43+
high = mid
44+
45+
return low
46+
47+
48+
def upper_bound(arr: Any, target: Any) -> int:
49+
"""Return the index of the first element > *target*.
50+
51+
Time: O(log n). Space: O(1).
52+
53+
Returns ``len(arr)`` if no element is greater than *target*.
54+
"""
55+
low = 0
56+
high = len(arr)
57+
58+
while low < high:
59+
mid = low + (high - low) // 2
60+
if arr[mid] <= target:
61+
low = mid + 1
62+
else:
63+
high = mid
64+
65+
return low

data_structures/array/dynamic_array.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Iterable
12
from typing import Iterator
23

34
from .base import IArray
@@ -17,11 +18,23 @@ class DynamicArray[T](IArray[T]):
1718
elements between shrink thresholds.
1819
"""
1920

20-
def __init__(self) -> None:
21-
"""Create an empty dynamic array."""
22-
self._capacity: int = _DEFAULT_CAPACITY
23-
self._size: int = 0
24-
self._data: list[T | None] = [None] * self._capacity
21+
def __init__(self, values: Iterable[T] | None = None) -> None:
22+
"""Create a dynamic array, optionally pre-filled from *values*.
23+
24+
>>> DynamicArray() # empty
25+
DynamicArray([])
26+
>>> DynamicArray([1, 2, 3]) # pre-filled
27+
DynamicArray([1, 2, 3])
28+
"""
29+
if values is None:
30+
self._capacity: int = _DEFAULT_CAPACITY
31+
self._size: int = 0
32+
self._data: list[T | None] = [None] * self._capacity
33+
else:
34+
items = list(values)
35+
self._size = len(items)
36+
self._capacity = max(_DEFAULT_CAPACITY, self._size)
37+
self._data = items + [None] * (self._capacity - self._size)
2538

2639
def append(self, value: T) -> None:
2740
"""Append *value* after the last element. O(1) amortised."""
@@ -99,7 +112,7 @@ def __len__(self) -> int:
99112
def __iter__(self) -> Iterator[T]:
100113
"""Iterate over all elements from first to last."""
101114
for i in range(self._size):
102-
yield self._data[i] # type: ignore[misc]
115+
yield self._data[i]
103116

104117
def __contains__(self, item: object) -> bool:
105118
"""Return ``True`` if *item* is in the array. O(n)."""

data_structures/array/static_array.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Iterable
12
from typing import Iterator
23

34
from .base import IArray
@@ -8,15 +9,32 @@ class StaticArray[T](IArray[T]):
89
910
Capacity is set at construction time and cannot grow.
1011
Raises ``OverflowError`` when appending to a full array.
12+
13+
>>> StaticArray(3) # empty with capacity 3
14+
StaticArray([])
15+
>>> StaticArray([1, 2, 3]) # pre-filled, capacity == len
16+
StaticArray([1, 2, 3])
1117
"""
1218

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
19+
def __init__(self, capacity_or_values: int | Iterable[T]) -> None:
20+
"""Create a static array.
21+
22+
Pass an ``int`` for an empty array with that capacity, or an
23+
iterable to pre-fill (capacity equals the number of elements).
24+
"""
25+
if isinstance(capacity_or_values, int):
26+
if capacity_or_values <= 0:
27+
raise ValueError("Capacity must be positive")
28+
self._capacity: int = capacity_or_values
29+
self._size: int = 0
30+
self._data: list[T | None] = [None] * self._capacity
31+
else:
32+
values = list(capacity_or_values)
33+
if not values:
34+
raise ValueError("Capacity must be positive")
35+
self._capacity = len(values)
36+
self._size = len(values)
37+
self._data = list(values)
2038

2139
def append(self, value: T) -> None:
2240
"""Append *value* after the last element. O(1).
@@ -96,7 +114,7 @@ def __len__(self) -> int:
96114
def __iter__(self) -> Iterator[T]:
97115
"""Iterate over all elements from first to last."""
98116
for i in range(self._size):
99-
yield self._data[i] # type: ignore[misc]
117+
yield self._data[i]
100118

101119
def __contains__(self, item: object) -> bool:
102120
"""Return ``True`` if *item* is in the array. O(n)."""

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ select = ["E", "F", "W", "I"]
2323

2424
[tool.pytest.ini_options]
2525
pythonpath = ["."]
26-
addopts = "--cov=data_structures --cov-report=term-missing"
26+
addopts = "--cov=data_structures --cov=algorithms --cov-report=term-missing"

tests/test_arrays.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,24 @@ def test_static_repr_empty():
201201
assert repr(StaticArray(5)) == "StaticArray([])"
202202

203203

204+
def test_static_init_from_list():
205+
arr = StaticArray([10, 20, 30])
206+
assert list(arr) == [10, 20, 30]
207+
assert arr.capacity == 3
208+
assert len(arr) == 3
209+
210+
211+
def test_static_init_from_iterable():
212+
arr = StaticArray(range(4))
213+
assert list(arr) == [0, 1, 2, 3]
214+
assert arr.capacity == 4
215+
216+
217+
def test_static_init_empty_iterable_raises():
218+
with pytest.raises(ValueError):
219+
StaticArray([])
220+
221+
204222
# ── DynamicArray-specific tests ──────────────────────────────────────
205223

206224

@@ -269,3 +287,26 @@ def test_dynamic_repr():
269287

270288
def test_dynamic_repr_empty():
271289
assert repr(DynamicArray()) == "DynamicArray([])"
290+
291+
292+
def test_dynamic_init_from_list():
293+
arr = DynamicArray([10, 20, 30])
294+
assert list(arr) == [10, 20, 30]
295+
assert len(arr) == 3
296+
297+
298+
def test_dynamic_init_from_iterable():
299+
arr = DynamicArray(range(4))
300+
assert list(arr) == [0, 1, 2, 3]
301+
302+
303+
def test_dynamic_init_empty_iterable():
304+
arr = DynamicArray([])
305+
assert arr.is_empty()
306+
assert arr.capacity == 4
307+
308+
309+
def test_dynamic_init_large_sets_capacity():
310+
arr = DynamicArray(range(10))
311+
assert arr.capacity >= 10
312+
assert len(arr) == 10

tests/test_binary_search.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from algorithms.searching.binary_search import (
2+
binary_search,
3+
lower_bound,
4+
upper_bound,
5+
)
6+
from data_structures.array.dynamic_array import DynamicArray
7+
from data_structures.array.static_array import StaticArray
8+
9+
# ── binary_search ────────────────────────────────────────────────────
10+
11+
12+
def test_found_in_odd_length():
13+
assert binary_search([1, 3, 5, 7, 9], 5) == 2
14+
15+
16+
def test_found_in_even_length():
17+
assert binary_search([2, 4, 6, 8], 6) == 2
18+
19+
20+
def test_found_first_element():
21+
assert binary_search([1, 2, 3], 1) == 0
22+
23+
24+
def test_found_last_element():
25+
assert binary_search([1, 2, 3], 3) == 2
26+
27+
28+
def test_not_found():
29+
assert binary_search([1, 2, 3, 4, 5], 42) == -1
30+
31+
32+
def test_empty_list():
33+
assert binary_search([], 1) == -1
34+
35+
36+
def test_single_element_found():
37+
assert binary_search([7], 7) == 0
38+
39+
40+
def test_single_element_not_found():
41+
assert binary_search([7], 3) == -1
42+
43+
44+
def test_duplicates_returns_one_of_them():
45+
result = binary_search([1, 3, 3, 3, 5], 3)
46+
assert result in (1, 2, 3)
47+
48+
49+
def test_strings():
50+
assert binary_search(["a", "b", "c", "d"], "c") == 2
51+
52+
53+
def test_large_list():
54+
arr = list(range(0, 10000, 2)) # [0, 2, 4, ..., 9998]
55+
assert binary_search(arr, 4444) == 2222
56+
assert binary_search(arr, 4445) == -1
57+
58+
59+
# ── lower_bound ──────────────────────────────────────────────────────
60+
61+
62+
def test_lower_bound_exact():
63+
assert lower_bound([1, 3, 5, 7], 3) == 1
64+
65+
66+
def test_lower_bound_between():
67+
assert lower_bound([1, 3, 5, 7], 4) == 2
68+
69+
70+
def test_lower_bound_less_than_all():
71+
assert lower_bound([2, 4, 6], 0) == 0
72+
73+
74+
def test_lower_bound_greater_than_all():
75+
assert lower_bound([2, 4, 6], 10) == 3
76+
77+
78+
def test_lower_bound_duplicates():
79+
assert lower_bound([1, 3, 3, 3, 5], 3) == 1
80+
81+
82+
def test_lower_bound_empty():
83+
assert lower_bound([], 1) == 0
84+
85+
86+
# ── upper_bound ──────────────────────────────────────────────────────
87+
88+
89+
def test_upper_bound_exact():
90+
assert upper_bound([1, 3, 5, 7], 3) == 2
91+
92+
93+
def test_upper_bound_between():
94+
assert upper_bound([1, 3, 5, 7], 4) == 2
95+
96+
97+
def test_upper_bound_less_than_all():
98+
assert upper_bound([2, 4, 6], 0) == 0
99+
100+
101+
def test_upper_bound_greater_than_all():
102+
assert upper_bound([2, 4, 6], 10) == 3
103+
104+
105+
def test_upper_bound_duplicates():
106+
assert upper_bound([1, 3, 3, 3, 5], 3) == 4
107+
108+
109+
def test_upper_bound_empty():
110+
assert upper_bound([], 1) == 0
111+
112+
113+
# ── StaticArray ──────────────────────────────────────────────────────
114+
115+
116+
def test_binary_search_static_array():
117+
assert binary_search(StaticArray([1, 3, 5, 7, 9]), 5) == 2
118+
assert binary_search(StaticArray([1, 3, 5, 7, 9]), 42) == -1
119+
120+
121+
def test_lower_bound_static_array():
122+
assert lower_bound(StaticArray([1, 3, 5, 7]), 4) == 2
123+
124+
125+
def test_upper_bound_static_array():
126+
assert upper_bound(StaticArray([1, 3, 3, 3, 5]), 3) == 4
127+
128+
129+
# ── DynamicArray ─────────────────────────────────────────────────────
130+
131+
132+
def test_binary_search_dynamic_array():
133+
assert binary_search(DynamicArray([1, 3, 5, 7, 9]), 5) == 2
134+
assert binary_search(DynamicArray([1, 3, 5, 7, 9]), 42) == -1
135+
136+
137+
def test_lower_bound_dynamic_array():
138+
assert lower_bound(DynamicArray([1, 3, 5, 7]), 4) == 2
139+
140+
141+
def test_upper_bound_dynamic_array():
142+
assert upper_bound(DynamicArray([1, 3, 3, 3, 5]), 3) == 4

0 commit comments

Comments
 (0)