add exponential search algorithm (#10732)

* add exponential_search algorithm

* replace binary_search with binary_search_recursion

* convert left type to int to be useable in binary_search_recursion

* add docs and tests for exponential_search algorithm

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* move exponential_search to binary_search.py to pass github auto build tests

delete exponential_search.py file

* Update searches/binary_search.py

Co-authored-by: Christian Clauss <cclauss@me.com>

* remove additional space  searches/binary_search.py

Co-authored-by: Christian Clauss <cclauss@me.com>

* return single data type in exponential_search searches/binary_search.py

Co-authored-by: Christian Clauss <cclauss@me.com>

* add doctest mod searches/binary_search.py

Co-authored-by: Christian Clauss <cclauss@me.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* use // instread of int() convert  searches/binary_search.py

Co-authored-by: Christian Clauss <cclauss@me.com>

* change test according to new code searches/binary_search.py

Co-authored-by: Christian Clauss <cclauss@me.com>

* fix binary_search_recursion multiple type return error

* add a timeit benchmark for exponential_search

* sort input of binary search to be equal in performance test with exponential_search

* raise value error instead of sorting input in binary and exonential search to fix bugs

* Update binary_search.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Christian Clauss <cclauss@me.com>
Co-authored-by: user <user@kali.user>
This commit is contained in:
Kiarash Hajian 2023-10-21 14:53:34 -04:00 committed by GitHub
parent 06edc0eea0
commit b814cf3781
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
This is pure Python implementation of binary search algorithms Pure Python implementations of binary search algorithms
For doctests run following command: For doctests run the following command:
python3 -m doctest -v binary_search.py python3 -m doctest -v binary_search.py
For manual testing run: For manual testing run:
@ -34,16 +34,12 @@ def bisect_left(
Examples: Examples:
>>> bisect_left([0, 5, 7, 10, 15], 0) >>> bisect_left([0, 5, 7, 10, 15], 0)
0 0
>>> bisect_left([0, 5, 7, 10, 15], 6) >>> bisect_left([0, 5, 7, 10, 15], 6)
2 2
>>> bisect_left([0, 5, 7, 10, 15], 20) >>> bisect_left([0, 5, 7, 10, 15], 20)
5 5
>>> bisect_left([0, 5, 7, 10, 15], 15, 1, 3) >>> bisect_left([0, 5, 7, 10, 15], 15, 1, 3)
3 3
>>> bisect_left([0, 5, 7, 10, 15], 6, 2) >>> bisect_left([0, 5, 7, 10, 15], 6, 2)
2 2
""" """
@ -79,16 +75,12 @@ def bisect_right(
Examples: Examples:
>>> bisect_right([0, 5, 7, 10, 15], 0) >>> bisect_right([0, 5, 7, 10, 15], 0)
1 1
>>> bisect_right([0, 5, 7, 10, 15], 15) >>> bisect_right([0, 5, 7, 10, 15], 15)
5 5
>>> bisect_right([0, 5, 7, 10, 15], 6) >>> bisect_right([0, 5, 7, 10, 15], 6)
2 2
>>> bisect_right([0, 5, 7, 10, 15], 15, 1, 3) >>> bisect_right([0, 5, 7, 10, 15], 15, 1, 3)
3 3
>>> bisect_right([0, 5, 7, 10, 15], 6, 2) >>> bisect_right([0, 5, 7, 10, 15], 6, 2)
2 2
""" """
@ -124,7 +116,6 @@ def insort_left(
>>> insort_left(sorted_collection, 6) >>> insort_left(sorted_collection, 6)
>>> sorted_collection >>> sorted_collection
[0, 5, 6, 7, 10, 15] [0, 5, 6, 7, 10, 15]
>>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)] >>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)]
>>> item = (5, 5) >>> item = (5, 5)
>>> insort_left(sorted_collection, item) >>> insort_left(sorted_collection, item)
@ -134,12 +125,10 @@ def insort_left(
True True
>>> item is sorted_collection[2] >>> item is sorted_collection[2]
False False
>>> sorted_collection = [0, 5, 7, 10, 15] >>> sorted_collection = [0, 5, 7, 10, 15]
>>> insort_left(sorted_collection, 20) >>> insort_left(sorted_collection, 20)
>>> sorted_collection >>> sorted_collection
[0, 5, 7, 10, 15, 20] [0, 5, 7, 10, 15, 20]
>>> sorted_collection = [0, 5, 7, 10, 15] >>> sorted_collection = [0, 5, 7, 10, 15]
>>> insort_left(sorted_collection, 15, 1, 3) >>> insort_left(sorted_collection, 15, 1, 3)
>>> sorted_collection >>> sorted_collection
@ -167,7 +156,6 @@ def insort_right(
>>> insort_right(sorted_collection, 6) >>> insort_right(sorted_collection, 6)
>>> sorted_collection >>> sorted_collection
[0, 5, 6, 7, 10, 15] [0, 5, 6, 7, 10, 15]
>>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)] >>> sorted_collection = [(0, 0), (5, 5), (7, 7), (10, 10), (15, 15)]
>>> item = (5, 5) >>> item = (5, 5)
>>> insort_right(sorted_collection, item) >>> insort_right(sorted_collection, item)
@ -177,12 +165,10 @@ def insort_right(
False False
>>> item is sorted_collection[2] >>> item is sorted_collection[2]
True True
>>> sorted_collection = [0, 5, 7, 10, 15] >>> sorted_collection = [0, 5, 7, 10, 15]
>>> insort_right(sorted_collection, 20) >>> insort_right(sorted_collection, 20)
>>> sorted_collection >>> sorted_collection
[0, 5, 7, 10, 15, 20] [0, 5, 7, 10, 15, 20]
>>> sorted_collection = [0, 5, 7, 10, 15] >>> sorted_collection = [0, 5, 7, 10, 15]
>>> insort_right(sorted_collection, 15, 1, 3) >>> insort_right(sorted_collection, 15, 1, 3)
>>> sorted_collection >>> sorted_collection
@ -191,29 +177,28 @@ def insort_right(
sorted_collection.insert(bisect_right(sorted_collection, item, lo, hi), item) sorted_collection.insert(bisect_right(sorted_collection, item, lo, hi), item)
def binary_search(sorted_collection: list[int], item: int) -> int | None: def binary_search(sorted_collection: list[int], item: int) -> int:
"""Pure implementation of binary search algorithm in Python """Pure implementation of a binary search algorithm in Python
Be careful collection must be ascending sorted, otherwise result will be Be careful collection must be ascending sorted otherwise, the result will be
unpredictable unpredictable
:param sorted_collection: some ascending sorted collection with comparable items :param sorted_collection: some ascending sorted collection with comparable items
:param item: item value to search :param item: item value to search
:return: index of found item or None if item is not found :return: index of the found item or -1 if the item is not found
Examples: Examples:
>>> binary_search([0, 5, 7, 10, 15], 0) >>> binary_search([0, 5, 7, 10, 15], 0)
0 0
>>> binary_search([0, 5, 7, 10, 15], 15) >>> binary_search([0, 5, 7, 10, 15], 15)
4 4
>>> binary_search([0, 5, 7, 10, 15], 5) >>> binary_search([0, 5, 7, 10, 15], 5)
1 1
>>> binary_search([0, 5, 7, 10, 15], 6) >>> binary_search([0, 5, 7, 10, 15], 6)
-1
""" """
if list(sorted_collection) != sorted(sorted_collection):
raise ValueError("sorted_collection must be sorted in ascending order")
left = 0 left = 0
right = len(sorted_collection) - 1 right = len(sorted_collection) - 1
@ -226,66 +211,66 @@ def binary_search(sorted_collection: list[int], item: int) -> int | None:
right = midpoint - 1 right = midpoint - 1
else: else:
left = midpoint + 1 left = midpoint + 1
return None return -1
def binary_search_std_lib(sorted_collection: list[int], item: int) -> int | None: def binary_search_std_lib(sorted_collection: list[int], item: int) -> int:
"""Pure implementation of binary search algorithm in Python using stdlib """Pure implementation of a binary search algorithm in Python using stdlib
Be careful collection must be ascending sorted, otherwise result will be Be careful collection must be ascending sorted otherwise, the result will be
unpredictable unpredictable
:param sorted_collection: some ascending sorted collection with comparable items :param sorted_collection: some ascending sorted collection with comparable items
:param item: item value to search :param item: item value to search
:return: index of found item or None if item is not found :return: index of the found item or -1 if the item is not found
Examples: Examples:
>>> binary_search_std_lib([0, 5, 7, 10, 15], 0) >>> binary_search_std_lib([0, 5, 7, 10, 15], 0)
0 0
>>> binary_search_std_lib([0, 5, 7, 10, 15], 15) >>> binary_search_std_lib([0, 5, 7, 10, 15], 15)
4 4
>>> binary_search_std_lib([0, 5, 7, 10, 15], 5) >>> binary_search_std_lib([0, 5, 7, 10, 15], 5)
1 1
>>> binary_search_std_lib([0, 5, 7, 10, 15], 6) >>> binary_search_std_lib([0, 5, 7, 10, 15], 6)
-1
""" """
if list(sorted_collection) != sorted(sorted_collection):
raise ValueError("sorted_collection must be sorted in ascending order")
index = bisect.bisect_left(sorted_collection, item) index = bisect.bisect_left(sorted_collection, item)
if index != len(sorted_collection) and sorted_collection[index] == item: if index != len(sorted_collection) and sorted_collection[index] == item:
return index return index
return None return -1
def binary_search_by_recursion( def binary_search_by_recursion(
sorted_collection: list[int], item: int, left: int, right: int sorted_collection: list[int], item: int, left: int = 0, right: int = -1
) -> int | None: ) -> int:
"""Pure implementation of binary search algorithm in Python by recursion """Pure implementation of a binary search algorithm in Python by recursion
Be careful collection must be ascending sorted, otherwise result will be Be careful collection must be ascending sorted otherwise, the result will be
unpredictable unpredictable
First recursion should be started with left=0 and right=(len(sorted_collection)-1) First recursion should be started with left=0 and right=(len(sorted_collection)-1)
:param sorted_collection: some ascending sorted collection with comparable items :param sorted_collection: some ascending sorted collection with comparable items
:param item: item value to search :param item: item value to search
:return: index of found item or None if item is not found :return: index of the found item or -1 if the item is not found
Examples: Examples:
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 0, 0, 4) >>> binary_search_by_recursion([0, 5, 7, 10, 15], 0, 0, 4)
0 0
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 15, 0, 4) >>> binary_search_by_recursion([0, 5, 7, 10, 15], 15, 0, 4)
4 4
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 5, 0, 4) >>> binary_search_by_recursion([0, 5, 7, 10, 15], 5, 0, 4)
1 1
>>> binary_search_by_recursion([0, 5, 7, 10, 15], 6, 0, 4) >>> binary_search_by_recursion([0, 5, 7, 10, 15], 6, 0, 4)
-1
""" """
if right < 0:
right = len(sorted_collection) - 1
if list(sorted_collection) != sorted(sorted_collection):
raise ValueError("sorted_collection must be sorted in ascending order")
if right < left: if right < left:
return None return -1
midpoint = left + (right - left) // 2 midpoint = left + (right - left) // 2
@ -297,12 +282,78 @@ def binary_search_by_recursion(
return binary_search_by_recursion(sorted_collection, item, midpoint + 1, right) return binary_search_by_recursion(sorted_collection, item, midpoint + 1, right)
def exponential_search(sorted_collection: list[int], item: int) -> int:
"""Pure implementation of an exponential search algorithm in Python
Resources used:
https://en.wikipedia.org/wiki/Exponential_search
Be careful collection must be ascending sorted otherwise, result will be
unpredictable
:param sorted_collection: some ascending sorted collection with comparable items
:param item: item value to search
:return: index of the found item or -1 if the item is not found
the order of this algorithm is O(lg I) where I is index position of item if exist
Examples:
>>> exponential_search([0, 5, 7, 10, 15], 0)
0
>>> exponential_search([0, 5, 7, 10, 15], 15)
4
>>> exponential_search([0, 5, 7, 10, 15], 5)
1
>>> exponential_search([0, 5, 7, 10, 15], 6)
-1
"""
if list(sorted_collection) != sorted(sorted_collection):
raise ValueError("sorted_collection must be sorted in ascending order")
bound = 1
while bound < len(sorted_collection) and sorted_collection[bound] < item:
bound *= 2
left = bound // 2
right = min(bound, len(sorted_collection) - 1)
last_result = binary_search_by_recursion(
sorted_collection=sorted_collection, item=item, left=left, right=right
)
if last_result is None:
return -1
return last_result
searches = ( # Fastest to slowest...
binary_search_std_lib,
binary_search,
exponential_search,
binary_search_by_recursion,
)
if __name__ == "__main__": if __name__ == "__main__":
user_input = input("Enter numbers separated by comma:\n").strip() import doctest
import timeit
doctest.testmod()
for search in searches:
name = f"{search.__name__:>26}"
print(f"{name}: {search([0, 5, 7, 10, 15], 10) = }") # type: ignore[operator]
print("\nBenchmarks...")
setup = "collection = range(1000)"
for search in searches:
name = search.__name__
print(
f"{name:>26}:",
timeit.timeit(
f"{name}(collection, 500)", setup=setup, number=5_000, globals=globals()
),
)
user_input = input("\nEnter numbers separated by comma: ").strip()
collection = sorted(int(item) for item in user_input.split(",")) collection = sorted(int(item) for item in user_input.split(","))
target = int(input("Enter a single number to be found in the list:\n")) target = int(input("Enter a single number to be found in the list: "))
result = binary_search(collection, target) result = binary_search(sorted_collection=collection, item=target)
if result is None: if result == -1:
print(f"{target} was not found in {collection}.") print(f"{target} was not found in {collection}.")
else: else:
print(f"{target} was found at position {result} in {collection}.") print(f"{target} was found at position {result} of {collection}.")