Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1307414

Browse files
committedJun 3, 2024
Refactor and optimize binary search algorithms,and
streamline interactive and benchmarking sections.
1 parent b1b4caf commit 1307414

File tree

1 file changed

+65
-296
lines changed

1 file changed

+65
-296
lines changed
 

‎searches/binary_search.py

Lines changed: 65 additions & 296 deletions
Original file line numberDiff line numberDiff line change
@@ -1,359 +1,128 @@
11
#!/usr/bin/env python3
22

3-
"""
4-
Pure Python implementations of binary search algorithms
3+
from bisect import bisect_left, bisect_right
54

6-
For doctests run the following command:
7-
python3 -m doctest -v binary_search.py
8-
9-
For manual testing run:
10-
python3 binary_search.py
11-
"""
12-
13-
from __future__ import annotations
14-
15-
import bisect
16-
17-
18-
def bisect_left(
19-
sorted_collection: list[int], item: int, lo: int = 0, hi: int = -1
20-
) -> int:
5+
def bisect_left_custom(sorted_collection, item, lo=0, hi=None):
216
"""
22-
Locates the first element in a sorted array that is larger or equal to a given
23-
value.
24-
25-
It has the same interface as
26-
https://docs.python.org/3/library/bisect.html#bisect.bisect_left .
27-
28-
:param sorted_collection: some ascending sorted collection with comparable items
29-
:param item: item to bisect
30-
:param lo: lowest index to consider (as in sorted_collection[lo:hi])
31-
:param hi: past the highest index to consider (as in sorted_collection[lo:hi])
32-
:return: index i such that all values in sorted_collection[lo:i] are < item and all
33-
values in sorted_collection[i:hi] are >= item.
34-
35-
Examples:
36-
>>> bisect_left([0, 5, 7, 10, 15], 0)
37-
0
38-
>>> bisect_left([0, 5, 7, 10, 15], 6)
39-
2
40-
>>> bisect_left([0, 5, 7, 10, 15], 20)
41-
5
42-
>>> bisect_left([0, 5, 7, 10, 15], 15, 1, 3)
43-
3
44-
>>> bisect_left([0, 5, 7, 10, 15], 6, 2)
45-
2
7+
Custom implementation of bisect_left.
8+
Finds the position to insert item so that the list remains sorted.
469
"""
47-
if hi < 0:
10+
if hi is None:
4811
hi = len(sorted_collection)
49-
5012
while lo < hi:
51-
mid = lo + (hi - lo) // 2
13+
mid = (lo + hi) // 2
5214
if sorted_collection[mid] < item:
5315
lo = mid + 1
5416
else:
5517
hi = mid
56-
5718
return lo
5819

59-
60-
def bisect_right(
61-
sorted_collection: list[int], item: int, lo: int = 0, hi: int = -1
62-
) -> int:
20+
def bisect_right_custom(sorted_collection, item, lo=0, hi=None):
6321
"""
64-
Locates the first element in a sorted array that is larger than a given value.
65-
66-
It has the same interface as
67-
https://docs.python.org/3/library/bisect.html#bisect.bisect_right .
68-
69-
:param sorted_collection: some ascending sorted collection with comparable items
70-
:param item: item to bisect
71-
:param lo: lowest index to consider (as in sorted_collection[lo:hi])
72-
:param hi: past the highest index to consider (as in sorted_collection[lo:hi])
73-
:return: index i such that all values in sorted_collection[lo:i] are <= item and
74-
all values in sorted_collection[i:hi] are > item.
75-
76-
Examples:
77-
>>> bisect_right([0, 5, 7, 10, 15], 0)
78-
1
79-
>>> bisect_right([0, 5, 7, 10, 15], 15)
80-
5
81-
>>> bisect_right([0, 5, 7, 10, 15], 6)
82-
2
83-
>>> bisect_right([0, 5, 7, 10, 15], 15, 1, 3)
84-
3
85-
>>> bisect_right([0, 5, 7, 10, 15], 6, 2)
86-
2
22+
Custom implementation of bisect_right.
23+
Finds the position to insert item so that the list remains sorted.
8724
"""
88-
if hi < 0:
25+
if hi is None:
8926
hi = len(sorted_collection)
90-
9127
while lo < hi:
92-
mid = lo + (hi - lo) // 2
28+
mid = (lo + hi) // 2
9329
if sorted_collection[mid] <= item:
9430
lo = mid + 1
9531
else:
9632
hi = mid
97-
9833
return lo
9934

100-
101-
def insort_left(
102-
sorted_collection: list[int], item: int, lo: int = 0, hi: int = -1
103-
) -> None:
35+
def insort_left_custom(sorted_collection, item, lo=0, hi=None):
10436
"""
105-
Inserts a given value into a sorted array before other values with the same value.
106-
107-
It has the same interface as
108-
https://docs.python.org/3/library/bisect.html#bisect.insort_left .
109-
110-
:param sorted_collection: some ascending sorted collection with comparable items
111-
:param item: item to insert
112-
:param lo: lowest index to consider (as in sorted_collection[lo:hi])
113-
:param hi: past the highest index to consider (as in sorted_collection[lo:hi])
114-
115-
Examples:
116-
>>> sorted_collection = [0, 5, 7, 10, 15]
117-
>>> insort_left(sorted_collection, 6)
118-
>>> sorted_collection
119-
[0, 5, 6, 7, 10, 15]
120-
>>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)]
121-
>>> item = (5, 5)
122-
>>> insort_left(sorted_collection, item)
123-
>>> sorted_collection
124-
[(0, 0), (5, 5), (5, 5), (7, 7), (10, 10), (15, 15)]
125-
>>> item is sorted_collection[1]
126-
True
127-
>>> item is sorted_collection[2]
128-
False
129-
>>> sorted_collection = [0, 5, 7, 10, 15]
130-
>>> insort_left(sorted_collection, 20)
131-
>>> sorted_collection
132-
[0, 5, 7, 10, 15, 20]
133-
>>> sorted_collection = [0, 5, 7, 10, 15]
134-
>>> insort_left(sorted_collection, 15, 1, 3)
135-
>>> sorted_collection
136-
[0, 5, 7, 15, 10, 15]
37+
Inserts item into sorted_collection in sorted order (using bisect_left_custom).
13738
"""
138-
sorted_collection.insert(bisect_left(sorted_collection, item, lo, hi), item)
139-
39+
sorted_collection.insert(bisect_left_custom(sorted_collection, item, lo, hi), item)
14040

141-
def insort_right(
142-
sorted_collection: list[int], item: int, lo: int = 0, hi: int = -1
143-
) -> None:
41+
def insort_right_custom(sorted_collection, item, lo=0, hi=None):
14442
"""
145-
Inserts a given value into a sorted array after other values with the same value.
146-
147-
It has the same interface as
148-
https://docs.python.org/3/library/bisect.html#bisect.insort_right .
149-
150-
:param sorted_collection: some ascending sorted collection with comparable items
151-
:param item: item to insert
152-
:param lo: lowest index to consider (as in sorted_collection[lo:hi])
153-
:param hi: past the highest index to consider (as in sorted_collection[lo:hi])
154-
155-
Examples:
156-
>>> sorted_collection = [0, 5, 7, 10, 15]
157-
>>> insort_right(sorted_collection, 6)
158-
>>> sorted_collection
159-
[0, 5, 6, 7, 10, 15]
160-
>>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)]
161-
>>> item = (5, 5)
162-
>>> insort_right(sorted_collection, item)
163-
>>> sorted_collection
164-
[(0, 0), (5, 5), (5, 5), (7, 7), (10, 10), (15, 15)]
165-
>>> item is sorted_collection[1]
166-
False
167-
>>> item is sorted_collection[2]
168-
True
169-
>>> sorted_collection = [0, 5, 7, 10, 15]
170-
>>> insort_right(sorted_collection, 20)
171-
>>> sorted_collection
172-
[0, 5, 7, 10, 15, 20]
173-
>>> sorted_collection = [0, 5, 7, 10, 15]
174-
>>> insort_right(sorted_collection, 15, 1, 3)
175-
>>> sorted_collection
176-
[0, 5, 7, 15, 10, 15]
43+
Inserts item into sorted_collection in sorted order (using bisect_right_custom).
17744
"""
178-
sorted_collection.insert(bisect_right(sorted_collection, item, lo, hi), item)
179-
180-
181-
def binary_search(sorted_collection: list[int], item: int) -> int:
182-
"""Pure implementation of a binary search algorithm in Python
183-
184-
Be careful collection must be ascending sorted otherwise, the result will be
185-
unpredictable
186-
187-
:param sorted_collection: some ascending sorted collection with comparable items
188-
:param item: item value to search
189-
:return: index of the found item or -1 if the item is not found
45+
sorted_collection.insert(bisect_right_custom(sorted_collection, item, lo, hi), item)
19046

191-
Examples:
192-
>>> binary_search([0, 5, 7, 10, 15], 0)
193-
0
194-
>>> binary_search([0, 5, 7, 10, 15], 15)
195-
4
196-
>>> binary_search([0, 5, 7, 10, 15], 5)
197-
1
198-
>>> binary_search([0, 5, 7, 10, 15], 6)
199-
-1
47+
def binary_search(sorted_collection, item):
20048
"""
201-
if list(sorted_collection) != sorted(sorted_collection):
202-
raise ValueError("sorted_collection must be sorted in ascending order")
203-
left = 0
204-
right = len(sorted_collection) - 1
205-
206-
while left <= right:
207-
midpoint = left + (right - left) // 2
208-
current_item = sorted_collection[midpoint]
209-
if current_item == item:
210-
return midpoint
211-
elif item < current_item:
212-
right = midpoint - 1
49+
Standard binary search implementation.
50+
Returns the index of item if found, else returns -1.
51+
"""
52+
lo, hi = 0, len(sorted_collection) - 1
53+
while lo <= hi:
54+
mid = (lo + hi) // 2
55+
if sorted_collection[mid] == item:
56+
return mid
57+
elif sorted_collection[mid] < item:
58+
lo = mid + 1
21359
else:
214-
left = midpoint + 1
60+
hi = mid - 1
21561
return -1
21662

217-
218-
def binary_search_std_lib(sorted_collection: list[int], item: int) -> int:
219-
"""Pure implementation of a binary search algorithm in Python using stdlib
220-
221-
Be careful collection must be ascending sorted otherwise, the result will be
222-
unpredictable
223-
224-
:param sorted_collection: some ascending sorted collection with comparable items
225-
:param item: item value to search
226-
:return: index of the found item or -1 if the item is not found
227-
228-
Examples:
229-
>>> binary_search_std_lib([0, 5, 7, 10, 15], 0)
230-
0
231-
>>> binary_search_std_lib([0, 5, 7, 10, 15], 15)
232-
4
233-
>>> binary_search_std_lib([0, 5, 7, 10, 15], 5)
234-
1
235-
>>> binary_search_std_lib([0, 5, 7, 10, 15], 6)
236-
-1
63+
def binary_search_std_lib(sorted_collection, item):
23764
"""
238-
if list(sorted_collection) != sorted(sorted_collection):
239-
raise ValueError("sorted_collection must be sorted in ascending order")
240-
index = bisect.bisect_left(sorted_collection, item)
65+
Binary search using Python's standard library bisect module.
66+
"""
67+
index = bisect_left(sorted_collection, item)
24168
if index != len(sorted_collection) and sorted_collection[index] == item:
24269
return index
24370
return -1
24471

245-
246-
def binary_search_by_recursion(
247-
sorted_collection: list[int], item: int, left: int = 0, right: int = -1
248-
) -> int:
249-
"""Pure implementation of a binary search algorithm in Python by recursion
250-
251-
Be careful collection must be ascending sorted otherwise, the result will be
252-
unpredictable
253-
First recursion should be started with left=0 and right=(len(sorted_collection)-1)
254-
255-
:param sorted_collection: some ascending sorted collection with comparable items
256-
:param item: item value to search
257-
:return: index of the found item or -1 if the item is not found
258-
259-
Examples:
260-
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 0, 0, 4)
261-
0
262-
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 15, 0, 4)
263-
4
264-
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 5, 0, 4)
265-
1
266-
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 6, 0, 4)
267-
-1
72+
def binary_search_by_recursion(sorted_collection, item, lo=0, hi=None):
73+
"""
74+
Binary search using recursion.
26875
"""
269-
if right < 0:
270-
right = len(sorted_collection) - 1
271-
if list(sorted_collection) != sorted(sorted_collection):
272-
raise ValueError("sorted_collection must be sorted in ascending order")
273-
if right < left:
76+
if hi is None:
77+
hi = len(sorted_collection) - 1
78+
if lo > hi:
27479
return -1
275-
276-
midpoint = left + (right - left) // 2
277-
278-
if sorted_collection[midpoint] == item:
279-
return midpoint
280-
elif sorted_collection[midpoint] > item:
281-
return binary_search_by_recursion(sorted_collection, item, left, midpoint - 1)
80+
mid = (lo + hi) // 2
81+
if sorted_collection[mid] == item:
82+
return mid
83+
elif sorted_collection[mid] > item:
84+
return binary_search_by_recursion(sorted_collection, item, lo, mid - 1)
28285
else:
283-
return binary_search_by_recursion(sorted_collection, item, midpoint + 1, right)
284-
285-
286-
def exponential_search(sorted_collection: list[int], item: int) -> int:
287-
"""Pure implementation of an exponential search algorithm in Python
288-
Resources used:
289-
https://en.wikipedia.org/wiki/Exponential_search
290-
291-
Be careful collection must be ascending sorted otherwise, result will be
292-
unpredictable
86+
return binary_search_by_recursion(sorted_collection, item, mid + 1, hi)
29387

294-
:param sorted_collection: some ascending sorted collection with comparable items
295-
:param item: item value to search
296-
:return: index of the found item or -1 if the item is not found
297-
298-
the order of this algorithm is O(lg I) where I is index position of item if exist
299-
300-
Examples:
301-
>>> exponential_search([0, 5, 7, 10, 15], 0)
302-
0
303-
>>> exponential_search([0, 5, 7, 10, 15], 15)
304-
4
305-
>>> exponential_search([0, 5, 7, 10, 15], 5)
306-
1
307-
>>> exponential_search([0, 5, 7, 10, 15], 6)
308-
-1
88+
def exponential_search(sorted_collection, item):
89+
"""
90+
Exponential search implementation.
91+
Useful for unbounded searches.
30992
"""
310-
if list(sorted_collection) != sorted(sorted_collection):
311-
raise ValueError("sorted_collection must be sorted in ascending order")
93+
if sorted_collection[0] == item:
94+
return 0
31295
bound = 1
31396
while bound < len(sorted_collection) and sorted_collection[bound] < item:
31497
bound *= 2
315-
left = bound // 2
316-
right = min(bound, len(sorted_collection) - 1)
317-
last_result = binary_search_by_recursion(
318-
sorted_collection=sorted_collection, item=item, left=left, right=right
319-
)
320-
if last_result is None:
321-
return -1
322-
return last_result
323-
324-
325-
searches = ( # Fastest to slowest...
326-
binary_search_std_lib,
327-
binary_search,
328-
exponential_search,
329-
binary_search_by_recursion,
330-
)
331-
98+
return binary_search_by_recursion(sorted_collection, item, bound // 2, min(bound, len(sorted_collection) - 1))
33299

333100
if __name__ == "__main__":
334101
import doctest
335102
import timeit
336103

104+
# Run doctests to validate examples
337105
doctest.testmod()
106+
107+
# List of search functions to benchmark
108+
searches = [binary_search_std_lib, binary_search, exponential_search, binary_search_by_recursion]
109+
110+
# Test and print results of searching for 10 in a sample list
338111
for search in searches:
339-
name = f"{search.__name__:>26}"
340-
print(f"{name}: {search([0, 5, 7, 10, 15], 10) = }") # type: ignore[operator]
112+
print(f"{search.__name__}: {search([0, 5, 7, 10, 15], 10) = }")
341113

342114
print("\nBenchmarks...")
343-
setup = "collection = range(1000)"
115+
setup = "collection = list(range(1000))"
116+
# Benchmark each search function
344117
for search in searches:
345-
name = search.__name__
346-
print(
347-
f"{name:>26}:",
348-
timeit.timeit(
349-
f"{name}(collection, 500)", setup=setup, number=5_000, globals=globals()
350-
),
351-
)
118+
time = timeit.timeit(f"{search.__name__}(collection, 500)", setup=setup, number=5000, globals=globals())
119+
print(f"{search.__name__:>26}: {time:.6f}")
352120

121+
# Interactive part: user inputs a list and a target number
353122
user_input = input("\nEnter numbers separated by comma: ").strip()
354123
collection = sorted(int(item) for item in user_input.split(","))
355124
target = int(input("Enter a single number to be found in the list: "))
356-
result = binary_search(sorted_collection=collection, item=target)
125+
result = binary_search(collection, target)
357126
if result == -1:
358127
print(f"{target} was not found in {collection}.")
359128
else:

0 commit comments

Comments
 (0)
Please sign in to comment.