From 7202b49d9e18b1d6fdee66e170ba59c7597a6dc1 Mon Sep 17 00:00:00 2001 From: Microindole <1513979779@qq.com> Date: Fri, 17 Oct 2025 10:56:12 +0800 Subject: [PATCH 1/2] Fix ConvexHull to return points in counter-clockwise order - Add sortCounterClockwise method to ensure CCW ordering - Start from bottom-most, left-most point for deterministic results - Fix issue where unordered HashSet broke downstream algorithms - Add comprehensive tests with CCW order verification --- .../thealgorithms/geometry/ConvexHull.java | 80 ++++++++++++++- .../geometry/ConvexHullTest.java | 97 ++++++++++++++++++- 2 files changed, 167 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/thealgorithms/geometry/ConvexHull.java b/src/main/java/com/thealgorithms/geometry/ConvexHull.java index 17f400854c64..80ca167139d5 100644 --- a/src/main/java/com/thealgorithms/geometry/ConvexHull.java +++ b/src/main/java/com/thealgorithms/geometry/ConvexHull.java @@ -61,11 +61,24 @@ public static List convexHullBruteForce(List points) { return new ArrayList<>(convexSet); } + /** + * Computes the convex hull using a recursive divide-and-conquer approach. + * Returns points in counter-clockwise order starting from the bottom-most, left-most point. + * + * @param points the input points + * @return the convex hull points in counter-clockwise order + */ public static List convexHullRecursive(List points) { + if (points.size() < 3) { + List result = new ArrayList<>(points); + Collections.sort(result); + return result; + } + Collections.sort(points); Set convexSet = new HashSet<>(); - Point leftMostPoint = points.get(0); - Point rightMostPoint = points.get(points.size() - 1); + Point leftMostPoint = points.getFirst(); + Point rightMostPoint = points.getLast(); convexSet.add(leftMostPoint); convexSet.add(rightMostPoint); @@ -85,9 +98,8 @@ public static List convexHullRecursive(List points) { constructHull(upperHull, leftMostPoint, rightMostPoint, convexSet); constructHull(lowerHull, rightMostPoint, leftMostPoint, convexSet); - List result = new ArrayList<>(convexSet); - Collections.sort(result); - return result; + // Convert to list and sort in counter-clockwise order + return sortCounterClockwise(new ArrayList<>(convexSet)); } private static void constructHull(Collection points, Point left, Point right, Set convexSet) { @@ -114,4 +126,62 @@ private static void constructHull(Collection points, Point left, Point ri } } } + + /** + * Sorts convex hull points in counter-clockwise order starting from + * the bottom-most, left-most point. + * + * @param hullPoints the unsorted convex hull points + * @return the points sorted in counter-clockwise order + */ + private static List sortCounterClockwise(List hullPoints) { + if (hullPoints.size() <= 2) { + Collections.sort(hullPoints); + return hullPoints; + } + + // Find the bottom-most, left-most point (pivot) + Point pivot = hullPoints.getFirst(); + for (Point p : hullPoints) { + if (p.y() < pivot.y() || (p.y() == pivot.y() && p.x() < pivot.x())) { + pivot = p; + } + } + + // Sort other points by polar angle with respect to pivot + final Point finalPivot = pivot; + List sorted = new ArrayList<>(hullPoints); + sorted.remove(finalPivot); + + sorted.sort((p1, p2) -> { + int crossProduct = Point.orientation(finalPivot, p1, p2); + + if (crossProduct == 0) { + // Collinear points: sort by distance from pivot (closer first for convex hull) + long dist1 = distanceSquared(finalPivot, p1); + long dist2 = distanceSquared(finalPivot, p2); + return Long.compare(dist1, dist2); + } + + // Positive cross product means p2 is counter-clockwise from p1 + // We want counter-clockwise order, so if p2 is CCW from p1, p1 should come first + return -crossProduct; + }); + + // Build result with pivot first + List result = new ArrayList<>(); + result.add(finalPivot); + result.addAll(sorted); + + return result; + } + + /** + * Computes the squared distance between two points to avoid floating point operations. + */ + private static long distanceSquared(Point p1, Point p2) { + long dx = (long) p1.x() - p2.x(); + long dy = (long) p1.y() - p2.y(); + return dx * dx + dy * dy; + } } diff --git a/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java b/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java index e3e32e43c6de..8e2a2eeb6dba 100644 --- a/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java +++ b/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java @@ -1,6 +1,7 @@ package com.thealgorithms.geometry; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; import java.util.List; @@ -10,14 +11,17 @@ public class ConvexHullTest { @Test void testConvexHullBruteForce() { + // Test 1: Triangle with intermediate point List points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1)); List expected = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1)); assertEquals(expected, ConvexHull.convexHullBruteForce(points)); + // Test 2: Collinear points points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 0)); expected = Arrays.asList(new Point(0, 0), new Point(10, 0)); assertEquals(expected, ConvexHull.convexHullBruteForce(points)); + // Test 3: Complex polygon points = Arrays.asList(new Point(0, 3), new Point(2, 2), new Point(1, 1), new Point(2, 1), new Point(3, 0), new Point(0, 0), new Point(3, 3), new Point(2, -1), new Point(2, -4), new Point(1, -3)); expected = Arrays.asList(new Point(2, -4), new Point(1, -3), new Point(0, 0), new Point(3, 0), new Point(0, 3), new Point(3, 3)); assertEquals(expected, ConvexHull.convexHullBruteForce(points)); @@ -25,16 +29,99 @@ void testConvexHullBruteForce() { @Test void testConvexHullRecursive() { + // Test 1: Triangle - CCW order starting from bottom-left + // The algorithm includes (1,0) as it's detected as an extreme point List points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1)); + List result = ConvexHull.convexHullRecursive(points); List expected = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1)); - assertEquals(expected, ConvexHull.convexHullRecursive(points)); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Points should be in counter-clockwise order"); + // Test 2: Collinear points points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 0)); + result = ConvexHull.convexHullRecursive(points); expected = Arrays.asList(new Point(0, 0), new Point(10, 0)); - assertEquals(expected, ConvexHull.convexHullRecursive(points)); + assertEquals(expected, result); - points = Arrays.asList(new Point(0, 3), new Point(2, 2), new Point(1, 1), new Point(2, 1), new Point(3, 0), new Point(0, 0), new Point(3, 3), new Point(2, -1), new Point(2, -4), new Point(1, -3)); - expected = Arrays.asList(new Point(2, -4), new Point(1, -3), new Point(0, 0), new Point(3, 0), new Point(0, 3), new Point(3, 3)); - assertEquals(expected, ConvexHull.convexHullRecursive(points)); + // Test 3: Complex polygon + // Convex hull vertices in CCW order from bottom-most point (2,-4): + // (2,-4) -> (3,0) -> (3,3) -> (0,3) -> (0,0) -> (1,-3) -> back to (2,-4) + points = Arrays.asList( + new Point(0, 3), new Point(2, 2), new Point(1, 1), + new Point(2, 1), new Point(3, 0), new Point(0, 0), + new Point(3, 3), new Point(2, -1), new Point(2, -4), + new Point(1, -3) + ); + result = ConvexHull.convexHullRecursive(points); + expected = Arrays.asList( + new Point(2, -4), // Bottom-most, left-most (starting point) + new Point(3, 0), // Right side going up + new Point(3, 3), // Top right corner + new Point(0, 3), // Top left corner + new Point(0, 0), // Left side coming down + new Point(1, -3) // Bottom section, back towards start + ); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Points should be in counter-clockwise order"); + } + + @Test + void testConvexHullRecursiveAdditionalCases() { + // Test 4: Square (all corners on hull) + List points = Arrays.asList( + new Point(0, 0), new Point(2, 0), + new Point(2, 2), new Point(0, 2) + ); + List result = ConvexHull.convexHullRecursive(points); + List expected = Arrays.asList( + new Point(0, 0), new Point(2, 0), + new Point(2, 2), new Point(0, 2) + ); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Square points should be in CCW order"); + + // Test 5: Pentagon with interior point + points = Arrays.asList( + new Point(0, 0), new Point(4, 0), new Point(5, 3), + new Point(2, 5), new Point(-1, 3), new Point(2, 2) // (2,2) is interior + ); + result = ConvexHull.convexHullRecursive(points); + // CCW from (0,0): (0,0) -> (4,0) -> (5,3) -> (2,5) -> (-1,3) + expected = Arrays.asList( + new Point(0, 0), new Point(4, 0), new Point(5, 3), + new Point(2, 5), new Point(-1, 3) + ); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Pentagon points should be in CCW order"); + + // Test 6: Simple triangle (clearly convex) + points = Arrays.asList( + new Point(0, 0), new Point(4, 0), new Point(2, 3) + ); + result = ConvexHull.convexHullRecursive(points); + expected = Arrays.asList( + new Point(0, 0), new Point(4, 0), new Point(2, 3) + ); + assertEquals(expected, result); + assertTrue(isCounterClockwise(result), "Triangle points should be in CCW order"); + } + + /** + * Helper method to verify if points are in counter-clockwise order. + * Uses the signed area method: positive area means CCW. + */ + private boolean isCounterClockwise(List points) { + if (points.size() < 3) { + return true; // Less than 3 points, trivially true + } + + long signedArea = 0; + for (int i = 0; i < points.size(); i++) { + Point p1 = points.get(i); + Point p2 = points.get((i + 1) % points.size()); + signedArea += (long) p1.x() * p2.y() - (long) p2.x() * p1.y(); + } + + return signedArea > 0; // Positive signed area means counter-clockwise } } From e690d0b3d9d87ca7e3d3053167d545af92b8c28a Mon Sep 17 00:00:00 2001 From: Microindole <1513979779@qq.com> Date: Fri, 17 Oct 2025 13:02:54 +0800 Subject: [PATCH 2/2] test(geometry): Achieve 100% test coverage for ConvexHull --- .../thealgorithms/geometry/ConvexHull.java | 24 +++++- .../geometry/ConvexHullTest.java | 81 +++++++++++-------- 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/thealgorithms/geometry/ConvexHull.java b/src/main/java/com/thealgorithms/geometry/ConvexHull.java index 80ca167139d5..d44d20c68807 100644 --- a/src/main/java/com/thealgorithms/geometry/ConvexHull.java +++ b/src/main/java/com/thealgorithms/geometry/ConvexHull.java @@ -168,10 +168,30 @@ private static List sortCounterClockwise(List hullPoints) { return -crossProduct; }); - // Build result with pivot first + // Build result with pivot first, filtering out intermediate collinear points List result = new ArrayList<>(); result.add(finalPivot); - result.addAll(sorted); + + if (!sorted.isEmpty()) { + // This loop iterates through the points sorted by angle. + // For points that are collinear with the pivot, we only want the one that is farthest away. + // The sort places closer points first. + for (int i = 0; i < sorted.size() - 1; i++) { + // Check the orientation of the pivot, the current point, and the next point. + int orientation = Point.orientation(finalPivot, sorted.get(i), sorted.get(i + 1)); + + // If the orientation is not 0, it means the next point (i+1) is at a new angle. + // Therefore, the current point (i) must be the farthest point at its angle. We keep it. + if (orientation != 0) { + result.add(sorted.get(i)); + } + // If the orientation is 0, the points are collinear. We discard the current point (i) + // because it is closer to the pivot than the next point (i+1). + } + // Always add the very last point from the sorted list. It is either the only point + // at its angle, or it's the farthest among a set of collinear points. + result.add(sorted.getLast()); + } return result; } diff --git a/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java b/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java index 8e2a2eeb6dba..d3ca0df65829 100644 --- a/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java +++ b/src/test/java/com/thealgorithms/geometry/ConvexHullTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; @@ -46,21 +47,9 @@ void testConvexHullRecursive() { // Test 3: Complex polygon // Convex hull vertices in CCW order from bottom-most point (2,-4): // (2,-4) -> (3,0) -> (3,3) -> (0,3) -> (0,0) -> (1,-3) -> back to (2,-4) - points = Arrays.asList( - new Point(0, 3), new Point(2, 2), new Point(1, 1), - new Point(2, 1), new Point(3, 0), new Point(0, 0), - new Point(3, 3), new Point(2, -1), new Point(2, -4), - new Point(1, -3) - ); + points = Arrays.asList(new Point(0, 3), new Point(2, 2), new Point(1, 1), new Point(2, 1), new Point(3, 0), new Point(0, 0), new Point(3, 3), new Point(2, -1), new Point(2, -4), new Point(1, -3)); result = ConvexHull.convexHullRecursive(points); - expected = Arrays.asList( - new Point(2, -4), // Bottom-most, left-most (starting point) - new Point(3, 0), // Right side going up - new Point(3, 3), // Top right corner - new Point(0, 3), // Top left corner - new Point(0, 0), // Left side coming down - new Point(1, -3) // Bottom section, back towards start - ); + expected = Arrays.asList(new Point(2, -4), new Point(3, 0), new Point(3, 3), new Point(0, 3), new Point(0, 0), new Point(1, -3)); assertEquals(expected, result); assertTrue(isCounterClockwise(result), "Points should be in counter-clockwise order"); } @@ -68,40 +57,25 @@ void testConvexHullRecursive() { @Test void testConvexHullRecursiveAdditionalCases() { // Test 4: Square (all corners on hull) - List points = Arrays.asList( - new Point(0, 0), new Point(2, 0), - new Point(2, 2), new Point(0, 2) - ); + List points = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(2, 2), new Point(0, 2)); List result = ConvexHull.convexHullRecursive(points); - List expected = Arrays.asList( - new Point(0, 0), new Point(2, 0), - new Point(2, 2), new Point(0, 2) - ); + List expected = Arrays.asList(new Point(0, 0), new Point(2, 0), new Point(2, 2), new Point(0, 2)); assertEquals(expected, result); assertTrue(isCounterClockwise(result), "Square points should be in CCW order"); // Test 5: Pentagon with interior point - points = Arrays.asList( - new Point(0, 0), new Point(4, 0), new Point(5, 3), - new Point(2, 5), new Point(-1, 3), new Point(2, 2) // (2,2) is interior + points = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(5, 3), new Point(2, 5), new Point(-1, 3), new Point(2, 2) // (2,2) is interior ); result = ConvexHull.convexHullRecursive(points); // CCW from (0,0): (0,0) -> (4,0) -> (5,3) -> (2,5) -> (-1,3) - expected = Arrays.asList( - new Point(0, 0), new Point(4, 0), new Point(5, 3), - new Point(2, 5), new Point(-1, 3) - ); + expected = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(5, 3), new Point(2, 5), new Point(-1, 3)); assertEquals(expected, result); assertTrue(isCounterClockwise(result), "Pentagon points should be in CCW order"); // Test 6: Simple triangle (clearly convex) - points = Arrays.asList( - new Point(0, 0), new Point(4, 0), new Point(2, 3) - ); + points = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3)); result = ConvexHull.convexHullRecursive(points); - expected = Arrays.asList( - new Point(0, 0), new Point(4, 0), new Point(2, 3) - ); + expected = Arrays.asList(new Point(0, 0), new Point(4, 0), new Point(2, 3)); assertEquals(expected, result); assertTrue(isCounterClockwise(result), "Triangle points should be in CCW order"); } @@ -124,4 +98,41 @@ private boolean isCounterClockwise(List points) { return signedArea > 0; // Positive signed area means counter-clockwise } + + @Test + void testRecursiveHullForCoverage() { + // 1. Test the base cases of the convexHullRecursive method (covering scenarios with < 3 input points). + + // Test Case: 0 points + List pointsEmpty = new ArrayList<>(); + List resultEmpty = ConvexHull.convexHullRecursive(pointsEmpty); + assertTrue(resultEmpty.isEmpty(), "Should return an empty list for an empty input list"); + + // Test Case: 1 point + List pointsOne = List.of(new Point(5, 5)); + // Pass a new ArrayList because the original method modifies the input list. + List resultOne = ConvexHull.convexHullRecursive(new ArrayList<>(pointsOne)); + List expectedOne = List.of(new Point(5, 5)); + assertEquals(expectedOne, resultOne, "Should return the single point for a single-point input"); + + // Test Case: 2 points + List pointsTwo = Arrays.asList(new Point(10, 1), new Point(0, 0)); + List resultTwo = ConvexHull.convexHullRecursive(new ArrayList<>(pointsTwo)); + List expectedTwo = Arrays.asList(new Point(0, 0), new Point(10, 1)); // Should return the two points, sorted. + assertEquals(expectedTwo, resultTwo, "Should return the two sorted points for a two-point input"); + + // 2. Test the logic for handling collinear points in the sortCounterClockwise method. + + // Construct a scenario where multiple collinear points lie on an edge of the convex hull. + // The expected convex hull vertices are (0,0), (10,0), and (5,5). + // When (0,0) is used as the pivot for polar angle sorting, (5,0) and (10,0) are collinear. + // This will trigger the crossProduct == 0 branch in the sortCounterClockwise method. + List pointsWithCollinearOnHull = Arrays.asList(new Point(0, 0), new Point(5, 0), new Point(10, 0), new Point(5, 5), new Point(2, 2)); + + List resultCollinear = ConvexHull.convexHullRecursive(new ArrayList<>(pointsWithCollinearOnHull)); + List expectedCollinear = Arrays.asList(new Point(0, 0), new Point(10, 0), new Point(5, 5)); + + assertEquals(expectedCollinear, resultCollinear, "Should correctly handle collinear points on the hull edge"); + assertTrue(isCounterClockwise(resultCollinear), "The result of the collinear test should be in counter-clockwise order"); + } }