Skip to content

Commit 40e2805

Browse files
committed
Add materials for binary search
1 parent d2cd714 commit 40e2805

File tree

7 files changed

+365
-0
lines changed

7 files changed

+365
-0
lines changed

binary-search/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# How to Do a Binary Search in Python?
2+
3+
Code snippets supplementing the [How to Do a Binary Search in Python?](https://realpython.com/binary-search-python/) article on [Real Python](https://realpython.com/).

binary-search/benchmark.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
Benchmark the performance of a search algorithm.
3+
4+
Requirements:
5+
* Python 3.7+
6+
7+
Usage:
8+
$ python benchmark.py -a handbag -f names.txt 'Arnold Schwarzenegger'
9+
$ python benchmark.py -a linear -f names.txt 'Arnold Schwarzenegger'
10+
$ python benchmark.py -a binary -f sorted_names.txt 'Arnold Schwarzenegger'
11+
"""
12+
13+
import argparse
14+
import time
15+
from statistics import median
16+
from typing import List
17+
18+
from search.binary import find_index as binary_search
19+
from search.handbag import find_index as handbag_search
20+
from search.linear import find_index as linear_search
21+
22+
23+
def main(args: argparse.Namespace) -> None:
24+
"""Script entry point."""
25+
26+
algorithms = {
27+
'handbag': handbag_search,
28+
'linear': linear_search,
29+
'binary': binary_search
30+
}
31+
32+
benchmark(
33+
algorithms[args.algorithm],
34+
load_names(args.path),
35+
args.search_term)
36+
37+
38+
def parse_args() -> argparse.Namespace:
39+
"""Parse command line arguments."""
40+
parser = argparse.ArgumentParser()
41+
parser.add_argument('-a', '--algorithm', choices=('handbag', 'linear', 'binary'))
42+
parser.add_argument('-f', '--file', dest='path')
43+
parser.add_argument('search_term')
44+
return parser.parse_args()
45+
46+
47+
def load_names(path: str) -> List[str]:
48+
"""Return a list of names from the given file."""
49+
print('Loading names...', end='', flush=True)
50+
with open(path) as text_file:
51+
names = text_file.read().splitlines()
52+
print('ok')
53+
return names
54+
55+
56+
def convert(nano: int) -> str:
57+
"""Convert nano seconds to a formatted string."""
58+
59+
kilo, mega, giga = 1e3, 1e6, 1e9
60+
61+
if nano < kilo:
62+
return f'{nano} ns'
63+
64+
if nano < mega:
65+
return f'{nano / kilo:.2f} µs'
66+
67+
if nano < giga:
68+
return f'{nano / mega:.2f} ms'
69+
70+
return f'{nano / giga:.2f} s'
71+
72+
73+
def benchmark(algorithm, elements: List[str], value: str, repeat: int = 10) -> None:
74+
"""Search for a value in elements using the given algorithm."""
75+
76+
times: List[int] = []
77+
for i in range(repeat):
78+
print(f'[{i + 1}/{repeat}] Searching...', end='', flush=True)
79+
start_time = time.perf_counter_ns()
80+
index = algorithm(elements, value)
81+
elapsed_time = time.perf_counter_ns() - start_time
82+
times.append(elapsed_time)
83+
print('\b' * 12, end='')
84+
if index is None:
85+
print(f'Not found ({convert(elapsed_time)})')
86+
else:
87+
print(f'Found at index={index} ({convert(elapsed_time)})')
88+
89+
print(f'best={convert(min(times))}',
90+
f'worst={convert(max(times))}',
91+
f'avg={convert(int(sum(times) / len(times)))}',
92+
f'median={convert(int(median(times)))}',
93+
sep=', ')
94+
95+
96+
if __name__ == '__main__':
97+
try:
98+
main(parse_args())
99+
except KeyboardInterrupt:
100+
print('Aborted')

binary-search/mandelbrot.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
Visualize the Mandelbrot set.
3+
4+
Usage:
5+
$ pip install Pillow
6+
$ python mandelbrot.py image.png
7+
"""
8+
9+
import argparse
10+
from PIL import Image
11+
12+
13+
class Rect:
14+
def __init__(self, x0, x1, y0, y1) -> None:
15+
self.x0, self.x1 = x0, x1
16+
self.y0, self.y1 = y0, y1
17+
18+
@property
19+
def width(self) -> int:
20+
return self.x1 - self.x0
21+
22+
@property
23+
def height(self) -> int:
24+
return self.y1 - self.y0
25+
26+
@property
27+
def aspect_ratio(self) -> float:
28+
return self.height / self.width
29+
30+
31+
def main(args: argparse.Namespace) -> None:
32+
"""Script entry point."""
33+
image = draw(bbox=Rect(-2.2, 0.8, -1.25, 1.25), width=1280, iterations=20)
34+
image.save(args.path)
35+
36+
37+
def parse_args() -> argparse.Namespace:
38+
"""Parse command line arguments."""
39+
parser = argparse.ArgumentParser()
40+
parser.add_argument('path')
41+
return parser.parse_args()
42+
43+
44+
def draw(bbox: Rect, width: int, iterations: int) -> Image:
45+
"""Return an image instance."""
46+
47+
height = int(width * bbox.aspect_ratio)
48+
image = Image.new('L', (width, height))
49+
50+
for y in range(height):
51+
for x in range(width):
52+
re = x * bbox.width / width + bbox.x0
53+
im = (height - y) * bbox.height / height + bbox.y0
54+
color = 255 - int(255 * divergence(complex(re, im), iterations))
55+
image.putpixel((x, y), color)
56+
57+
return image
58+
59+
60+
def divergence(number: complex, max_iterations: int, limit: int = 2) -> float:
61+
"""Return Mandelbrot set membership as a value between 0 and 1 inclusive."""
62+
z = 0j
63+
for _ in range(max_iterations):
64+
z = z**2 + number
65+
if abs(z) > limit:
66+
return 0
67+
return abs(z) / limit
68+
69+
70+
if __name__ == '__main__':
71+
main(parse_args())

binary-search/search/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from typing import TypeVar, Union
2+
3+
T = TypeVar('T')
4+
S = TypeVar('S')
5+
6+
7+
def identity(element: T) -> Union[T, S]:
8+
"""Identity function serving as a default key provider."""
9+
return element

binary-search/search/binary.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
The binary search algorithm.
3+
"""
4+
5+
from typing import Callable, Optional, Set, Sequence, Union
6+
7+
from search import T, S, identity
8+
9+
10+
def find_index(elements: Sequence[T],
11+
value: S,
12+
key: Callable[[T], Union[T, S]] = identity) -> Optional[int]:
13+
"""Return the index of value in elements or None."""
14+
15+
left, right = 0, len(elements) - 1
16+
17+
while left <= right:
18+
middle = (left + right) // 2
19+
20+
middle_element = key(elements[middle])
21+
22+
if middle_element == value:
23+
return middle
24+
25+
if middle_element < value:
26+
left = middle + 1
27+
elif middle_element > value:
28+
right = middle - 1
29+
30+
return None
31+
32+
33+
def find_leftmost_index(elements: Sequence[T],
34+
value: S,
35+
key: Callable[[T], Union[T, S]] = identity) -> Optional[int]:
36+
"""Return the leftmost index of value in elements or None."""
37+
38+
index = find_index(elements, value, key)
39+
40+
if index is not None:
41+
while index >= 0 and key(elements[index]) == value:
42+
index -= 1
43+
index += 1
44+
45+
return index
46+
47+
48+
def find_rightmost_index(elements: Sequence[T],
49+
value: S,
50+
key: Callable[[T], Union[T, S]] = identity) -> Optional[int]:
51+
"""Return the rightmost index of value in elements or None."""
52+
53+
index = find_index(elements, value, key)
54+
55+
if index is not None:
56+
while index < len(elements) and key(elements[index]) == value:
57+
index += 1
58+
index -= 1
59+
60+
return index
61+
62+
63+
def find_all_indices(elements: Sequence[T],
64+
value: S,
65+
key: Callable[[T], Union[T, S]] = identity) -> Set[int]:
66+
"""Return a set of indices of elements with matching key."""
67+
68+
left = find_leftmost_index(elements, value, key)
69+
right = find_rightmost_index(elements, value, key)
70+
71+
if left and right:
72+
return set(range(left, right + 1))
73+
74+
return set()
75+
76+
77+
def find(elements: Sequence[T],
78+
value: S,
79+
key: Callable[[T], Union[T, S]] = identity) -> Optional[T]:
80+
"""Return an element with matching key or None."""
81+
return _get(elements, find_index(elements, value, key))
82+
83+
84+
def find_leftmost(elements: Sequence[T],
85+
value: S,
86+
key: Callable[[T], Union[T, S]] = identity) -> Optional[T]:
87+
"""Return the leftmost element or None."""
88+
return _get(elements, find_leftmost_index(elements, value, key))
89+
90+
91+
def find_rightmost(elements: Sequence[T],
92+
value: S,
93+
key: Callable[[T], Union[T, S]] = identity) -> Optional[T]:
94+
"""Return the rightmost element or None."""
95+
return _get(elements, find_rightmost_index(elements, value, key))
96+
97+
98+
def find_all(elements: Sequence[T],
99+
value: S,
100+
key: Callable[[T], Union[T, S]] = identity) -> Set[T]:
101+
"""Return a set of elements with matching key."""
102+
return {elements[i] for i in find_all_indices(elements, value, key)}
103+
104+
105+
def contains(elements: Sequence[T],
106+
value: S,
107+
key: Callable[[T], Union[T, S]] = identity) -> bool:
108+
"""Return True if value is present in elements."""
109+
return find_index(elements, value, key) is not None
110+
111+
112+
def _get(elements: Sequence[T], index: Optional[int]) -> Optional[T]:
113+
"""Return element at the given index or None."""
114+
return None if index is None else elements[index]

binary-search/search/handbag.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
The handbag search algorithm.
3+
"""
4+
5+
import random
6+
from typing import Callable, Optional, Set, Sequence, Union
7+
8+
from search import T, S, identity
9+
10+
11+
def find_index(elements: Sequence[T],
12+
value: S,
13+
key: Callable[[T], Union[T, S]] = identity) -> Optional[int]:
14+
"""Return the index of value in elements or None."""
15+
visited: Set[int] = set()
16+
while len(visited) < len(elements):
17+
random_index = random.randint(0, len(elements) - 1)
18+
visited.add(random_index)
19+
if key(elements[random_index]) == value:
20+
return random_index
21+
return None
22+
23+
24+
def find(elements: Sequence[T],
25+
value: S,
26+
key: Callable[[T], Union[T, S]] = identity) -> Optional[T]:
27+
"""Return an element with matching key or None."""
28+
index = find_index(elements, value, key)
29+
return None if index is None else elements[index]
30+
31+
32+
def contains(elements: Sequence[T],
33+
value: S,
34+
key: Callable[[T], Union[T, S]] = identity) -> bool:
35+
"""Return True if value is present in elements."""
36+
return find_index(elements, value, key) is not None

binary-search/search/linear.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
The linear search algorithm.
3+
"""
4+
5+
from typing import Callable, Optional, Sequence, Union
6+
7+
from search import T, S, identity
8+
9+
10+
def find_index(elements: Sequence[T],
11+
value: S,
12+
key: Callable[[T], Union[T, S]] = identity) -> Optional[int]:
13+
"""Return the index of value in elements or None."""
14+
for i, element in enumerate(elements):
15+
if key(element) == value:
16+
return i
17+
return None
18+
19+
20+
def find(elements: Sequence[T],
21+
value: S,
22+
key: Callable[[T], Union[T, S]] = identity) -> Optional[T]:
23+
"""Return an element with matching key or None."""
24+
index = find_index(elements, value, key)
25+
return None if index is None else elements[index]
26+
27+
28+
def contains(elements: Sequence[T],
29+
value: S,
30+
key: Callable[[T], Union[T, S]] = identity) -> bool:
31+
"""Return True if value is present in elements."""
32+
return find_index(elements, value, key) is not None

0 commit comments

Comments
 (0)