RedEnginePress logo
RedEnginePress
AlgorithmsLanguagesPlaygroundAbout

Jarvis March

A
"""
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}")