Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion data_structures/hashing/hash_table_with_linked_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def _set_value(self, key, data):
self.values[key] = deque([]) if self.values[key] is None else self.values[key]
self.values[key] = deque() if self.values[key] is None else self.values[key]
self.values[key].appendleft(data)
self._keys[key] = self.values[key]

Expand Down
142 changes: 142 additions & 0 deletions geometry/jarvis_march.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""
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 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 []

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(
convex_hull[last - 1], convex_hull[last], points[current_idx]
):
# 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[last - 1], convex_hull[last], convex_hull[0]):
convex_hull.pop()
if len(convex_hull) == 2:
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}")
115 changes: 115 additions & 0 deletions geometry/jarvis_march_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
Unit tests for Jarvis March (Gift Wrapping) algorithm.
"""

from geometry.jarvis_march import Point, jarvis_march


class TestPoint:
"""Tests for the Point class."""

def test_point_creation(self) -> None:
"""Test Point initialization."""
p = Point(1.0, 2.0)
assert p.x == 1.0
assert p.y == 2.0

def test_point_equality(self) -> None:
"""Test Point equality comparison."""
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
p3 = Point(2.0, 1.0)
assert p1 == p2
assert p1 != p3

def test_point_repr(self) -> None:
"""Test Point string representation."""
p = Point(1.5, 2.5)
assert repr(p) == "Point(1.5, 2.5)"

def test_point_hash(self) -> None:
"""Test Point hashing."""
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
assert hash(p1) == hash(p2)


class TestJarvisMarch:
"""Tests for the jarvis_march function."""

def test_triangle(self) -> None:
"""Test convex hull of a triangle."""
p1, p2, p3 = Point(1, 1), Point(2, 1), Point(1.5, 2)
hull = jarvis_march([p1, p2, p3])
assert len(hull) == 3
assert all(p in hull for p in [p1, p2, p3])

def test_collinear_points(self) -> None:
"""Test that collinear points return empty hull."""
points = [Point(i, 0) for i in range(5)]
hull = jarvis_march(points)
assert hull == []

def test_rectangle_with_interior_point(self) -> None:
"""Test 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])
assert len(hull) == 4
assert p5 not in hull

def test_star_shape(self) -> None:
"""Test 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)
assert len(hull) == 5
assert all(p in hull for p in tips)
assert not any(p in hull for p in interior)

def test_empty_list(self) -> None:
"""Test empty list returns empty hull."""
assert jarvis_march([]) == []

def test_single_point(self) -> None:
"""Test single point returns empty hull."""
assert jarvis_march([Point(0, 0)]) == []

def test_two_points(self) -> None:
"""Test two points return empty hull."""
assert jarvis_march([Point(0, 0), Point(1, 1)]) == []

def test_square(self) -> None:
"""Test convex hull of a square."""
p1, p2 = Point(0, 0), Point(1, 0)
p3, p4 = Point(1, 1), Point(0, 1)
hull = jarvis_march([p1, p2, p3, p4])
assert len(hull) == 4
assert all(p in hull for p in [p1, p2, p3, p4])

def test_duplicate_points(self) -> None:
"""Test handling of duplicate points."""
p1, p2, p3 = Point(0, 0), Point(1, 0), Point(0, 1)
points = [p1, p2, p3, p1, p2] # Include duplicates
hull = jarvis_march(points)
assert len(hull) == 3

def test_pentagon(self) -> None:
"""Test convex hull of a pentagon."""
points = [
Point(0, 1),
Point(1, 2),
Point(2, 1),
Point(1.5, 0),
Point(0.5, 0),
]
hull = jarvis_march(points)
assert len(hull) == 5
assert all(p in hull for p in points)