Skip to content

Commit 7de6d88

Browse files
Merge branch 'master' into add-type-hints-parentheses
2 parents 75c44f7 + f527d43 commit 7de6d88

File tree

8 files changed

+818
-11
lines changed

8 files changed

+818
-11
lines changed

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)