forked from TheAlgorithms/Python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjarvis_march.py
More file actions
187 lines (140 loc) · 5.73 KB
/
jarvis_march.py
File metadata and controls
187 lines (140 loc) · 5.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""
Jarvis March (Gift Wrapping) algorithm for finding the convex hull of a set of points.
The convex hull is the smallest convex polygon that contains all the points.
Time Complexity: O(n*h) where n is the number of points and h is the number of
hull points.
Space Complexity: O(h) where h is the number of hull points.
USAGE:
-> Import this file into your project.
-> Use the jarvis_march() function to find the convex hull of a set of points.
-> Parameters:
-> points: A list of Point objects representing 2D coordinates
REFERENCES:
-> Wikipedia reference: https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
-> GeeksforGeeks:
https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/
"""
from __future__ import annotations
class Point:
"""Represents a 2D point with x and y coordinates."""
def __init__(self, x_coordinate: float, y_coordinate: float) -> None:
self.x = x_coordinate
self.y = y_coordinate
def __eq__(self, other: object) -> bool:
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"
def __hash__(self) -> int:
return hash((self.x, self.y))
def _cross_product(origin: Point, point_a: Point, point_b: Point) -> float:
"""
Calculate the cross product of vectors OA and OB.
Returns:
> 0: Counter-clockwise turn (left turn)
= 0: Collinear
< 0: Clockwise turn (right turn)
"""
return (point_a.x - origin.x) * (point_b.y - origin.y) - (point_a.y - origin.y) * (
point_b.x - origin.x
)
def _is_point_on_segment(p1: Point, p2: Point, point: Point) -> bool:
"""Check if a point lies on the line segment between p1 and p2."""
# Check if point is collinear with segment endpoints
cross = (point.y - p1.y) * (p2.x - p1.x) - (point.x - p1.x) * (p2.y - p1.y)
if abs(cross) > 1e-9:
return False
# Check if point is within the bounding box of the segment
return min(p1.x, p2.x) <= point.x <= max(p1.x, p2.x) and min(
p1.y, p2.y
) <= point.y <= max(p1.y, p2.y)
def _find_leftmost_point(points: list[Point]) -> int:
"""Find index of leftmost point (and bottom-most in case of tie)."""
left_idx = 0
for i in range(1, len(points)):
if points[i].x < points[left_idx].x or (
points[i].x == points[left_idx].x and points[i].y < points[left_idx].y
):
left_idx = i
return left_idx
def _find_next_hull_point(points: list[Point], current_idx: int) -> int:
"""Find the next point on the convex hull."""
next_idx = (current_idx + 1) % len(points)
# Ensure next_idx is not the same as current_idx
while next_idx == current_idx:
next_idx = (next_idx + 1) % len(points)
for i in range(len(points)):
if i == current_idx:
continue
cross = _cross_product(points[current_idx], points[i], points[next_idx])
if cross > 0:
next_idx = i
return next_idx
def _is_valid_polygon(hull: list[Point]) -> bool:
"""Check if hull forms a valid polygon (has at least one non-collinear turn)."""
for i in range(len(hull)):
p1 = hull[i]
p2 = hull[(i + 1) % len(hull)]
p3 = hull[(i + 2) % len(hull)]
if abs(_cross_product(p1, p2, p3)) > 1e-9:
return True
return False
def _add_point_to_hull(hull: list[Point], point: Point) -> None:
"""Add a point to hull, removing collinear intermediate points."""
last = len(hull) - 1
if len(hull) > 1 and _is_point_on_segment(hull[last - 1], hull[last], point):
hull[last] = Point(point.x, point.y)
else:
hull.append(Point(point.x, point.y))
def jarvis_march(points: list[Point]) -> list[Point]:
"""
Find the convex hull of a set of points using the Jarvis March algorithm.
The algorithm starts with the leftmost point and wraps around the set of
points, selecting the most counter-clockwise point at each step.
Args:
points: List of Point objects representing 2D coordinates
Returns:
List of Points that form the convex hull in counter-clockwise order.
Returns empty list if there are fewer than 3 non-collinear points.
"""
if len(points) <= 2:
return []
# Remove duplicate points to avoid infinite loops
unique_points = list(set(points))
if len(unique_points) <= 2:
return []
convex_hull: list[Point] = []
# Find the leftmost point
left_point_idx = _find_leftmost_point(unique_points)
convex_hull.append(
Point(unique_points[left_point_idx].x, unique_points[left_point_idx].y)
)
current_idx = left_point_idx
while True:
# Find the next counter-clockwise point
next_idx = _find_next_hull_point(unique_points, current_idx)
if next_idx == left_point_idx:
break
if next_idx == current_idx:
break
current_idx = next_idx
_add_point_to_hull(convex_hull, unique_points[current_idx])
# Check for degenerate cases
if len(convex_hull) <= 2:
return []
# Check if last point is collinear with first and second-to-last
last = len(convex_hull) - 1
if _is_point_on_segment(convex_hull[last - 1], convex_hull[last], convex_hull[0]):
convex_hull.pop()
if len(convex_hull) == 2:
return []
# Verify the hull forms a valid polygon
if not _is_valid_polygon(convex_hull):
return []
return convex_hull
if __name__ == "__main__":
# Example usage
points = [Point(0, 0), Point(1, 1), Point(0, 1), Point(1, 0), Point(0.5, 0.5)]
hull = jarvis_march(points)
print(f"Convex hull: {hull}")