Skip to content

Commit 24b7987

Browse files
author
Ethan Xiong
committed
fix: #13840 - handle duplicate items correctly in binary search
* Update searches/binary_search.py. Always move left to locate the first occurrence of duplicate items when found a match. * Update doctests to cover duplicate items. * In `binary_search_by_recursion`, verify `sorted_collection` only once to reduce runtime.
1 parent e2a78d4 commit 24b7987

File tree

1 file changed

+36
-5
lines changed

1 file changed

+36
-5
lines changed

searches/binary_search.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ def binary_search(sorted_collection: list[int], item: int) -> int:
195195
4
196196
>>> binary_search([0, 5, 7, 10, 15], 5)
197197
1
198+
>>> binary_search([0, 5, 6, 7, 7, 8, 10, 10, 10, 13, 15], 10)
199+
6
198200
>>> binary_search([0, 5, 7, 10, 15], 6)
199201
-1
200202
"""
@@ -207,6 +209,9 @@ def binary_search(sorted_collection: list[int], item: int) -> int:
207209
midpoint = left + (right - left) // 2
208210
current_item = sorted_collection[midpoint]
209211
if current_item == item:
212+
# Found a match; now move left to locate the first occurrence
213+
while midpoint > 0 and sorted_collection[midpoint - 1] == item:
214+
midpoint -= 1
210215
return midpoint
211216
elif item < current_item:
212217
right = midpoint - 1
@@ -232,6 +237,8 @@ def binary_search_std_lib(sorted_collection: list[int], item: int) -> int:
232237
4
233238
>>> binary_search_std_lib([0, 5, 7, 10, 15], 5)
234239
1
240+
>>> binary_search_std_lib([0, 5, 6, 7, 7, 8, 10, 10, 10, 13, 15], 10)
241+
6
235242
>>> binary_search_std_lib([0, 5, 7, 10, 15], 6)
236243
-1
237244
"""
@@ -263,19 +270,30 @@ def binary_search_by_recursion(
263270
4
264271
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 5, 0, 4)
265272
1
273+
>>> binary_search_by_recursion([0, 5, 6, 7, 7, 8, 10, 10, 10, 13, 15], 10)
274+
6
266275
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 6, 0, 4)
267276
-1
268277
"""
278+
if (
279+
left == 0
280+
and right == len(sorted_collection) - 1
281+
and list(sorted_collection) != sorted(sorted_collection)
282+
): # only verify ascending once
283+
raise ValueError("sorted_collection must be sorted in ascending order")
284+
269285
if right < 0:
270286
right = len(sorted_collection) - 1
271-
if list(sorted_collection) != sorted(sorted_collection):
272-
raise ValueError("sorted_collection must be sorted in ascending order")
287+
273288
if right < left:
274289
return -1
275290

276291
midpoint = left + (right - left) // 2
277292

278293
if sorted_collection[midpoint] == item:
294+
# Found a match; now move left to locate the first occurrence
295+
while midpoint > 0 and sorted_collection[midpoint - 1] == item:
296+
midpoint -= 1
279297
return midpoint
280298
elif sorted_collection[midpoint] > item:
281299
return binary_search_by_recursion(sorted_collection, item, left, midpoint - 1)
@@ -304,6 +322,8 @@ def exponential_search(sorted_collection: list[int], item: int) -> int:
304322
4
305323
>>> exponential_search([0, 5, 7, 10, 15], 5)
306324
1
325+
>>> exponential_search([0, 5, 6, 7, 7, 8, 10, 10, 10, 13, 15], 10)
326+
6
307327
>>> exponential_search([0, 5, 7, 10, 15], 6)
308328
-1
309329
"""
@@ -335,9 +355,20 @@ def exponential_search(sorted_collection: list[int], item: int) -> int:
335355
import timeit
336356

337357
doctest.testmod()
338-
for search in searches:
339-
name = f"{search.__name__:>26}"
340-
print(f"{name}: {search([0, 5, 7, 10, 15], 10) = }") # type: ignore[operator]
358+
359+
test_cases = [
360+
([0, 5, 7, 10, 15], 10, 3),
361+
([0, 5, 6, 7, 7, 8, 10, 10, 10, 13, 15], 10, 6),
362+
([0, 0, 0, 5, 6, 7, 7, 8, 10, 10, 10, 13, 15], 0, 0),
363+
]
364+
365+
for items, target, answer in test_cases:
366+
print(f"search({items}, {target})")
367+
for search in searches:
368+
name = f"{search.__name__:>26}"
369+
res = search(items, target)
370+
mark = "✅" if res == answer else "❌"
371+
print(f"{mark} {name}: {res}")
341372

342373
print("\nBenchmarks...")
343374
setup = "collection = range(1000)"

0 commit comments

Comments
 (0)