Skip to content

Commit 7202b49

Browse files
committed
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
1 parent e1773e9 commit 7202b49

File tree

2 files changed

+167
-10
lines changed

2 files changed

+167
-10
lines changed

src/main/java/com/thealgorithms/geometry/ConvexHull.java

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,24 @@ public static List<Point> convexHullBruteForce(List<Point> points) {
6161
return new ArrayList<>(convexSet);
6262
}
6363

64+
/**
65+
* Computes the convex hull using a recursive divide-and-conquer approach.
66+
* Returns points in counter-clockwise order starting from the bottom-most, left-most point.
67+
*
68+
* @param points the input points
69+
* @return the convex hull points in counter-clockwise order
70+
*/
6471
public static List<Point> convexHullRecursive(List<Point> points) {
72+
if (points.size() < 3) {
73+
List<Point> result = new ArrayList<>(points);
74+
Collections.sort(result);
75+
return result;
76+
}
77+
6578
Collections.sort(points);
6679
Set<Point> convexSet = new HashSet<>();
67-
Point leftMostPoint = points.get(0);
68-
Point rightMostPoint = points.get(points.size() - 1);
80+
Point leftMostPoint = points.getFirst();
81+
Point rightMostPoint = points.getLast();
6982

7083
convexSet.add(leftMostPoint);
7184
convexSet.add(rightMostPoint);
@@ -85,9 +98,8 @@ public static List<Point> convexHullRecursive(List<Point> points) {
8598
constructHull(upperHull, leftMostPoint, rightMostPoint, convexSet);
8699
constructHull(lowerHull, rightMostPoint, leftMostPoint, convexSet);
87100

88-
List<Point> result = new ArrayList<>(convexSet);
89-
Collections.sort(result);
90-
return result;
101+
// Convert to list and sort in counter-clockwise order
102+
return sortCounterClockwise(new ArrayList<>(convexSet));
91103
}
92104

93105
private static void constructHull(Collection<Point> points, Point left, Point right, Set<Point> convexSet) {
@@ -114,4 +126,62 @@ private static void constructHull(Collection<Point> points, Point left, Point ri
114126
}
115127
}
116128
}
129+
130+
/**
131+
* Sorts convex hull points in counter-clockwise order starting from
132+
* the bottom-most, left-most point.
133+
*
134+
* @param hullPoints the unsorted convex hull points
135+
* @return the points sorted in counter-clockwise order
136+
*/
137+
private static List<Point> sortCounterClockwise(List<Point> hullPoints) {
138+
if (hullPoints.size() <= 2) {
139+
Collections.sort(hullPoints);
140+
return hullPoints;
141+
}
142+
143+
// Find the bottom-most, left-most point (pivot)
144+
Point pivot = hullPoints.getFirst();
145+
for (Point p : hullPoints) {
146+
if (p.y() < pivot.y() || (p.y() == pivot.y() && p.x() < pivot.x())) {
147+
pivot = p;
148+
}
149+
}
150+
151+
// Sort other points by polar angle with respect to pivot
152+
final Point finalPivot = pivot;
153+
List<Point> sorted = new ArrayList<>(hullPoints);
154+
sorted.remove(finalPivot);
155+
156+
sorted.sort((p1, p2) -> {
157+
int crossProduct = Point.orientation(finalPivot, p1, p2);
158+
159+
if (crossProduct == 0) {
160+
// Collinear points: sort by distance from pivot (closer first for convex hull)
161+
long dist1 = distanceSquared(finalPivot, p1);
162+
long dist2 = distanceSquared(finalPivot, p2);
163+
return Long.compare(dist1, dist2);
164+
}
165+
166+
// Positive cross product means p2 is counter-clockwise from p1
167+
// We want counter-clockwise order, so if p2 is CCW from p1, p1 should come first
168+
return -crossProduct;
169+
});
170+
171+
// Build result with pivot first
172+
List<Point> result = new ArrayList<>();
173+
result.add(finalPivot);
174+
result.addAll(sorted);
175+
176+
return result;
177+
}
178+
179+
/**
180+
* Computes the squared distance between two points to avoid floating point operations.
181+
*/
182+
private static long distanceSquared(Point p1, Point p2) {
183+
long dx = (long) p1.x() - p2.x();
184+
long dy = (long) p1.y() - p2.y();
185+
return dx * dx + dy * dy;
186+
}
117187
}
Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.thealgorithms.geometry;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
45

56
import java.util.Arrays;
67
import java.util.List;
@@ -10,31 +11,117 @@ public class ConvexHullTest {
1011

1112
@Test
1213
void testConvexHullBruteForce() {
14+
// Test 1: Triangle with intermediate point
1315
List<Point> points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1));
1416
List<Point> expected = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1));
1517
assertEquals(expected, ConvexHull.convexHullBruteForce(points));
1618

19+
// Test 2: Collinear points
1720
points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 0));
1821
expected = Arrays.asList(new Point(0, 0), new Point(10, 0));
1922
assertEquals(expected, ConvexHull.convexHullBruteForce(points));
2023

24+
// Test 3: Complex polygon
2125
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));
2226
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));
2327
assertEquals(expected, ConvexHull.convexHullBruteForce(points));
2428
}
2529

2630
@Test
2731
void testConvexHullRecursive() {
32+
// Test 1: Triangle - CCW order starting from bottom-left
33+
// The algorithm includes (1,0) as it's detected as an extreme point
2834
List<Point> points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1));
35+
List<Point> result = ConvexHull.convexHullRecursive(points);
2936
List<Point> expected = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 1));
30-
assertEquals(expected, ConvexHull.convexHullRecursive(points));
37+
assertEquals(expected, result);
38+
assertTrue(isCounterClockwise(result), "Points should be in counter-clockwise order");
3139

40+
// Test 2: Collinear points
3241
points = Arrays.asList(new Point(0, 0), new Point(1, 0), new Point(10, 0));
42+
result = ConvexHull.convexHullRecursive(points);
3343
expected = Arrays.asList(new Point(0, 0), new Point(10, 0));
34-
assertEquals(expected, ConvexHull.convexHullRecursive(points));
44+
assertEquals(expected, result);
3545

36-
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));
37-
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));
38-
assertEquals(expected, ConvexHull.convexHullRecursive(points));
46+
// Test 3: Complex polygon
47+
// Convex hull vertices in CCW order from bottom-most point (2,-4):
48+
// (2,-4) -> (3,0) -> (3,3) -> (0,3) -> (0,0) -> (1,-3) -> back to (2,-4)
49+
points = Arrays.asList(
50+
new Point(0, 3), new Point(2, 2), new Point(1, 1),
51+
new Point(2, 1), new Point(3, 0), new Point(0, 0),
52+
new Point(3, 3), new Point(2, -1), new Point(2, -4),
53+
new Point(1, -3)
54+
);
55+
result = ConvexHull.convexHullRecursive(points);
56+
expected = Arrays.asList(
57+
new Point(2, -4), // Bottom-most, left-most (starting point)
58+
new Point(3, 0), // Right side going up
59+
new Point(3, 3), // Top right corner
60+
new Point(0, 3), // Top left corner
61+
new Point(0, 0), // Left side coming down
62+
new Point(1, -3) // Bottom section, back towards start
63+
);
64+
assertEquals(expected, result);
65+
assertTrue(isCounterClockwise(result), "Points should be in counter-clockwise order");
66+
}
67+
68+
@Test
69+
void testConvexHullRecursiveAdditionalCases() {
70+
// Test 4: Square (all corners on hull)
71+
List<Point> points = Arrays.asList(
72+
new Point(0, 0), new Point(2, 0),
73+
new Point(2, 2), new Point(0, 2)
74+
);
75+
List<Point> result = ConvexHull.convexHullRecursive(points);
76+
List<Point> expected = Arrays.asList(
77+
new Point(0, 0), new Point(2, 0),
78+
new Point(2, 2), new Point(0, 2)
79+
);
80+
assertEquals(expected, result);
81+
assertTrue(isCounterClockwise(result), "Square points should be in CCW order");
82+
83+
// Test 5: Pentagon with interior point
84+
points = Arrays.asList(
85+
new Point(0, 0), new Point(4, 0), new Point(5, 3),
86+
new Point(2, 5), new Point(-1, 3), new Point(2, 2) // (2,2) is interior
87+
);
88+
result = ConvexHull.convexHullRecursive(points);
89+
// CCW from (0,0): (0,0) -> (4,0) -> (5,3) -> (2,5) -> (-1,3)
90+
expected = Arrays.asList(
91+
new Point(0, 0), new Point(4, 0), new Point(5, 3),
92+
new Point(2, 5), new Point(-1, 3)
93+
);
94+
assertEquals(expected, result);
95+
assertTrue(isCounterClockwise(result), "Pentagon points should be in CCW order");
96+
97+
// Test 6: Simple triangle (clearly convex)
98+
points = Arrays.asList(
99+
new Point(0, 0), new Point(4, 0), new Point(2, 3)
100+
);
101+
result = ConvexHull.convexHullRecursive(points);
102+
expected = Arrays.asList(
103+
new Point(0, 0), new Point(4, 0), new Point(2, 3)
104+
);
105+
assertEquals(expected, result);
106+
assertTrue(isCounterClockwise(result), "Triangle points should be in CCW order");
107+
}
108+
109+
/**
110+
* Helper method to verify if points are in counter-clockwise order.
111+
* Uses the signed area method: positive area means CCW.
112+
*/
113+
private boolean isCounterClockwise(List<Point> points) {
114+
if (points.size() < 3) {
115+
return true; // Less than 3 points, trivially true
116+
}
117+
118+
long signedArea = 0;
119+
for (int i = 0; i < points.size(); i++) {
120+
Point p1 = points.get(i);
121+
Point p2 = points.get((i + 1) % points.size());
122+
signedArea += (long) p1.x() * p2.y() - (long) p2.x() * p1.y();
123+
}
124+
125+
return signedArea > 0; // Positive signed area means counter-clockwise
39126
}
40127
}

0 commit comments

Comments
 (0)