mirror of
https://github.com/TheAlgorithms/Python.git
synced 2026-03-13 09:50:19 +08:00
* Add Jarvis March (Gift Wrapping) convex hull algorithm * Use descriptive parameter names per algorithms-keeper review * Update jarvis_march.py * Update jarvis_march.py * fix: add pytest marker * Update jarvis_march.py * feat: removed doctests and created a separate test file for CI to pass * Update jarvis_march_unit.py * Update jarvis_march.py * Update jarvis_march_unit.py * feat: added test folder with tests to pass CI checks * fix: duplicate points handled * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: fixed ruff errors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
188 lines
5.7 KiB
Python
188 lines
5.7 KiB
Python
"""
|
|
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}")
|