|
1 | 1 | #!/usr/bin/env python3
|
2 | 2 |
|
3 |
| -""" |
4 |
| -Pure Python implementations of binary search algorithms |
| 3 | +from bisect import bisect_left, bisect_right |
5 | 4 |
|
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): |
21 | 6 | """
|
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. |
46 | 9 | """
|
47 |
| - if hi < 0: |
| 10 | + if hi is None: |
48 | 11 | hi = len(sorted_collection)
|
49 |
| - |
50 | 12 | while lo < hi:
|
51 |
| - mid = lo + (hi - lo) // 2 |
| 13 | + mid = (lo + hi) // 2 |
52 | 14 | if sorted_collection[mid] < item:
|
53 | 15 | lo = mid + 1
|
54 | 16 | else:
|
55 | 17 | hi = mid
|
56 |
| - |
57 | 18 | return lo
|
58 | 19 |
|
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): |
63 | 21 | """
|
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. |
87 | 24 | """
|
88 |
| - if hi < 0: |
| 25 | + if hi is None: |
89 | 26 | hi = len(sorted_collection)
|
90 |
| - |
91 | 27 | while lo < hi:
|
92 |
| - mid = lo + (hi - lo) // 2 |
| 28 | + mid = (lo + hi) // 2 |
93 | 29 | if sorted_collection[mid] <= item:
|
94 | 30 | lo = mid + 1
|
95 | 31 | else:
|
96 | 32 | hi = mid
|
97 |
| - |
98 | 33 | return lo
|
99 | 34 |
|
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): |
104 | 36 | """
|
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). |
137 | 38 | """
|
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) |
140 | 40 |
|
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): |
144 | 42 | """
|
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). |
177 | 44 | """
|
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) |
190 | 46 |
|
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): |
200 | 48 | """
|
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 |
213 | 59 | else:
|
214 |
| - left = midpoint + 1 |
| 60 | + hi = mid - 1 |
215 | 61 | return -1
|
216 | 62 |
|
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): |
237 | 64 | """
|
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) |
241 | 68 | if index != len(sorted_collection) and sorted_collection[index] == item:
|
242 | 69 | return index
|
243 | 70 | return -1
|
244 | 71 |
|
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. |
268 | 75 | """
|
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: |
274 | 79 | 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) |
282 | 85 | 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) |
293 | 87 |
|
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. |
309 | 92 | """
|
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 |
312 | 95 | bound = 1
|
313 | 96 | while bound < len(sorted_collection) and sorted_collection[bound] < item:
|
314 | 97 | 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)) |
332 | 99 |
|
333 | 100 | if __name__ == "__main__":
|
334 | 101 | import doctest
|
335 | 102 | import timeit
|
336 | 103 |
|
| 104 | + # Run doctests to validate examples |
337 | 105 | 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 |
338 | 111 | 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) = }") |
341 | 113 |
|
342 | 114 | print("\nBenchmarks...")
|
343 |
| - setup = "collection = range(1000)" |
| 115 | + setup = "collection = list(range(1000))" |
| 116 | + # Benchmark each search function |
344 | 117 | 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}") |
352 | 120 |
|
| 121 | + # Interactive part: user inputs a list and a target number |
353 | 122 | user_input = input("\nEnter numbers separated by comma: ").strip()
|
354 | 123 | collection = sorted(int(item) for item in user_input.split(","))
|
355 | 124 | 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) |
357 | 126 | if result == -1:
|
358 | 127 | print(f"{target} was not found in {collection}.")
|
359 | 128 | else:
|
|
0 commit comments