Convex Hull Algorithms
Convex Hull Algorithms
Introduction
Given a set of points in a 2D plane, the convex hull is the smallest convex polygon that contains all the points. Every point in the set either lies on the boundary of this polygon or inside it.
A helpful way to visualise this is the rubber-band analogy: imagine stretching a rubber band around all the points on a table and then letting it snap tight. The shape it forms — the taut outline — is the convex hull. Only the "outermost" points touch the band; all others are enclosed within.
Convex hulls are a foundational primitive in computational geometry. They appear as a preprocessing step or sub-routine in problems ranging from collision detection and path planning to image processing and geographic information systems. Understanding how to compute them efficiently — and why the algorithms work — is a key skill for anyone working in algorithms or applied mathematics.
Video Explanation

Characteristics
- Smallest convex polygon: No smaller convex set encloses all the input points.
- Boundary points only: The hull is defined by a subset of the input points; interior points are discarded.
- Orientation tests: Every mainstream convex hull algorithm relies on cross-product-based orientation tests to decide whether a turn is left (counter-clockwise), right (clockwise), or straight (collinear).
- Broad applicability: Convex hulls are used across computational geometry, computer graphics, robotics, GIS, and machine learning.
Mathematical Foundation
Cross Product
The backbone of convex hull algorithms is the 2D cross product, which tells us the orientation of an ordered triple of points A, B, C.
Given three points , , and , we define:
This is the z-component of the 3D cross product of the vectors and .
The sign of the result tells us the turn direction:
| Result | Meaning |
|---|---|
| Positive | Counter-clockwise (left) turn from AB to AC |
| Negative | Clockwise (right) turn from AB to AC |
| Zero | A, B, C are collinear |
Why Orientation Testing Works
Imagine walking from point A toward point B. When you then look toward point C:
- If C is to your left, you are making a counter-clockwise turn — a "left turn."
- If C is to your right, you are making a clockwise turn — a "right turn."
- If C is directly ahead or behind, the three points are collinear.
Convex hull algorithms exploit this: a valid convex polygon only makes turns in one direction (counter-clockwise, by convention). Whenever a new point would force a clockwise turn at the current hull vertex, that vertex is no longer on the boundary and must be removed. The cross product lets us check this in O(1) time.
Convex Hull Visualization
The following flowchart summarises the high-level structure shared by most convex hull algorithms:
Graham Scan Algorithm
Idea
Graham Scan, published by Ronald Graham in 1972, computes the convex hull in O(N log N) time. The algorithm works as follows:
- Find the anchor point: Select the point with the lowest y-coordinate (break ties by x-coordinate). This point is guaranteed to be on the hull.
- Sort by polar angle: Sort all remaining points by their polar angle relative to the anchor. Points at the same angle are ordered by distance.
- Use a stack: Iterate through the sorted points and maintain a stack of candidate hull vertices.
- Remove clockwise turns: Before pushing a new point onto the stack, pop any point that would cause a clockwise (right) turn with the top two stack elements.
After processing all points, the stack contains exactly the vertices of the convex hull in counter-clockwise order.
Step-by-Step Example
Consider the following six points:
P0 = (0, 0) P1 = (1, 1) P2 = (2, 2)
P3 = (0, 2) P4 = (2, 0) P5 = (1, 3)
Step 1 — Anchor: The lowest point is P0 = (0, 0).
Step 2 — Sort by polar angle from P0: Sorted order: P4(2,0), P2(2,2), P1(1,1), P5(1,3), P3(0,2). Note that P1 is collinear between P0 and P2, so it will be eliminated.
Step 3 — Stack processing:
| Action | Stack (bottom → top) |
|---|---|
| Push P0 | [P0] |
| Push P4 | [P0, P4] |
| Push P2 | [P0, P4, P2] |
| P1 is collinear/inside — popped or skipped | [P0, P4, P2] |
| Push P5 | [P0, P4, P2, P5] |
| P3: cross(P2, P5, P3) > 0 → push | [P0, P4, P2, P5, P3] |
Final hull (counter-clockwise): P0 → P4 → P2 → P5 → P3 → P0.
P1 is an interior point and correctly excluded.
Graham Scan C++ Implementation
#include <bits/stdc++.h>
using namespace std;
struct Point {
long long x, y;
};
// Cross product of vectors (O->A) and (O->B)
long long cross(const Point& O, const Point& A, const Point& B) {
return (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x);
}
long long distSq(const Point& A, const Point& B) {
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y);
}
vector<Point> grahamScan(vector<Point> pts) {
int n = pts.size();
if (n < 3) return pts;
// Find anchor (lowest y, then leftmost)
int minIdx = 0;
for (int i = 1; i < n; i++) {
if (pts[i].y < pts[minIdx].y ||
(pts[i].y == pts[minIdx].y && pts[i].x < pts[minIdx].x)) {
minIdx = i;
}
}
swap(pts[0], pts[minIdx]);
const Point& anchorLocal = pts[0];
// Sort remaining points by polar angle from anchor
sort(pts.begin() + 1, pts.end(), [&anchorLocal](const Point& A, const Point& B) {
long long cp = cross(anchorLocal, A, B);
if (cp != 0) return cp > 0;
return distSq(anchorLocal, A) < distSq(anchorLocal, B);
});
// Remove collinear points (keep only the farthest)
int m = 1;
for (int i = 1; i < n; i++) {
while (i < n - 1 && cross(anchorLocal, pts[i], pts[i + 1]) == 0) i++;
pts[m++] = pts[i];
}
if (m < 3) return vector<Point>(pts.begin(), pts.begin() + m);
// Stack-based hull construction
vector<Point> hull;
hull.push_back(pts[0]);
hull.push_back(pts[1]);
hull.push_back(pts[2]);
for (int i = 3; i < m; i++) {
while (hull.size() > 1 &&
cross(hull[hull.size()-2], hull[hull.size()-1], pts[i]) <= 0) {
hull.pop_back();
}
hull.push_back(pts[i]);
}
return hull;
}
int main() {
vector<Point> points = {{0,0},{1,1},{2,2},{0,2},{2,0},{1,3}};
vector<Point> hull = grahamScan(points);
cout << "Convex Hull vertices:\n";
for (auto& p : hull)
cout << "(" << p.x << ", " << p.y << ")\n";
return 0;
}
Graham Scan Java Implementation
import java.util.*;
public class GrahamScan {
static class Point {
long x, y;
Point(long x, long y) { this.x = x; this.y = y; }
}
static long cross(Point O, Point A, Point B) {
return (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x);
}
static long distSq(Point A, Point B) {
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y);
}
static List<Point> grahamScan(List<Point> input) {
// Copy the input to avoid mutating the caller's list
List<Point> pts = new ArrayList<>(input);
int n = pts.size();
if (n < 3) return pts;
// Find anchor (lowest y, then leftmost x) — local variable, no global state
int minIdx = 0;
for (int i = 1; i < n; i++) {
Point cur = pts.get(i), best = pts.get(minIdx);
if (cur.y < best.y || (cur.y == best.y && cur.x < best.x))
minIdx = i;
}
Collections.swap(pts, 0, minIdx);
final Point anchorLocal = pts.get(0);
// Sort remaining points by polar angle from anchorLocal
List<Point> rest = new ArrayList<>(pts.subList(1, n));
rest.sort((a, b) -> {
long cp = cross(anchorLocal, a, b);
if (cp != 0) return cp > 0 ? -1 : 1;
long da = distSq(anchorLocal, a), db = distSq(anchorLocal, b);
return Long.compare(da, db);
});
// Remove collinear points — keep only the farthest from anchorLocal
List<Point> filtered = new ArrayList<>();
filtered.add(anchorLocal);
int i = 0;
while (i < rest.size()) {
int j = i;
while (j + 1 < rest.size() &&
cross(anchorLocal, rest.get(j), rest.get(j + 1)) == 0) {
j++;
}
filtered.add(rest.get(j)); // keep only the farthest collinear point
i = j + 1;
}
// Handle all-collinear input safely
if (filtered.size() < 3) return filtered;
// Stack-based hull construction with size guard
Stack<Point> stack = new Stack<>();
stack.push(filtered.get(0));
stack.push(filtered.get(1));
stack.push(filtered.get(2));
for (int k = 3; k < filtered.size(); k++) {
while (stack.size() > 1) {
Point top = stack.peek();
Point second = stack.get(stack.size() - 2);
if (cross(second, top, filtered.get(k)) <= 0)
stack.pop();
else break;
}
stack.push(filtered.get(k));
}
return new ArrayList<>(stack);
}
public static void main(String[] args) {
List<Point> points = new ArrayList<>(Arrays.asList(
new Point(0,0), new Point(1,1), new Point(2,2),
new Point(0,2), new Point(2,0), new Point(1,3)
));
List<Point> hull = grahamScan(points);
System.out.println("Convex Hull vertices:");
for (Point p : hull)
System.out.println("(" + p.x + ", " + p.y + ")");
}
}
Graham Scan Python Implementation
from functools import cmp_to_key
def cross(O, A, B):
"""Return the cross product of vectors OA and OB."""
return (A[0] - O[0]) * (B[1] - O[1]) - (A[1] - O[1]) * (B[0] - O[0])
def dist_sq(A, B):
return (A[0] - B[0]) ** 2 + (A[1] - B[1]) ** 2
def graham_scan(points):
# Remove duplicate points
points = list(set(points))
n = len(points)
if n < 3:
return points
# Find anchor: lowest y, then leftmost x
anchor = min(points, key=lambda p: (p[1], p[0]))
rest = [p for p in points if p != anchor]
# Sort by polar angle from anchor; closer points first when collinear
def cmp(a, b):
cp = cross(anchor, a, b)
if cp != 0:
return -1 if cp > 0 else 1
da, db = dist_sq(anchor, a), dist_sq(anchor, b)
return -1 if da < db else (1 if da > db else 0)
rest.sort(key=cmp_to_key(cmp))
# Remove collinear points — keep only the farthest along each direction
filtered = []
i = 0
while i < len(rest):
j = i
while j + 1 < len(rest) and cross(anchor, rest[j], rest[j + 1]) == 0:
j += 1
filtered.append(rest[j]) # farthest collinear point in this direction
i = j + 1
pts = [anchor] + filtered
# Handle all-collinear inputs safely
if len(pts) < 3:
return pts
# Stack-based hull construction
hull = [pts[0], pts[1], pts[2]]
for i in range(3, len(pts)):
while len(hull) > 1 and cross(hull[-2], hull[-1], pts[i]) <= 0:
hull.pop()
hull.append(pts[i])
return hull
if __name__ == "__main__":
points = [(0,0),(1,1),(2,2),(0,2),(2,0),(1,3)]
hull = graham_scan(points)
print("Convex Hull vertices:")
for p in hull:
print(p)
Graham Scan Complexity Analysis
| Phase | Time Complexity |
|---|---|
| Finding anchor | O(N) |
| Sorting by polar angle | O(N log N) |
| Hull construction (stack) | O(N) |
| Total | O(N log N) |
| Space | O(N) |
The O(N log N) sorting step dominates. The stack-based construction is O(N) because each point is pushed and popped at most once. The space complexity is O(N) for storing sorted points and the hull.
Andrew's Monotone Chain Algorithm
Idea
Andrew's Monotone Chain (1979) is closely related to Graham Scan but uses a different sorting strategy that avoids trigonometry and is numerically more robust. The algorithm:
- Lexicographic sorting: Sort all points by (x, y) — left to right, bottom to top for ties.
- Build the lower hull: Iterate left-to-right, maintaining a sequence that only makes left (counter-clockwise) turns. Remove the last point whenever a right turn would occur.
- Build the upper hull: Iterate right-to-left over the same sorted list, applying the same rule.
- Merge: Concatenate the lower and upper hulls (removing duplicate endpoints) to obtain the complete convex hull.
The result is a counter-clockwise list of hull vertices.
Step-by-Step Example
Using the same six points as before, sorted lexicographically:
Sorted: (0,0), (0,2), (1,1), (1,3), (2,0), (2,2)
Lower hull (left to right, remove right turns):
| Point added | Hull | Action |
|---|---|---|
| (0,0) | [(0,0)] | push |
| (0,2) | [(0,0),(0,2)] | push |
| (1,1) | [(0,0),(1,1)] | (0,2) caused right turn → pop, push (1,1) |
| (1,3) | [(0,0),(1,1),(1,3)] | push |
| (2,0) | [(0,0),(1,1),(1,3)] → pop → [(0,0),(1,1)] → pop → [(0,0),(2,0)] | push |
| (2,2) | [(0,0),(2,0),(2,2)] | push |
Lower hull: [(0,0), (2,0), (2,2)]
Upper hull (right to left):
Starting fresh from (2,2) → (1,3) → (0,2) → (0,0)
Upper hull: [(2,2), (1,3), (0,2), (0,0)]
Merged (removing duplicated endpoints (0,0) and (2,2)):
(0,0) → (2,0) → (2,2) → (1,3) → (0,2) → back to (0,0)
Andrew's Monotone Chain C++ Implementation
#include <bits/stdc++.h>
using namespace std;
struct Point {
long long x, y;
bool operator<(const Point& o) const {
return x < o.x || (x == o.x && y < o.y);
}
};
long long cross(const Point& O, const Point& A, const Point& B) {
return (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x);
}
vector<Point> monotonicChain(vector<Point> pts) {
int n = pts.size();
if (n < 3) return pts;
sort(pts.begin(), pts.end());
vector<Point> hull;
// Build lower hull
for (int i = 0; i < n; i++) {
while (hull.size() >= 2 &&
cross(hull[hull.size()-2], hull[hull.size()-1], pts[i]) <= 0)
hull.pop_back();
hull.push_back(pts[i]);
}
// Build upper hull
int lower_size = hull.size() + 1;
for (int i = n - 2; i >= 0; i--) {
while ((int)hull.size() >= lower_size &&
cross(hull[hull.size()-2], hull[hull.size()-1], pts[i]) <= 0)
hull.pop_back();
hull.push_back(pts[i]);
}
hull.pop_back(); // Remove the last point (same as first)
return hull;
}
int main() {
vector<Point> points = {{0,0},{1,1},{2,2},{0,2},{2,0},{1,3}};
vector<Point> hull = monotonicChain(points);
cout << "Convex Hull vertices (counter-clockwise):\n";
for (auto& p : hull)
cout << "(" << p.x << ", " << p.y << ")\n";
return 0;
}