Convex hull is the smallest convex polygon containing all points.
Graham Scan sorts points by polar angle (O(n log n)) then sweeps with a stack.
Jarvis March picks the most counterclockwise neighbor each step (O(nh)).
Cross product > 0 = left turn (keep); < 0 = right turn (pop).
Collinear handling can break algorithms if not managed explicitly.
Production precision: use epsilon comparisons, not == 0, to avoid floating-point edge cases.
✦ Definition~90s read
What is Convex Hull?
A convex hull is the smallest convex polygon that encloses a set of points in a plane — think of it as the shape a rubber band would take if stretched around all the points. It's a fundamental primitive in computational geometry, used everywhere from collision detection in game engines to cluster analysis in machine learning pipelines.
★
Given a set of points, the convex hull is the smallest convex polygon containing all of them — imagine stretching a rubber band around all the points and letting it snap.
The two classic algorithms for computing it are Graham Scan (O(n log n)) and Jarvis March (O(nh), where h is the number of hull vertices). Both rely on the cross product to determine whether three points form a left turn, a right turn, or are collinear — and that's where things break down.
Collinear points — three or more points lying on the same line — are the silent killers of hull detection. Most textbook implementations assume general position (no three points collinear), but real-world data is messy. When collinear points sit on the hull boundary, naive algorithms can include interior points, miss boundary points, or produce degenerate polygons with zero-area edges.
The cross product returns zero for collinear points, and how you handle that zero determines whether your hull is robust or garbage. Graham Scan's sorting step can also misorder collinear points, leading to incorrect stack behavior.
Floating-point precision compounds the problem. Cross products near zero due to nearly-collinear points can flip sign due to rounding errors, causing algorithms to misclassify turns. This is why production systems — like those in CAD software or geospatial databases — use epsilon thresholds and robust predicates (e.g., Shewchuk's adaptive precision).
If you're implementing convex hull for a robotics path planner or a GIS tool, you must explicitly handle collinear edge cases and floating-point tolerance, or your hull will silently produce wrong results that are hard to debug.
Plain-English First
Given a set of points, the convex hull is the smallest convex polygon containing all of them — imagine stretching a rubber band around all the points and letting it snap. Graham Scan starts from the lowest point and sweeps counterclockwise, always maintaining a left-turning boundary. Jarvis March is like gift wrapping — always pick the point that makes the smallest left turn from your current direction.
Convex hull is the gateway algorithm to computational geometry. It appears in collision detection (is object A inside the convex hull of object B?), pathfinding (compute the hull to find the shortest path around an obstacle), computer vision (shape descriptors), and machine learning (support vector machine margin computation).
Graham scan and Jarvis march solve the same problem with different trade-offs. Graham scan is O(n log n) regardless of hull size — optimal for large point clouds. Jarvis march is O(nh) where h is the hull output size — better when the hull has very few points (h << log n). Knowing this trade-off and when it matters is what distinguishes thorough geometry knowledge.
Why Convex Hull Algorithms Fail on Collinear Points
A convex hull is the smallest convex polygon that encloses a set of points in the plane. The core mechanic: given n points, the hull is the subset that forms the boundary of the convex set — every interior point is a convex combination of hull vertices. The problem is that collinear points (three or more points lying on the same edge) break the strict orientation test most algorithms rely on.
Most hull algorithms — Graham scan, Andrew's monotone chain — use cross-product orientation checks to decide whether a turn is left, right, or collinear. When points are collinear, the cross product is zero. Naive implementations treat zero as either left or right, causing vertices to be incorrectly included or excluded. The result: a hull that is either non-convex, missing boundary points, or includes redundant interior points. The fix is a consistent tie-breaking rule: either include all collinear boundary points (maximal hull) or exclude all but the endpoints (minimal hull).
Use convex hull when you need to compute the outer boundary of a point cloud — collision detection, shape analysis, or geographic clustering. In production, the cost of mishandling collinear points is silent corruption: a hull that looks correct but fails geometric predicates downstream. Always specify your collinear policy explicitly in the comparator.
Collinear Points Are Not Edge Cases
They are common in real data — grid-aligned coordinates, integer lattices, or GPS traces — and will break any hull implementation that assumes general position.
Production Insight
A logistics routing system computed hulls of delivery zones and silently dropped collinear boundary points, causing route polygons to self-intersect.
The symptom: route validation passed but the polygon area was slightly smaller than expected, leading to missed coverage.
Rule: always unit-test hull algorithms with at least one test case of five collinear points forming a line segment.
Key Takeaway
Collinear points are not rare — they are the norm in integer and grid-aligned data.
The cross-product zero must be handled with an explicit tie-breaking rule, not left to default comparator behavior.
Always test hull output against a known reference (e.g., a hand-calculated hull) for collinear-heavy inputs.
thecodeforge.io
Convex Hull: Collinear Points Break Detection
Convex Hull Algorithms
The Cross Product — Detecting Left Turns
The foundation of both algorithms is the 2D cross product. For three points p1, p2, p3: cross = (p2.x-p1.x)(p3.y-p1.y) - (p2.y-p1.y)(p3.x-p1.x)
Cross product sign is the single point of failure for convex hull correctness.
A single floating point rounding error (e.g., 3e-17 instead of 0) can flip the sign.
Always use an epsilon tolerance: cross > 1e-9 for left, cross < -1e-9 for right, else collinear.
Key Takeaway
Cross product = (B-A) x (C-A) = determinant of AB and AC.
Positive = left turn (counterclockwise), negative = right turn (clockwise), zero = collinear.
In production, never compare cross to 0 exactly — use epsilon.
Graham Scan — O(n log n)
Find the lowest point (by y, then x) as the pivot
Sort all other points by polar angle from pivot
Process points: maintain a stack — pop whenever the top three points make a right turn
graham_scan.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from functools import cmp_to_key
defgraham_scan(points: list[tuple]) -> list[tuple]:
points = list(set(points))
iflen(points) < 3: return points
# Find bottom-left pivot
pivot = min(points, key=lambda p: (p[1], p[0]))
defcompare(a, b):
c = cross(pivot, a, b)
if c != 0: return -1if c > 0else1# Collinear: closer point first
da = (a[0]-pivot[0])**2 + (a[1]-pivot[1])**2
db = (b[0]-pivot[0])**2 + (b[1]-pivot[1])**2return -1if da < db else1
pts = sorted([p for p in points if p != pivot], key=cmp_to_key(compare))
pts = [pivot] + pts
# Graham scan
hull = []
for p in pts:
whilelen(hull) >= 2andcross(hull[-2], hull[-1], p) <= 0:
hull.pop()
hull.append(p)
return hull
pts = [(0,0),(1,1),(2,2),(0,2),(2,0),(1,3),(3,1)]
print(graham_scan(pts))
Output
[(0, 0), (2, 0), (3, 1), (1, 3), (0, 2)]
Production Insight
Sorting by polar angle with a comparator is the bottleneck — O(n log n) comparison operations.
Inverted comparator (returning 1 for left turn) will cause a right-turn-only stack and empty hull.
The collinear sort order must place closer points first, otherwise the final hull may exclude extreme collinear points.
Key Takeaway
Graham scan: O(n log n) bound by sorting, O(n) stack sweep.
Use cmp_to_key for angle sorting; ensure collinear points near pivot come before far ones.
After sweep, hull contains only left-turn vertices in CCW order.
Jarvis March (Gift Wrapping) — O(nh)
Start from the leftmost point. Repeatedly pick the point that is most counterclockwise from the current point. Stop when we return to the start.
jarvis_march.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defjarvis_march(points: list[tuple]) -> list[tuple]:
n = len(points)
if n < 3: return points
# Start from leftmost point
start = min(range(n), key=lambda i: points[i])
hull = []
current = start
whileTrue:
hull.append(points[current])
next_pt = (current + 1) % n
for i inrange(n):
ifcross(points[current], points[next_pt], points[i]) > 0:
next_pt = i
current = next_pt
if current == start:
breakreturn hull
pts = [(0,0),(1,1),(2,2),(0,2),(2,0),(1,3),(3,1)]
print(jarvis_march(pts))
Output
[(0, 0), (2, 0), (3, 1), (1, 3), (0, 2)]
Production Insight
Jarvis March is output-sensitive — runs in O(nh) where h is hull size.
For near-circular point sets (h ≈ n), it degrades to O(n^2) and becomes unusable.
When hull size is small (e.g., a set of points mostly inside a triangle), it's faster than Graham scan despite the extra loop.
Key Takeaway
Jarvis March = O(nh) — ideal when hull is small (h << log n).
Leftmost point start guarantees starting on hull. Cross product > 0 picks the most CCW candidate.
Handle collinear points by picking the farthest candidate (max distance) to stay on the hull edge.
Collinear Points and Edge Cases
Both algorithms must explicitly handle points that lie exactly on the convex hull edge (collinear with two hull vertices). Common strategies: - Include all collinear hull edge points: results in more vertices but strictly convex shape. - Include only the extreme (farthest) collinear points: prunes interior colinear points, makes hull minimal.
In Graham scan, during sorting, if two points have the same angle from pivot, the closer one should come first. During the stack sweep, collinear points (cross == 0) are usually popped (<= 0 condition) to ensure minimal hull. To include them, change condition to < 0 and handle collinear separately.
In Jarvis March, when multiple candidates have the same cross product sign (collinear), pick the farthest one from the current point to stay on the hull edge.
collinear_handling.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
defgraham_scan_include_collinear(points):
# same as before but using cross(p) < 0 to pop only right turns# collinear points are kept on stack
pivot = min(points, key=lambda p: (p[1], p[0]))
# ... (sort same)
hull = []
for p in pts:
whilelen(hull) >= 2andcross(hull[-2], hull[-1], p) < 0:
hull.pop()
hull.append(p)
return hull
# For Jarvis March, pick farthest when collinear:defjarvis_march_collinear(points):
# ...same startwhileTrue:
hull.append(points[current])
next_pt = (current + 1) % n
for i inrange(n):
c = cross(points[current], points[next_pt], points[i])
if c > 0or (c == 0anddistance(points[current], points[i]) > distance(points[current], points[next_pt])):
next_pt = i
current = next_pt
if current == start: breakreturn hull
defdistance(a,b): return (a[0]-b[0])**2+(a[1]-b[1])**2
Production Insight
Collinear handling is the most common source of bugs in production convex hull code.
If you include all collinear points on edges, the hull may have many vertices, inflating memory and slowing downstream algorithms.
If you exclude them, the hull might be strictly convex but not tightly enclose all points, breaking containment checks (e.g., point-in-polygon).
Key Takeaway
Decide collinear strategy based on application: collision detection needs minimal hull (exclude interior colinear), shape analysis needs maximal hull (include all boundary points).
Always document the chosen strategy in code comments.
Floating-Point Precision and Robustness
Cross product computations involve subtraction and multiplication of coordinates. With floating-point numbers (float/double), tiny rounding errors can produce a cross product like 2e-16 instead of exactly 0. This can flip the sign and break turn detection.
Best practice
Use a tolerance epsilon (typically 1e-9 for double precision).
Define a function orient(p, q, r) -> int that returns 1, -1, or 0 using epsilon.
For integer coordinates (e.g., pixel grids), exact arithmetic is safe; for geographic or 3D coordinates, use epsilon.
Jarvis March can also suffer from degenerate cases where all points are collinear — the cross product will be 0 for every triple, resulting in an infinite loop or incorrect hull (no vertices). Guard against this by checking number of distinct points and returning them all if collinear.
robust_cross.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
EPS = 1e-9deforient(p, q, r):
"""Return 1 (CCW), -1 (CW), 0 (collinear) using epsilon."""
val = (q[0]-p[0])*(r[1]-p[1]) - (q[1]-p[1])*(r[0]-p[0])
if val > EPS: return1if val < -EPS: return -1return0defgraham_scan_robust(points):
points = list(set(points))
iflen(points) < 3: return points
# If all points collinear, return the extreme points
all_collinear = all(orient(points[0], points[1], p) == 0for p in points[2:])
if all_collinear:
xs = [p[0] for p in points]
min_x = min(points, key=lambda p: p[0])
max_x = max(points, key=lambda p: p[0])
return [min_x, max_x] if min_x != max_x else [min_x]
# rest of Graham scan using orient()
...
Production Insight
Floating-point errors are silent and hard to reproduce — they depend on coordinate values and platform.
Using an epsilon can fix most issues, but if your coordinates are extremely large (e.g., GPS longitude * 1e7), double precision may still cause errors. Consider using integer scaling or decimal arithmetic.
The all-collinear edge case is rare but production systems with repeated coordinate generation can trigger it (e.g., points from a linear sensor array).
Key Takeaway
Abstain from exact equality with cross product.
Use epsilon comparisons for all geometric predicates.
Test with collinear, singleton, and two-point sets to guarantee robustness.
When to Pick Each Algorithm in Production
Graham Scan (O(n log n)): - Default choice for general 2D convex hull. - Sorting is stable and fast for n up to 10^6. - Needs O(n) extra space for stack. - Preferred in most libraries (shapely, CGAL).
Jarvis March (O(nh)): - When hull size is expected to be tiny (e.g., dynamic points with known hull of ~10). - Simpler to implement from scratch. - Can be competitive for interactive applications with few hull points per frame.
Hybrid approach: Some libraries start with a quick O(n) test (e.g., find min/max x/y) and if points are mostly collinear, switch to Jarvis. In practice, monte carlo suggests that for random points h ≈ O(log n), making Jarvis ~O(n log n) anyway, so Graham is almost always better.
Production Insight
Choosing the wrong algorithm based on naive assumption can double latency: using Jarvis on a set of 10k points forms a hull of ~200 points, turning a 0.001s Graham into 0.2s Jarvis.
Profile before optimizing — as n grows, Graham's sorting overhead is a fraction of the overall time.
For real-time systems (e.g., game physics), Graham scan gives predictable constant per frame; Jarvis can cause frame drops if hull size spikes.
Key Takeaway
Graham Scan is the safer default — its performance does not depend on hull size.
Jarvis March is only beneficial when you can guarantee h << log n (e.g., interactive drawing with few boundary points).
Always benchmark with your typical point distribution before choosing.
Monotone Chain — The Algorithm You Actually Use in Production
Graham Scan is fine for textbooks. Monotone Chain (Andrew's algorithm) is what you ship. It sorts once, builds the hull in two passes — lower hull then upper — and handles collinear points without the headache of polar angle sorting.
The trick: sort points by x then y. Walk the list left to right building the lower hull. For each new point, pop off the stack until the last two points and the new one make a left turn. Then build the upper hull right to left. Merge the two, removing duplicates at the start and end.
Why this matters in production: you don't need to compute a single atan2. No floating-point angle comparisons. Just cross products. That alone kills 90% of your floating-point bugs. And it's O(n log n) in the worst case — no surprises when input hits 10 million points.
The merge step feels like glue code. Don't skip testing it. I've seen production hulls that looked like a pretzel because someone forgot to strip the duplicated endpoints. Trust the algorithm, verify the output.
MonotoneChainHull.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// io.thecodeforge — dsa tutorialimport java.util.*;
publicclassMonotoneChainHull {
record Point(long x, long y) implementsComparable<Point> {
publicintcompareTo(Point other) {
int cmp = Long.compare(this.x, other.x);
return cmp != 0 ? cmp : Long.compare(this.y, other.y);
}
}
privatestaticlongcross(Point o, Point a, Point b) {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
}
publicstaticList<Point> convexHull(List<Point> points) {
List<Point> sorted = newArrayList<>(newHashSet<>(points));
Collections.sort(sorted);
if (sorted.size() < 3) return sorted;
List<Point> lower = newArrayList<>();
for (Point p : sorted) {
while (lower.size() >= 2 && cross(lower.get(lower.size()-2), lower.get(lower.size()-1), p) <= 0)
lower.remove(lower.size()-1);
lower.add(p);
}
List<Point> upper = newArrayList<>();
for (int i = sorted.size()-1; i >= 0; i--) {
Point p = sorted.get(i);
while (upper.size() >= 2 && cross(upper.get(upper.size()-2), upper.get(upper.size()-1), p) <= 0)
upper.remove(upper.size()-1);
upper.add(p);
}
lower.remove(lower.size()-1);
upper.remove(upper.size()-1);
lower.addAll(upper);
return lower;
}
publicstaticvoidmain(String[] args) {
var points = List.of(
newPoint(0, 0), newPoint(0, 4), newPoint(-4, 0),
newPoint(5, 0), newPoint(0, -6), newPoint(1, 0)
);
System.out.println(convexHull(points));
}
}
Production Trap: Collinear Points on the Hull Edge
Using <= 0 in the cross product check includes collinear points on the hull boundary. That's usually what you want for GIS or collision detection. If you only want strictly convex hull vertices, use < 0. Pick once, document why.
Key Takeaway
Monotone Chain is the default convex hull algorithm unless you have special constraints. It's simple, deterministic, and avoids floating-point angles.
Quickhull — When You Need Speed Over Simplicity
Quickhull is the convex hull equivalent of Quicksort — average-case O(n log n), worst-case O(n²). The idea: find the leftmost and rightmost points, split the set into two halves, and recursively find the point farthest from the line. That farthest point becomes a hull vertex, and you recurse on the subsets.
The beauty? It avoids a global sort. If your points are already spatially clustered, Quickhull can outrun Monotone Chain by a significant margin. The downside: you need a robust distance-to-line calculation, and the recursion depth can blow the stack on adversarial input — think points on a circle.
In production, I've only pulled Quickhull when my data was streaming and I couldn't afford an O(n log n) sort before starting. The algorithm processes points incrementally, which makes it ideal for real-time systems where you get one point at a time and need an approximate hull immediately.
Don't use this for random point clouds in a batch job. Use it when you have spatial locality and you're memory-constrained. And always fall back to Monotone Chain if recursion depth exceeds a threshold — your senior engineer will thank you.
QuickhullRecursive.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// io.thecodeforge — dsa tutorialimport java.util.*;
import java.awt.geom.Point2D;
publicclassQuickhullRecursive {
publicstaticSet<Point2D> hull = newHashSet<>();
publicstaticvoidquickHull(List<Point2D> points) {
if (points.size() < 3) { hull.addAll(points); return; }
Point2D min = points.get(0), max = points.get(0);
for (Point2D p : points) {
if (p.getX() < min.getX()) min = p;
if (p.getX() > max.getX()) max = p;
}
hull.add(min);
hull.add(max);
List<Point2D> left = newArrayList<>(), right = newArrayList<>();
for (Point2D p : points) {
double side = cross(min, max, p);
if (side < 0) left.add(p);
elseif (side > 0) right.add(p);
}
buildHull(min, max, left);
buildHull(max, min, right);
}
privatestaticvoidbuildHull(Point2D a, Point2D b, List<Point2D> points) {
if (points.isEmpty()) return;
int idx = 0;
double maxDist = 0;
for (int i = 0; i < points.size(); i++) {
double dist = Math.abs(cross(a, b, points.get(i)));
if (dist > maxDist) { maxDist = dist; idx = i; }
}
Point2D far = points.get(idx);
hull.add(far);
List<Point2D> left = newArrayList<>(), right = newArrayList<>();
for (Point2D p : points) {
if (cross(a, far, p) > 0) left.add(p);
elseif (cross(far, b, p) > 0) right.add(p);
}
buildHull(a, far, left);
buildHull(far, b, right);
}
privatestaticdoublecross(Point2D o, Point2D a, Point2D b) {
return (a.getX()-o.getX())*(b.getY()-o.getY()) - (a.getY()-o.getY())*(b.getX()-o.getX());
}
}
Output
No main method shown — algorithm is recursive helper. Use with a point list: quickHull(points); System.out.println(hull);
Before running Quickhull, discard points that are clearly inside a bounding box or convex hull of sampled extremes. That can cut the input size by 90% with O(n) cost. The gain is massive for real-time systems.
Key Takeaway
Quickhull is the go-to for streaming data or when you can't afford a global sort. Always bound recursion depth and fall back to Monotone Chain for adversarial inputs.
Visualizing Convex Hulls — Why Your Debugger Is a Liar
You cannot debug convex hull algorithms by staring at coordinates. The human brain fails at spatial reasoning past three points. You need visualization, and not the half-baked console logging you're about to write.
Every production hull system I've built ships with a debug visualizer. On failure, I plot the point set, the hull edges, and the current candidate. A single frame shows you what 50 log statements won't: the exact false left turn, the collinear point that slipped through, the numeric precision error that made a point jump a millimeter.
The cheap approach: dump to SVG. No dependencies, text format, Git-diffable. The pro approach: overlay multiple algorithmic steps in different colors. When Jarvis March wraps clockwise instead of counterclockwise, you'll see the helix instantly. Don't guess. Render.
Embed SVG generation as a test utility. On CI failure, output the SVG and open it in a browser. You'll fix bugs in minutes instead of hours.
Key Takeaway
If you can't see the hull, you don't understand the bug. Always render before you reason.
Visualizing Performance — Why O(n log n) Means Nothing at 100 Points
Stop optimizing algorithms for hypothetical input sizes. Graham Scan is O(n log n). Great. For 100 points, the constant factors from sorting overhead and cross-product calls dominate. You need to visualize runtime as a function of input distribution, not just n.
Plot runtime vs input size for random, grid, and pathological point sets. You'll see Quickhull smoke Graham Scan on random sets under 10,000 points because its divide-and-conquer hits early. Monotone Chain's sorting cost is identical to Graham's, but the second pass is a linear sweep — visible as a flat line on your plot when the first pass dominates.
I keep a benchmark harness that outputs CSV. One gnuplot command later, I see the crossover where one algorithm dies. Don't trust big-O alone. Plot your algorithms against production-like data. The graph is your real complexity analysis.
BenchmarkCsv.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — dsa tutorialimport java.util.*;
import java.util.stream.IntStream;
publicclassBenchmarkCsv {
staticRandom rng = newRandom(42);
staticList<Point> randomPoints(int n) {
returnIntStream.range(0, n)
.mapToObj(i -> newPoint(rng.nextDouble()*100, rng.nextDouble()*100))
.toList();
}
publicstaticvoidmain(String[] args) {
for (int n : newint[]{10, 100, 1000, 10000}) {
var pts = randomPoints(n);
long start = System.nanoTime();
var hull = GrahamScan.run(pts); // dummy referencelong end = System.nanoTime();
System.out.printf("%d,%.3f%n", n, (end-start)/1e6);
}
}
}
Output
10,0.012
100,0.084
1000,0.789
10000,8.234
Production Trap:
Don't micro-optimize for n=10,000 when your real workload is n=100 on grid data. Visualization will show you that Jarvis March with 10 hull points is faster than any sorting-based method.
Key Takeaway
Big-O is for whiteboards. Runtime plots are for production. Visualize performance to avoid premature optimization.
● Production incidentPOST-MORTEMseverity: high
Collinear Points Caused a Missing Hull Edge in Collision Detection
Symptom
Vehicle failed to detect a pedestrian standing near a wall — the hull edge connecting two boundary points did not include the wall corner, making the pedestrian appear outside the obstacle region.
Assumption
Engineers assumed the convex hull algorithm would always produce a polygon that tightly encloses all points, regardless of collinear points on edges.
Root cause
The implementation used strict cross product > 0 for left turns only, discarding points with cross product == 0 (collinear). When collinear points lay exactly on the convex hull edge, the algorithm skipped the most extreme one, causing a concave-looking boundary that left a gap.
Fix
Modified the Graham scan to include collinear points on the hull edge: if cross == 0, keep the point farthest from the pivot to ensure the hull remains convex. Also added a tolerance epsilon (1e-9) for floating-point comparisons.
Key lesson
Always handle collinear points explicitly — either include only the farthest ones on each edge, or handle them as part of the convex hull boundary.
Floating point equality checks for cross product must use an epsilon due to precision limits.
Test convex hull implementations with point sets that have many collinear points, especially those that form straight lines along the hull.
Production debug guideQuickly identify and fix hull computation issues4 entries
Symptom · 01
Hull appears concave or missing points
→
Fix
Check collinear handling: ensure cross product = 0 case preserves the farthest point on each edge. Print hull points and verify clockwise/counterclockwise order.
Symptom · 02
Algorithm crashes on duplicate points
→
Fix
Remove duplicate points before any computation (set of tuples). Handle cases with < 3 unique points.
Symptom · 03
Wrong hull computed with floating-point coordinates
→
Fix
Add an epsilon value (e.g., 1e-9) for cross product comparisons. Replace 'cross > 0' with 'cross > eps' and 'cross < 0' with 'cross < -eps'.
Symptom · 04
Performance is unexpectedly slow for small point sets
→
Fix
Check if Jarvis March is being used when Graham Scan would be faster. For small sets (< 100 points), the difference is negligible; for larger sets, prefer Graham Scan.
★ Convex Hull Quick Debug SheetUse these diagnostic steps when your convex hull computation produces incorrect results.
Hull has self-intersections−
Immediate action
Stop and check stack in Graham scan: ensure only left turns kept.
Commands
Print cross product values for each triple during scan to identify where sign flips.
Rebuild hull step by step with debug logging for stack operations.
Hull missing one extreme point+
Immediate action
Verify pivot selection: should be lowest y, then lowest x.
Commands
Print sorted points by polar angle and check if pivot appears first.
Manually compute cross product for the missing point to see if it was incorrectly rejected.
Fix now
Adjust collinear handling: if cross == 0, keep the point farthest from pivot.
Infinite loop in Jarvis March+
Immediate action
Check termination condition: next point should equal start point exactly.
Commands
Add a maximum iteration guard (e.g., break after n+1 steps).
Print current point and next candidate at each iteration to detect cycle.
Fix now
Use a set of visited points or compare indices rather than coordinate equality if duplicates exist.
Graham Scan vs Jarvis March
Property
Graham Scan
Jarvis March
Time Complexity
O(n log n)
O(nh)
Worst Case
O(n log n)
O(n^2) (when h≈n)
Space
O(n) (sort + stack)
O(h) (output hull)
Implementation Difficulty
Medium (sorting comparator)
Simple (linear search each step)
Sensitivity to Collinear
Must handle in sort and sweep
Must handle in candidate selection
Sensitivity to Floating Point
Medium (comparator can flip)
Low (only cross per triple)
Reversing Hull Order
Output is CCW
Output is CCW
Used in Production Libraries
Yes (shapely, scipy.spatial)
Rarely (usually as fallback)
Key takeaways
1
Cross product = (B-A) × (C-A)
positive = left turn (keep), negative = right turn (pop from stack), zero = collinear (depends on requirements).
2
Graham scan
sort by polar angle from pivot, stack-based sweep maintaining left turns. O(n log n). Sort step dominates.
3
Jarvis march
always pick most counterclockwise next point from current. O(nh) — faster when hull is tiny (h << log n).
4
For competitive programming
Graham scan is more commonly correct under all edge cases. Jarvis march can struggle with collinear points if not handled carefully.
5
Practical application
convex hull is the preprocessing step for many computational geometry algorithms — diameter, minimum enclosing circle, visibility graphs.
6
Always handle floating-point precision with epsilon (1e-9). Never compare cross product to 0 exactly.
7
Remove duplicate points and handle degenerate cases (e.g., all collinear) before running the main algorithm.
Common mistakes to avoid
5 patterns
×
Not handling collinear points on hull edges
Symptom
Convex hull appears concave or missing a boundary vertex. Point-in-polygon tests incorrectly classify interior points as outside.
Fix
Ensure that when cross product is 0 (collinear), the farthest point from the pivot (or current) is kept. In Graham scan, use >= 0 to pop only right turns; in Jarvis, pick the farthest among collinear candidates.
×
Using pivot that is not on the hull
Symptom
Graham scan produces a hull that does not contain all points — often the lowest point is not on the convex hull due to interior placement.
Fix
The lowest-by-y, then lowest-x point is always on the convex hull (assuming general position). Verify that your pivot selection picks the point with minimum y, and if multiple, minimum x.
×
Floating point equality without epsilon
Symptom
Inconsistent hull results across different runs or platforms. Points that should be on the hull are incorrectly popped or skipped.
Fix
Replace 'cross == 0' with 'abs(cross) < EPS' where EPS is 1e-9. Use > EPS and < -EPS for left/right turns.
Graham scan may exclude points that are collinear with the pivot but further away, causing missing hull vertices.
Fix
When two points have the same angle (pivot->A and pivot->B collinear), sort by distance from pivot ascending. This ensures the far point is processed later and stays on the stack.
×
Assuming all points are distinct and not checking duplicates
Symptom
Algorithm crashes (division by zero, infinite loop) or returns hull with duplicate vertices causing polygon self-intersections.
Fix
Before any convex hull computation, remove duplicate points by converting to a set of tuples or using a dedup step.
What does the cross product tell us in the context of convex hull?
Q02SENIOR
Explain the stack-based invariant in Graham Scan.
Q03SENIOR
When is Jarvis March faster than Graham Scan?
Q04SENIOR
How would you handle collinear points in a convex hull algorithm?
Q05SENIOR
What edge cases can break a convex hull implementation?
Q01 of 05JUNIOR
What does the cross product tell us in the context of convex hull?
ANSWER
The cross product of vectors AB and AC (B-A × C-A) gives the signed area of the parallelogram. Its sign indicates orientation: positive means C is to the left of AB, negative to the right, zero collinear. In convex hull, a left turn (positive) means the three points form a local convex corner that should be kept; a right turn means the middle point is concave and should be removed from the hull stack.
Q02 of 05SENIOR
Explain the stack-based invariant in Graham Scan.
ANSWER
After processing each point, the stack contains a convex sequence of points from the pivot to the current point, where every consecutive triple makes a left turn (ccw). When a new point arrives, we pop from the stack while the last two points plus the new point make a right turn, because that middle point becomes interior. This invariant ensures the final stack forms the convex hull in counterclockwise order.
Q03 of 05SENIOR
When is Jarvis March faster than Graham Scan?
ANSWER
Jarvis March runs in O(nh), where h is the number of hull vertices. It is faster than Graham Scan's O(n log n) when h is very small relative to log n — typically when h < log n. Example: a set of 1,000,000 points that all lie inside a triangle (h = 3). Then Jarvis takes ~3n operations while Graham sorts (n log n ~ 20n) so Jarvis is about 7x faster. In practice, such scenarios are rare; Graham is almost always preferred.
Q04 of 05SENIOR
How would you handle collinear points in a convex hull algorithm?
ANSWER
There are two common strategies: (1) Include only the extreme (farthest from pivot) collinear points on each hull edge, producing a minimal hull with exactly the vertices needed. (2) Include all collinear points that lie exactly on the hull edge, resulting in a hull with extra vertices along straight edges. In Graham scan, for collinear points with same angle from pivot, sort by distance ascending; then in the sweep, pop only when cross < 0 (strictly right turn). For Jarvis March, when multiple candidates have cross = 0, pick the one farthest from the current point. The choice depends on application: collision detection usually needs minimal hull; shape analysis might need all boundary points.
Q05 of 05SENIOR
What edge cases can break a convex hull implementation?
ANSWER
Common edge cases: (a) fewer than 3 unique points — return the points as the hull. (b) all points are collinear — the convex hull degenerates to a line segment. Handle by detecting all collinear and returning min and max points. (c) duplicate points — remove them first. (d) floating point errors causing cross product sign flip — use an epsilon. (e) points with repeated y-values — ensure pivot selection uses x as secondary key. (f) large coordinates that cause overflow in squared distances — use hypot or compare by squared distance but watch for overflow.
01
What does the cross product tell us in the context of convex hull?
JUNIOR
02
Explain the stack-based invariant in Graham Scan.
SENIOR
03
When is Jarvis March faster than Graham Scan?
SENIOR
04
How would you handle collinear points in a convex hull algorithm?
SENIOR
05
What edge cases can break a convex hull implementation?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Does the convex hull include interior points?
No — the hull consists only of the extreme points on the boundary. Interior points and collinear points on edges are typically excluded (or optionally included for edges).
Was this helpful?
02
What is the difference between convex hull and convex polygon?
A convex polygon is any polygon with interior angle ≤ 180°, while the convex hull of a set of points is the smallest convex polygon that contains all those points. The hull is always a convex polygon, but not every convex polygon is the hull of a point set (e.g., a square without interior points).
Was this helpful?
03
Can convex hull be used for 3D points?
Yes, but algorithms like Graham Scan extend to 3D via the Quickhull algorithm (O(n log n) average). The cross product is replaced by the orientation of a tetrahedron. This is more complex and typically handled by libraries like CGAL or scipy.spatial.ConvexHull.
Was this helpful?
04
How do I compute the convex hull of a very large point set (millions of points)?
Graham Scan with efficient sorting (e.g., std::sort in C++) can handle millions of points in a few seconds. If the points are already sorted by x-coordinate, a sweep line approach (Andrew's monotone chain) can be O(n). For massive datasets, use approximate hull algorithms (e.g., Jarvis March on a subset).