Skip to content
Home DSA Convex Hull — Collinear Points That Break Hull Detection

Convex Hull — Collinear Points That Break Hull Detection

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Recursion → Topic 9 of 9
Skipping cross product == 0 collinear points creates hull gaps that miss obstacles.
🔥 Advanced — solid DSA foundation required
In this tutorial, you'll learn
Skipping cross product == 0 collinear points creates hull gaps that miss obstacles.
  • 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).
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • 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.
🚨 START HERE

Convex Hull Quick Debug Sheet

Use these diagnostic steps when your convex hull computation produces incorrect results.
🟡

Hull has self-intersections

Immediate ActionStop 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.
Visualize points and computed hull (e.g., matplotlib scatter + polygon plot).
Fix NowRebuild hull step by step with debug logging for stack operations.
🟡

Hull missing one extreme point

Immediate ActionVerify 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 NowAdjust collinear handling: if cross == 0, keep the point farthest from pivot.
🟡

Infinite loop in Jarvis March

Immediate ActionCheck 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 NowUse a set of visited points or compare indices rather than coordinate equality if duplicates exist.
Production Incident

Collinear Points Caused a Missing Hull Edge in Collision Detection

A self-driving car's obstacle avoidance system used a convex hull simplification and missed a critical object because collinear points on the hull boundary were excluded in the wrong order.
SymptomVehicle 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.
AssumptionEngineers assumed the convex hull algorithm would always produce a polygon that tightly encloses all points, regardless of collinear points on edges.
Root causeThe 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.
FixModified 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 Guide

Quickly identify and fix hull computation issues

Hull appears concave or missing pointsCheck collinear handling: ensure cross product = 0 case preserves the farthest point on each edge. Print hull points and verify clockwise/counterclockwise order.
Algorithm crashes on duplicate pointsRemove duplicate points before any computation (set of tuples). Handle cases with < 3 unique points.
Wrong hull computed with floating-point coordinatesAdd an epsilon value (e.g., 1e-9) for cross product comparisons. Replace 'cross > 0' with 'cross > eps' and 'cross < 0' with 'cross < -eps'.
Performance is unexpectedly slow for small point setsCheck 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 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
cross_product.py · PYTHON
123456789
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
▶ Output
1
-1
0
📊 Production Insight
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)

  1. Find the lowest point (by y, then x) as the pivot
  2. Sort all other points by polar angle from pivot
  3. Process points: maintain a stack — pop whenever the top three points make a right turn
graham_scan.py · PYTHON
1234567891011121314151617181920212223242526
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))
▶ 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.py · PYTHON
1234567891011121314151617181920
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))
▶ 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.py · PYTHON
123456789101112131415161718192021222324252627
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
📊 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.py · PYTHON
123456789101112131415161718192021
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()
    ...
📊 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.
🗂 Graham Scan vs Jarvis March
PropertyGraham ScanJarvis March
Time ComplexityO(n log n)O(nh)
Worst CaseO(n log n)O(n^2) (when h≈n)
SpaceO(n) (sort + stack)O(h) (output hull)
Implementation DifficultyMedium (sorting comparator)Simple (linear search each step)
Sensitivity to CollinearMust handle in sort and sweepMust handle in candidate selection
Sensitivity to Floating PointMedium (comparator can flip)Low (only cross per triple)
Reversing Hull OrderOutput is CCWOutput is CCW
Used in Production LibrariesYes (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

    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.

    Sorting polar angle incorrectly — placing farther points before closer points
    Symptom

    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.

Interview Questions on This Topic

  • QWhat does the cross product tell us in the context of convex hull?JuniorReveal
    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.
  • QExplain the stack-based invariant in Graham Scan.Mid-levelReveal
    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.
  • QWhen is Jarvis March faster than Graham Scan?SeniorReveal
    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.
  • QHow would you handle collinear points in a convex hull algorithm?Mid-levelReveal
    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.
  • QWhat edge cases can break a convex hull implementation?SeniorReveal
    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.

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).

🔥
Naren Founder & Author

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.

← PreviousStrassen's Matrix Multiplication Algorithm
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged