Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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):
"""Test Point initialization."""
p = Point(1.0, 2.0)
assert p.x == 1.0
assert p.y == 2.0

def test_point_equality(self):
"""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):
"""Test Point string representation."""
p = Point(1.5, 2.5)
assert repr(p) == "Point(1.5, 2.5)"

def test_point_hash(self):
"""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):
"""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):
"""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):
"""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):
"""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):
"""Test empty list returns empty hull."""
assert jarvis_march([]) == []

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

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

def test_square(self):
"""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):
"""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):
"""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)