-
-
Notifications
You must be signed in to change notification settings - Fork 50k
feat: add Jarvis March (Gift Wrapping) convex hull algorithm #14225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AliAlimohammadi
wants to merge
2
commits into
TheAlgorithms:master
Choose a base branch
from
AliAlimohammadi:add-jarvis-march-algorithm
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+214
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| """ | ||
| 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. | ||
|
|
||
| >>> p = Point(1.0, 2.0) | ||
| >>> p.x | ||
| 1.0 | ||
| >>> p.y | ||
| 2.0 | ||
| """ | ||
|
|
||
| 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) | ||
|
|
||
| >>> origin = Point(0, 0) | ||
| >>> point_a = Point(1, 1) | ||
| >>> point_b = Point(2, 0) | ||
| >>> _cross_product(origin, point_a, point_b) < 0 | ||
| True | ||
| >>> _cross_product(origin, Point(1, 0), Point(2, 0)) == 0 | ||
| True | ||
| >>> _cross_product(origin, Point(1, 0), Point(1, 1)) > 0 | ||
| True | ||
| """ | ||
| 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. | ||
|
|
||
| >>> _is_point_on_segment(Point(0, 0), Point(2, 2), Point(1, 1)) | ||
| True | ||
| >>> _is_point_on_segment(Point(0, 0), Point(2, 2), Point(3, 3)) | ||
| False | ||
| >>> _is_point_on_segment(Point(0, 0), Point(2, 0), Point(1, 0)) | ||
| True | ||
| """ | ||
| # 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 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. | ||
|
|
||
| Examples: | ||
| >>> # Triangle | ||
| >>> p1, p2, p3 = Point(1, 1), Point(2, 1), Point(1.5, 2) | ||
| >>> hull = jarvis_march([p1, p2, p3]) | ||
| >>> len(hull) | ||
| 3 | ||
| >>> all(p in hull for p in [p1, p2, p3]) | ||
| True | ||
|
|
||
| >>> # Collinear points return empty hull | ||
| >>> points = [Point(i, 0) for i in range(5)] | ||
| >>> jarvis_march(points) | ||
| [] | ||
|
|
||
| >>> # Rectangle with interior point - interior point excluded | ||
| >>> p1, p2 = Point(1, 1), Point(2, 1) | ||
| >>> p3, p4 = Point(2, 2), Point(1, 2) | ||
| >>> p5 = Point(1.5, 1.5) | ||
| >>> hull = jarvis_march([p1, p2, p3, p4, p5]) | ||
| >>> len(hull) | ||
| 4 | ||
| >>> p5 in hull | ||
| False | ||
|
|
||
| >>> # Star shape - only tips are in hull | ||
| >>> tips = [ | ||
| ... Point(-5, 6), Point(-11, 0), Point(-9, -8), | ||
| ... Point(4, 4), Point(6, -7) | ||
| ... ] | ||
| >>> interior = [Point(-7, -2), Point(-2, -4), Point(0, 1)] | ||
| >>> hull = jarvis_march(tips + interior) | ||
| >>> len(hull) | ||
| 5 | ||
| >>> all(p in hull for p in tips) | ||
| True | ||
| >>> any(p in hull for p in interior) | ||
| False | ||
|
|
||
| >>> # Too few points | ||
| >>> jarvis_march([]) | ||
| [] | ||
| >>> jarvis_march([Point(0, 0)]) | ||
| [] | ||
| >>> jarvis_march([Point(0, 0), Point(1, 1)]) | ||
| [] | ||
| """ | ||
| if len(points) <= 2: | ||
| return [] | ||
|
|
||
| convex_hull: list[Point] = [] | ||
|
|
||
| # Find the leftmost point (and bottom-most in case of tie) | ||
| left_point_idx = 0 | ||
| for i in range(1, len(points)): | ||
| if points[i].x < points[left_point_idx].x or ( | ||
| points[i].x == points[left_point_idx].x | ||
| and points[i].y < points[left_point_idx].y | ||
| ): | ||
| left_point_idx = i | ||
|
|
||
| convex_hull.append(Point(points[left_point_idx].x, points[left_point_idx].y)) | ||
|
|
||
| current_idx = left_point_idx | ||
| while True: | ||
| # Find the next counter-clockwise point | ||
| next_idx = (current_idx + 1) % len(points) | ||
| for i in range(len(points)): | ||
| if _cross_product(points[current_idx], points[i], points[next_idx]) > 0: | ||
| next_idx = i | ||
|
|
||
| if next_idx == left_point_idx: | ||
| # Completed constructing the hull | ||
| break | ||
|
|
||
| current_idx = next_idx | ||
|
|
||
| # Check if the last point is collinear with new point and second-to-last | ||
| last = len(convex_hull) - 1 | ||
| if len(convex_hull) > 1 and _is_point_on_segment( | ||
| points[current_idx], convex_hull[last - 1], convex_hull[last] | ||
| ): | ||
| # Remove the last point from the hull | ||
| convex_hull[last] = Point(points[current_idx].x, points[current_idx].y) | ||
| else: | ||
| convex_hull.append(Point(points[current_idx].x, points[current_idx].y)) | ||
|
|
||
| # Check for edge case: last point collinear with first and second-to-last | ||
| if len(convex_hull) <= 2: | ||
| return [] | ||
|
|
||
| last = len(convex_hull) - 1 | ||
| if _is_point_on_segment(convex_hull[0], convex_hull[last - 1], convex_hull[last]): | ||
| convex_hull.pop() | ||
| if len(convex_hull) == 2: | ||
| return [] | ||
|
|
||
| return convex_hull | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| import doctest | ||
|
|
||
| doctest.testmod() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this correct?