Convex Hull — Collinear Points That Break Hull Detection
- Cross product = (B-A) × (C-A): positive = left turn (keep), negative = right turn (pop from stack), zero = collinear (depends on requirements).
- Graham scan: sort by polar angle from pivot, stack-based sweep maintaining left turns. O(n log n). Sort step dominates.
- Jarvis march: always pick most counterclockwise next point from current. O(nh) — faster when hull is tiny (h << log n).
- 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.
Convex Hull Quick Debug Sheet
Hull has self-intersections
Print cross product values for each triple during scan to identify where sign flips.Visualize points and computed hull (e.g., matplotlib scatter + polygon plot).Hull missing one extreme point
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.Infinite loop in Jarvis March
Add a maximum iteration guard (e.g., break after n+1 steps).Print current point and next candidate at each iteration to detect cycle.Production Incident
Production Debug GuideQuickly identify and fix hull computation issues
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.
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 > 0: left turn (counterclockwise) ✓ keep
- cross < 0: right turn (clockwise) ✗ remove
- cross = 0: collinear
def cross(O, A, B): """Cross product of vectors OA and OB. > 0: CCW (left turn), < 0: CW (right turn), = 0: collinear """ return (A[0]-O[0])*(B[1]-O[1]) - (A[1]-O[1])*(B[0]-O[0]) print(cross((0,0),(1,0),(0,1))) # 1 → left turn print(cross((0,0),(0,1),(1,0))) # -1 → right turn print(cross((0,0),(1,1),(2,2))) # 0 → collinear
-1
0
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
from functools import cmp_to_key def graham_scan(points: list[tuple]) -> list[tuple]: points = list(set(points)) if len(points) < 3: return points # Find bottom-left pivot pivot = min(points, key=lambda p: (p[1], p[0])) def compare(a, b): c = cross(pivot, a, b) if c != 0: return -1 if c > 0 else 1 # 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])**2 return -1 if da < db else 1 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: while len(hull) >= 2 and cross(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))
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.
def jarvis_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 while True: hull.append(points[current]) next_pt = (current + 1) % n for i in range(n): if cross(points[current], points[next_pt], points[i]) > 0: next_pt = i current = next_pt if current == start: break return hull pts = [(0,0),(1,1),(2,2),(0,2),(2,0),(1,3),(3,1)] print(jarvis_march(pts))
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.
def graham_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: while len(hull) >= 2 and cross(hull[-2], hull[-1], p) < 0: hull.pop() hull.append(p) return hull # For Jarvis March, pick farthest when collinear: def jarvis_march_collinear(points): # ...same start while True: hull.append(points[current]) next_pt = (current + 1) % n for i in range(n): c = cross(points[current], points[next_pt], points[i]) if c > 0 or (c == 0 and distance(points[current], points[i]) > distance(points[current], points[next_pt])): next_pt = i current = next_pt if current == start: break return hull def distance(a,b): return (a[0]-b[0])**2+(a[1]-b[1])**2
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.
- Use a tolerance epsilon (typically 1e-9 for double precision).
- Define a function
orient(p, q, r) -> intthat 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.
EPS = 1e-9 def orient(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: return 1 if val < -EPS: return -1 return 0 def graham_scan_robust(points): points = list(set(points)) if len(points) < 3: return points # If all points collinear, return the extreme points all_collinear = all(orient(points[0], points[1], p) == 0 for 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() ...
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.
| 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
- Cross product = (B-A) × (C-A): positive = left turn (keep), negative = right turn (pop from stack), zero = collinear (depends on requirements).
- Graham scan: sort by polar angle from pivot, stack-based sweep maintaining left turns. O(n log n). Sort step dominates.
- Jarvis march: always pick most counterclockwise next point from current. O(nh) — faster when hull is tiny (h << log n).
- For competitive programming: Graham scan is more commonly correct under all edge cases. Jarvis march can struggle with collinear points if not handled carefully.
- Practical application: convex hull is the preprocessing step for many computational geometry algorithms — diameter, minimum enclosing circle, visibility graphs.
- Always handle floating-point precision with epsilon (1e-9). Never compare cross product to 0 exactly.
- Remove duplicate points and handle degenerate cases (e.g., all collinear) before running the main algorithm.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat does the cross product tell us in the context of convex hull?JuniorReveal
- QExplain the stack-based invariant in Graham Scan.Mid-levelReveal
- QWhen is Jarvis March faster than Graham Scan?SeniorReveal
- QHow would you handle collinear points in a convex hull algorithm?Mid-levelReveal
- QWhat edge cases can break a convex hull implementation?SeniorReveal
Frequently Asked Questions
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).
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).
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.
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).
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.