1515
1616REFERENCES:
1717 -> Wikipedia reference: https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
18- -> GeeksforGeeks: https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/
18+ -> GeeksforGeeks:
19+ https://www.geeksforgeeks.org/convex-hull-set-1-jarviss-algorithm-or-wrapping/
1920"""
2021
2122from __future__ import annotations
@@ -49,9 +50,9 @@ def _cross_product(origin: Point, point_a: Point, point_b: Point) -> float:
4950 = 0: Collinear
5051 < 0: Clockwise turn (right turn)
5152 """
52- return (point_a .x - origin .x ) * (point_b .y - origin .y ) - (point_a . y - origin . y ) * (
53- point_b . x - origin .x
54- )
53+ return (point_a .x - origin .x ) * (point_b .y - origin .y ) - (
54+ point_a . y - origin .y
55+ ) * ( point_b . x - origin . x )
5556
5657
5758def _is_point_on_segment (p1 : Point , p2 : Point , point : Point ) -> bool :
@@ -68,12 +69,62 @@ def _is_point_on_segment(p1: Point, p2: Point, point: Point) -> bool:
6869 ) <= point .y <= max (p1 .y , p2 .y )
6970
7071
72+ def _find_leftmost_point (points : list [Point ]) -> int :
73+ """Find index of leftmost point (and bottom-most in case of tie)."""
74+ left_idx = 0
75+ for i in range (1 , len (points )):
76+ if points [i ].x < points [left_idx ].x or (
77+ points [i ].x == points [left_idx ].x and points [i ].y < points [left_idx ].y
78+ ):
79+ left_idx = i
80+ return left_idx
81+
82+
83+ def _find_next_hull_point (points : list [Point ], current_idx : int ) -> int :
84+ """Find the next point on the convex hull."""
85+ next_idx = (current_idx + 1 ) % len (points )
86+ # Ensure next_idx is not the same as current_idx
87+ while next_idx == current_idx :
88+ next_idx = (next_idx + 1 ) % len (points )
89+
90+ for i in range (len (points )):
91+ if i == current_idx :
92+ continue
93+ cross = _cross_product (points [current_idx ], points [i ], points [next_idx ])
94+ if cross > 0 :
95+ next_idx = i
96+
97+ return next_idx
98+
99+
100+ def _is_valid_polygon (hull : list [Point ]) -> bool :
101+ """Check if hull forms a valid polygon (has at least one non-collinear turn)."""
102+ for i in range (len (hull )):
103+ p1 = hull [i ]
104+ p2 = hull [(i + 1 ) % len (hull )]
105+ p3 = hull [(i + 2 ) % len (hull )]
106+ if abs (_cross_product (p1 , p2 , p3 )) > 1e-9 :
107+ return True
108+ return False
109+
110+
111+ def _add_point_to_hull (hull : list [Point ], point : Point ) -> None :
112+ """Add a point to hull, removing collinear intermediate points."""
113+ last = len (hull ) - 1
114+ if len (hull ) > 1 and _is_point_on_segment (
115+ hull [last - 1 ], hull [last ], point
116+ ):
117+ hull [last ] = Point (point .x , point .y )
118+ else :
119+ hull .append (Point (point .x , point .y ))
120+
121+
71122def jarvis_march (points : list [Point ]) -> list [Point ]:
72123 """
73124 Find the convex hull of a set of points using the Jarvis March algorithm.
74125
75- The algorithm starts with the leftmost point and wraps around the set of points,
76- selecting the most counter-clockwise point at each step.
126+ The algorithm starts with the leftmost point and wraps around the set of
127+ points, selecting the most counter-clockwise point at each step.
77128
78129 Args:
79130 points: List of Point objects representing 2D coordinates
@@ -93,87 +144,41 @@ def jarvis_march(points: list[Point]) -> list[Point]:
93144
94145 convex_hull : list [Point ] = []
95146
96- # Find the leftmost point (and bottom-most in case of tie)
97- left_point_idx = 0
98- for i in range (1 , len (unique_points )):
99- if unique_points [i ].x < unique_points [left_point_idx ].x or (
100- unique_points [i ].x == unique_points [left_point_idx ].x
101- and unique_points [i ].y < unique_points [left_point_idx ].y
102- ):
103- left_point_idx = i
104-
147+ # Find the leftmost point
148+ left_point_idx = _find_leftmost_point (unique_points )
105149 convex_hull .append (
106150 Point (unique_points [left_point_idx ].x , unique_points [left_point_idx ].y )
107151 )
108152
109153 current_idx = left_point_idx
110154 while True :
111155 # Find the next counter-clockwise point
112- next_idx = (current_idx + 1 ) % len (unique_points )
113- # Make sure next_idx is not the same as current_idx (handle duplicates)
114- while next_idx == current_idx :
115- next_idx = (next_idx + 1 ) % len (unique_points )
116-
117- for i in range (len (unique_points )):
118- # Skip the current point itself (handles duplicates)
119- if i == current_idx :
120- continue
121- if (
122- _cross_product (
123- unique_points [current_idx ],
124- unique_points [i ],
125- unique_points [next_idx ],
126- )
127- > 0
128- ):
129- next_idx = i
156+ next_idx = _find_next_hull_point (unique_points , current_idx )
130157
131158 if next_idx == left_point_idx :
132- # Completed constructing the hull
133159 break
134160
135- # Safety check: if next_idx == current_idx, we have duplicates causing issues
136161 if next_idx == current_idx :
137162 break
138163
139164 current_idx = next_idx
165+ _add_point_to_hull (convex_hull , unique_points [current_idx ])
140166
141- # Check if the last point is collinear with new point and second-to-last
142- last = len (convex_hull ) - 1
143- if len (convex_hull ) > 1 and _is_point_on_segment (
144- convex_hull [last - 1 ], convex_hull [last ], unique_points [current_idx ]
145- ):
146- # Remove the last point from the hull
147- convex_hull [last ] = Point (
148- unique_points [current_idx ].x , unique_points [current_idx ].y
149- )
150- else :
151- convex_hull .append (
152- Point (unique_points [current_idx ].x , unique_points [current_idx ].y )
153- )
154-
155- # Check for edge case: last point collinear with first and second-to-last
167+ # Check for degenerate cases
156168 if len (convex_hull ) <= 2 :
157169 return []
158170
171+ # Check if last point is collinear with first and second-to-last
159172 last = len (convex_hull ) - 1
160- if _is_point_on_segment (convex_hull [last - 1 ], convex_hull [last ], convex_hull [0 ]):
173+ if _is_point_on_segment (
174+ convex_hull [last - 1 ], convex_hull [last ], convex_hull [0 ]
175+ ):
161176 convex_hull .pop ()
162177 if len (convex_hull ) == 2 :
163178 return []
164179
165- # Final check: verify the hull forms a valid polygon (at least one non-zero cross product)
166- # If all cross products are zero, all points are collinear
167- has_turn = False
168- for i in range (len (convex_hull )):
169- p1 = convex_hull [i ]
170- p2 = convex_hull [(i + 1 ) % len (convex_hull )]
171- p3 = convex_hull [(i + 2 ) % len (convex_hull )]
172- if abs (_cross_product (p1 , p2 , p3 )) > 1e-9 :
173- has_turn = True
174- break
175-
176- if not has_turn :
180+ # Verify the hull forms a valid polygon
181+ if not _is_valid_polygon (convex_hull ):
177182 return []
178183
179184 return convex_hull
0 commit comments