Skip to content

Commit 776c8b8

Browse files
Merge branch 'master' into patch-8
2 parents 4b11822 + 6f9f431 commit 776c8b8

17 files changed

+860
-41
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ repos:
1414
- id: requirements-txt-fixer
1515

1616
- repo: https://github.com/MarcoGorelli/auto-walrus
17-
rev: 0.3.4
17+
rev: 0.4.1
1818
hooks:
1919
- id: auto-walrus
2020

2121
- repo: https://github.com/astral-sh/ruff-pre-commit
22-
rev: v0.14.10
22+
rev: v0.14.14
2323
hooks:
2424
- id: ruff-check
2525
- id: ruff-format
@@ -32,7 +32,7 @@ repos:
3232
- tomli
3333

3434
- repo: https://github.com/tox-dev/pyproject-fmt
35-
rev: v2.11.1
35+
rev: v2.12.1
3636
hooks:
3737
- id: pyproject-fmt
3838

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@
881881
* [Quine](other/quine.py)
882882
* [Scoring Algorithm](other/scoring_algorithm.py)
883883
* [Sdes](other/sdes.py)
884+
* [Sliding Window Maximum](other/sliding_window_maximum.py)
884885
* [Tower Of Hanoi](other/tower_of_hanoi.py)
885886
* [Word Search](other/word_search.py)
886887

backtracking/generate_parentheses_iterative.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
def generate_parentheses_iterative(length: int) -> list:
1+
def generate_parentheses_iterative(length: int) -> list[str]:
22
"""
33
Generate all valid combinations of parentheses (Iterative Approach).
44
55
The algorithm works as follows:
66
1. Initialize an empty list to store the combinations.
77
2. Initialize a stack to keep track of partial combinations.
8-
3. Start with empty string and push it onstack along with the counts of '(' and ')'.
8+
3. Start with empty string and push it on stack along with
9+
the counts of '(' and ')'.
910
4. While the stack is not empty:
1011
a. Pop a partial combination and its open and close counts from the stack.
1112
b. If the combination length is equal to 2*length, add it to the result.
@@ -34,8 +35,11 @@ def generate_parentheses_iterative(length: int) -> list:
3435
>>> generate_parentheses_iterative(0)
3536
['']
3637
"""
37-
result = []
38-
stack = []
38+
if length == 0:
39+
return [""]
40+
41+
result: list[str] = []
42+
stack: list[tuple[str, int, int]] = []
3943

4044
# Each element in stack is a tuple (current_combination, open_count, close_count)
4145
stack.append(("", 0, 0))
@@ -45,6 +49,7 @@ def generate_parentheses_iterative(length: int) -> list:
4549

4650
if len(current_combination) == 2 * length:
4751
result.append(current_combination)
52+
continue
4853

4954
if open_count < length:
5055
stack.append((current_combination + "(", open_count + 1, close_count))

ciphers/caesar_cipher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
4545
And our shift is ``2``
4646
4747
We can then encode the message, one letter at a time. ``H`` would become ``J``,
48-
since ``J`` is two letters away, and so on. If the shift is ever two large, or
48+
since ``J`` is two letters away, and so on. If the shift is ever too large, or
4949
our letter is at the end of the alphabet, we just start at the beginning
5050
(``Z`` would shift to ``a`` then ``b`` and so on).
5151

data_structures/hashing/hash_table_with_linked_list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def __init__(self, *args, **kwargs):
88
super().__init__(*args, **kwargs)
99

1010
def _set_value(self, key, data):
11-
self.values[key] = deque([]) if self.values[key] is None else self.values[key]
11+
self.values[key] = deque() if self.values[key] is None else self.values[key]
1212
self.values[key].appendleft(data)
1313
self._keys[key] = self.values[key]
1414

geometry/graham_scan.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
"""
2+
Graham Scan algorithm for finding the convex hull of a set of points.
3+
4+
The Graham scan is a method of computing the convex hull of a finite set of points
5+
in the plane with time complexity O(n log n). It is named after Ronald Graham, who
6+
published the original algorithm in 1972.
7+
8+
The algorithm finds all vertices of the convex hull ordered along its boundary.
9+
It uses a stack to efficiently identify and remove points that would create
10+
non-convex angles.
11+
12+
References:
13+
- https://en.wikipedia.org/wiki/Graham_scan
14+
- Graham, R.L. (1972). "An Efficient Algorithm for Determining the Convex Hull of a
15+
Finite Planar Set"
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from collections.abc import Sequence
21+
from dataclasses import dataclass
22+
from typing import TypeVar
23+
24+
T = TypeVar("T", bound="Point")
25+
26+
27+
@dataclass
28+
class Point:
29+
"""
30+
A point in 2D space.
31+
32+
>>> Point(0, 0)
33+
Point(x=0.0, y=0.0)
34+
>>> Point(1.5, 2.5)
35+
Point(x=1.5, y=2.5)
36+
"""
37+
38+
x: float
39+
y: float
40+
41+
def __init__(self, x_coordinate: float, y_coordinate: float) -> None:
42+
"""
43+
Initialize a 2D point.
44+
45+
Args:
46+
x_coordinate: The x-coordinate (horizontal position) of the point
47+
y_coordinate: The y-coordinate (vertical position) of the point
48+
"""
49+
self.x = float(x_coordinate)
50+
self.y = float(y_coordinate)
51+
52+
def __eq__(self, other: object) -> bool:
53+
"""
54+
Check if two points are equal.
55+
56+
>>> Point(1, 2) == Point(1, 2)
57+
True
58+
>>> Point(1, 2) == Point(2, 1)
59+
False
60+
"""
61+
if not isinstance(other, Point):
62+
return NotImplemented
63+
return self.x == other.x and self.y == other.y
64+
65+
def __lt__(self, other: Point) -> bool:
66+
"""
67+
Compare two points for sorting (bottom-most, then left-most).
68+
69+
>>> Point(1, 2) < Point(1, 3)
70+
True
71+
>>> Point(1, 2) < Point(2, 2)
72+
True
73+
>>> Point(2, 2) < Point(1, 2)
74+
False
75+
"""
76+
if self.y == other.y:
77+
return self.x < other.x
78+
return self.y < other.y
79+
80+
def euclidean_distance(self, other: Point) -> float:
81+
"""
82+
Calculate Euclidean distance between two points.
83+
84+
>>> Point(0, 0).euclidean_distance(Point(3, 4))
85+
5.0
86+
>>> Point(1, 1).euclidean_distance(Point(4, 5))
87+
5.0
88+
"""
89+
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
90+
91+
def consecutive_orientation(self, point_a: Point, point_b: Point) -> float:
92+
"""
93+
Calculate the cross product of vectors (self -> point_a) and
94+
(point_a -> point_b).
95+
96+
Returns:
97+
- Positive value: counter-clockwise turn
98+
- Negative value: clockwise turn
99+
- Zero: collinear points
100+
101+
>>> Point(0, 0).consecutive_orientation(Point(1, 0), Point(1, 1))
102+
1.0
103+
>>> Point(0, 0).consecutive_orientation(Point(1, 0), Point(1, -1))
104+
-1.0
105+
>>> Point(0, 0).consecutive_orientation(Point(1, 0), Point(2, 0))
106+
0.0
107+
"""
108+
return (point_a.x - self.x) * (point_b.y - point_a.y) - (point_a.y - self.y) * (
109+
point_b.x - point_a.x
110+
)
111+
112+
113+
def graham_scan(points: Sequence[Point]) -> list[Point]:
114+
"""
115+
Find the convex hull of a set of points using the Graham scan algorithm.
116+
117+
The algorithm works as follows:
118+
1. Find the bottom-most point (or left-most in case of tie)
119+
2. Sort all other points by polar angle with respect to the bottom-most point
120+
3. Process points in order, maintaining a stack of hull candidates
121+
4. Remove points that would create a clockwise turn
122+
123+
Args:
124+
points: A sequence of Point objects
125+
126+
Returns:
127+
A list of Point objects representing the convex hull in counter-clockwise order.
128+
Returns an empty list if there are fewer than 3 distinct points or if all
129+
points are collinear.
130+
131+
Time Complexity: O(n log n) due to sorting
132+
Space Complexity: O(n) for the output hull
133+
134+
>>> graham_scan([])
135+
[]
136+
>>> graham_scan([Point(0, 0)])
137+
[]
138+
>>> graham_scan([Point(0, 0), Point(1, 1)])
139+
[]
140+
>>> hull = graham_scan([Point(0, 0), Point(1, 0), Point(0.5, 1)])
141+
>>> len(hull)
142+
3
143+
>>> Point(0, 0) in hull and Point(1, 0) in hull and Point(0.5, 1) in hull
144+
True
145+
"""
146+
if len(points) <= 2:
147+
return []
148+
149+
# Find the bottom-most point (left-most in case of tie)
150+
min_point = min(points)
151+
152+
# Remove the min_point from the list
153+
points_list = [p for p in points if p != min_point]
154+
if not points_list:
155+
# Edge case where all points are the same
156+
return []
157+
158+
def polar_angle_key(point: Point) -> tuple[float, float, float]:
159+
"""
160+
Key function for sorting points by polar angle relative to min_point.
161+
162+
Points are sorted counter-clockwise. When two points have the same angle,
163+
the farther point comes first (we'll remove duplicates later).
164+
"""
165+
# We use a dummy third point (min_point itself) to calculate relative angles
166+
# Instead, we'll compute the angle between points
167+
dx = point.x - min_point.x
168+
dy = point.y - min_point.y
169+
170+
# Use atan2 for angle, but we can also use cross product for comparison
171+
# For sorting, we compare orientations between consecutive points
172+
distance = min_point.euclidean_distance(point)
173+
return (dx, dy, -distance) # Negative distance to sort farther points first
174+
175+
# Sort by polar angle using a comparison based on cross product
176+
def compare_points(point_a: Point, point_b: Point) -> int:
177+
"""Compare two points by polar angle relative to min_point."""
178+
orientation = min_point.consecutive_orientation(point_a, point_b)
179+
if orientation < 0.0:
180+
return 1 # point_a comes after point_b (clockwise)
181+
elif orientation > 0.0:
182+
return -1 # point_a comes before point_b (counter-clockwise)
183+
else:
184+
# Collinear: farther point should come first
185+
dist_a = min_point.euclidean_distance(point_a)
186+
dist_b = min_point.euclidean_distance(point_b)
187+
if dist_b < dist_a:
188+
return -1
189+
elif dist_b > dist_a:
190+
return 1
191+
else:
192+
return 0
193+
194+
from functools import cmp_to_key
195+
196+
points_list.sort(key=cmp_to_key(compare_points))
197+
198+
# Build the convex hull
199+
convex_hull: list[Point] = [min_point, points_list[0]]
200+
201+
for point in points_list[1:]:
202+
# Skip consecutive points with the same angle (collinear with min_point)
203+
if min_point.consecutive_orientation(point, convex_hull[-1]) == 0.0:
204+
continue
205+
206+
# Remove points that create a clockwise turn (or are collinear)
207+
while len(convex_hull) >= 2:
208+
orientation = convex_hull[-2].consecutive_orientation(
209+
convex_hull[-1], point
210+
)
211+
if orientation <= 0.0:
212+
convex_hull.pop()
213+
else:
214+
break
215+
216+
convex_hull.append(point)
217+
218+
# Need at least 3 points for a valid convex hull
219+
if len(convex_hull) <= 2:
220+
return []
221+
222+
return convex_hull
223+
224+
225+
if __name__ == "__main__":
226+
import doctest
227+
228+
doctest.testmod()
229+
230+
# Example usage
231+
points = [
232+
Point(0, 0),
233+
Point(1, 0),
234+
Point(2, 0),
235+
Point(2, 1),
236+
Point(2, 2),
237+
Point(1, 2),
238+
Point(0, 2),
239+
Point(0, 1),
240+
Point(1, 1), # Interior point
241+
]
242+
243+
hull = graham_scan(points)
244+
print("Convex hull vertices:")
245+
for point in hull:
246+
print(f" ({point.x}, {point.y})")

0 commit comments

Comments
 (0)