What is Graham Scan?

Graham Scan is an algorithm to find the Convex Hull of a set of 2D points. It works by finding the lowest point (pivot), sorting all other points by their polar angle with respect to the pivot, and then using a stack-based traversal to prune right turns (non-convex steps).

Explanation

  • Graham Scan builds the convex hull boundary in a single counter-clockwise sweep. It ensures the hull remains convex by validating that each new point forms a left turn (counter-clockwise) relative to the preceding two points on the hull.
  • Named after Ronald Graham (1972), it was one of the earliest algorithms in computational geometry.

Real-World Analogy

  • Imagine standing at the absolute bottom-left nail (pivot) on a pegboard and looking out at all the other nails. You turn your head from right to left (sweeping polar angles) to look at each nail in order.
  • As you walk from nail to nail in this sorted order, you lay a string. If the string ever bends to the right, you’ve made a detour inside the outer perimeter; you backtrack, remove the intermediate nails, and pull the string tight again.

Advantages and Disadvantages

Detail
DeterministicGuaranteed time complexity regardless of the shape of the hull
EfficientAmortized linear time traversal phase after sorting
Sorting OverheadRequires sorting all points by polar angle, which can be numerically unstable if not handled via cross products
Not Output-SensitiveAlways takes even if the hull has only 3 points (unlike Jarvis March or Chan’s)

Core Concept — Polar Sorting & Orientation

The Pivot Point

  • The algorithm starts by selecting a pivot point .
  • is guaranteed to be on the convex hull. We find it by picking the point with the minimum y-coordinate (if there is a tie, pick the one with the minimum x-coordinate).

Polar Angle Sorting via Cross Product

  • Sorting points by polar angle with respect to normally requires computing angles using atan2(y, x). However, floating-point trigonometry is slow and prone to precision issues.
  • Instead, we use the cross product to determine the relative angle of two points and from :
cross(P0, A, B) = (A.x - P0.x) * (B.y - P0.y) - (A.y - P0.y) * (B.x - P0.x)

Result > 0  →  A is clockwise from B (A has smaller polar angle than B)
Result < 0  →  A is counter-clockwise from B (A has larger polar angle than B)
Result = 0  →  Collinear (same angle) — keep the point farther from P0

Orientation Test (CCW)

  • For any three points , , , we want to determine the turn direction:
flowchart TD
    A["cross(P1, P2, P3)"] --> B{"Result Value"}
    B -- "> 0" --> C["Counter-Clockwise (Left Turn) ✅<br/>Keep P3 on stack"]
    B -- "= 0" --> D["Collinear (Straight) ❌<br/>Pop P2, keep farther P3"]
    B -- "< 0" --> E["Clockwise (Right Turn) ❌<br/>Pop P2, check previous"]

Algorithm Steps

Pseudocode

INPUT:  set of 2D points
OUTPUT: stack of hull points in counter-clockwise order

1. Find pivot P0 (lowest Y, then lowest X).
2. Sort remaining points by polar angle with P0.
   - If two points have the same angle, remove the closer one.
3. Initialize stack with P0, points[0], points[1].
4. FOR i from 2 to n-1:
      WHILE stack.size() >= 2 and cross(second_to_top(stack), top(stack), points[i]) <= 0:
          stack.pop()
      stack.push(points[i])
5. RETURN stack

Visual Walkthrough

  • Consider points: P0(0,0), A(2,1), B(1,2), C(0,2), D(1,1) (interior)
1. Pivot: P0(0,0)
2. Sorted by polar angle with P0: A(2,1), D(1,1), B(1,2), C(0,2) (Note: D and B are collinear with P0, D is closer so it precedes/is pruned).
3. Initialize Stack: [P0, A, B]
4. Process C(0,2):
   - Top of stack = B(1,2)
   - Second to top = A(2,1)
   - Check turn A -> B -> C: Left turn (CCW).
   - Push C. Stack: [P0, A, B, C]
5. Final Hull: [P0, A, B, C] -> (0,0), (2,1), (1,2), (0,2)

Time & Space Complexity

Complexity Table

PhaseTime ComplexityDetails
Pivot SearchSingle linear scan to find minimum Y and X
Polar SortSorting points
Stack TraversalEach point is pushed once and popped at most once
Total TimeDominated by the polar angle sorting phase
Space ComplexityTo store sorted points and stack

Implementation

  • All implementations below use cross products for custom sorting to maintain numerical precision. Python · Cpp · Java Script · Java

    Languages:

import math
 
def dist_sq(p1, p2):
    return (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2
 
def orientation(p, q, r):
    """
    0 -> Collinear
    1 -> Clockwise
    2 -> Counter-Clockwise
    """
    val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])
    if val == 0:
        return 0
    return 1 if val > 0 else 2
 
def graham_scan(points):
    n = len(points)
    if n < 3:
        return points
 
    # 1. Find pivot (lowest y, then lowest x)
    pivot_idx = 0
    for i in range(1, n):
        if points[i][1] < points[pivot_idx][1]:
            pivot_idx = i
        elif points[i][1] == points[pivot_idx][1] and points[i][0] < points[pivot_idx][0]:
            pivot_idx = i
 
    # Swap pivot to the first position
    points[0], points[pivot_idx] = points[pivot_idx], points[0]
    p0 = points[0]
 
    # 2. Sort remaining points by polar angle with p0
    # If polar angles are same, sort by distance from p0
    def polar_comparator(p):
        # Returns (angle, distance)
        # Using atan2 is simpler in Python, but we can also do custom keys
        return (math.atan2(p[1] - p0[1], p[0] - p0[0]), dist_sq(p0, p))
 
    sorted_points = sorted(points[1:], key=polar_comparator)
 
    # Handle collinear points: keep only the furthest point for same angle
    unique_points = []
    for p in sorted_points:
        if len(unique_points) > 0 and orientation(p0, unique_points[-1], p) == 0:
            if dist_sq(p0, p) > dist_sq(p0, unique_points[-1]):
                unique_points[-1] = p
        else:
            unique_points.append(p)
 
    if len(unique_points) < 2:
        return [p0] + unique_points
 
    # 3. Stack traversal
    stack = [p0, unique_points[0], unique_points[1]]
    for p in unique_points[2:]:
        while len(stack) > 1 and orientation(stack[-2], stack[-1], p) != 2:
            stack.pop()
        stack.append(p)
 
    return stack
 
    # Example
points = [(0, 3), (1, 1), (2, 2), (4, 4), (0, 0), (1, 2), (3, 1), (3, 3)]
print("Convex Hull:", graham_scan(points))
# Output: [(0, 0), (3, 1), (4, 4), (0, 3)]
#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
 
struct Point {
    int x, y;
};
 
Point p0; // Global pivot point for sorting
 
int distSq(Point p1, Point p2) {
    return (p1.x - p2.x)*(p1.x - p2.x) + (p1.y - p2.y)*(p1.y - p2.y);
}
 
int orientation(Point p, Point q, Point r) {
    int val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
    if (val == 0) return 0;  // Collinear
    return (val > 0) ? 1 : 2; // Clockwise or Counter-Clockwise
}
 
bool compare(Point p1, Point p2) {
    int orient = orientation(p0, p1, p2);
    if (orient == 0)
        return distSq(p0, p1) < distSq(p0, p2);
    return (orient == 2);
}
 
std::vector<Point> grahamScan(std::vector<Point>& points) {
    int n = points.size();
    if (n < 3) return points;
 
    // Find pivot
    int ymin = points[0].y, min_idx = 0;
    for (int i = 1; i < n; i++) {
        int y = points[i].y;
        if ((y < ymin) || (ymin == y && points[i].x < points[min_idx].x)) {
            ymin = points[i].y;
            min_idx = i;
        }
    }
 
    std::swap(points[0], points[min_idx]);
    p0 = points[0];
 
    // Sort points[1...n-1]
    std::sort(points.begin() + 1, points.end(), compare);
 
    // Filter collinear points
    int m = 1; // Index of modified array
    for (int i = 1; i < n; i++) {
        while (i < n - 1 && orientation(p0, points[i], points[i+1]) == 0)
            i++;
        points[m++] = points[i];
    }
 
    if (m < 3) return std::vector<Point>({points[0], points[1]});
 
    std::vector<Point> hull;
    hull.push_back(points[0]);
    hull.push_back(points[1]);
    hull.push_back(points[2]);
 
    for (int i = 3; i < m; i++) {
        while (hull.size() > 1 && orientation(hull[hull.size()-2], hull.back(), points[i]) != 2) {
            hull.pop_back();
        }
        hull.push_back(points[i]);
    }
 
    return hull;
}
 
int main() {
    std::vector<Point> points = {{0, 3}, {1, 1}, {2, 2}, {4, 4}, {0, 0}, {1, 2}, {3, 1}, {3, 3}};
    auto hull = grahamScan(points);
    std::cout << "Convex Hull:\n";
    for (auto p : hull)
        std::cout << "(" << p.x << ", " << p.y << ")\n";
    return 0;
}
function distSq(p1, p2) {
    return (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2;
}
 
function orientation(p, q, r) {
    const val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
    if (val === 0) return 0;
    return (val > 0) ? 1 : 2;
}
 
function grahamScan(points) {
    const n = points.length;
    if (n < 3) return points;
 
    // Find pivot
    let pivotIdx = 0;
    for (let i = 1; i < n; i++) {
        if (points[i][1] < points[pivotIdx][1]) {
            pivotIdx = i;
        } else if (points[i][1] === points[pivotIdx][1] && points[i][0] < points[pivotIdx][0]) {
            pivotIdx = i;
        }
    }
 
    const p0 = points[pivotIdx];
    const remaining = points.filter((_, idx) => idx !== pivotIdx);
 
    remaining.sort((p1, p2) => {
        const orient = orientation(p0, p1, p2);
        if (orient === 0) {
            return distSq(p0, p1) - distSq(p0, p2);
        }
        return (orient === 2) ? -1 : 1;
    });
 
    // Filter collinear points
    const unique = [];
    for (const p of remaining) {
        if (unique.length > 0 && orientation(p0, unique[unique.length - 1], p) === 0) {
            if (distSq(p0, p) > distSq(p0, unique[unique.length - 1])) {
                unique[unique.length - 1] = p;
            }
        } else {
            unique.push(p);
        }
    }
 
    if (unique.length < 2) return [p0, ...unique];
 
    const stack = [p0, unique[0], unique[1]];
    for (let i = 2; i < unique.length; i++) {
        while (stack.length > 1 && orientation(stack[stack.length - 2], stack[stack.length - 1], unique[i]) !== 2) {
            stack.pop();
        }
        stack.push(unique[i]);
    }
 
    return stack;
}
 
const points = [[0, 3], [1, 1], [2, 2], [4, 4], [0, 0], [1, 2], [3, 1], [3, 3]];
console.log("Convex Hull:", grahamScan(points));
import java.util.*;
 
public class GrahamScan {
    static class Point {
        int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
    }
 
    static Point p0;
 
    static int distSq(Point p1, Point p2) {
        return (p1.x - p2.x)*(p1.x - p2.x) + (p1.y - p2.y)*(p1.y - p2.y);
    }
 
    static int orientation(Point p, Point q, Point r) {
        int val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
        if (val == 0) return 0;
        return (val > 0) ? 1 : 2;
    }
 
    static List<Point> grahamScan(List<Point> points) {
        int n = points.size();
        if (n < 3) return points;
 
        int minIdx = 0;
        for (int i = 1; i < n; i++) {
            if (points.get(i).y < points.get(minIdx).y) {
                minIdx = i;
            } else if (points.get(i).y == points.get(minIdx).y && points.get(i).x < points.get(minIdx).x) {
                minIdx = i;
            }
        }
 
        p0 = points.get(minIdx);
        points.set(minIdx, points.get(0));
        points.set(0, p0);
 
        points.subList(1, n).sort((p1, p2) -> {
            int orient = orientation(p0, p1, p2);
            if (orient == 0) {
                return Integer.compare(distSq(p0, p1), distSq(p0, p2));
            }
            return (orient == 2) ? -1 : 1;
        });
 
        List<Point> unique = new ArrayList<>();
        for (int i = 1; i < n; i++) {
            Point p = points.get(i);
            if (!unique.isEmpty() && orientation(p0, unique.get(unique.size() - 1), p) == 0) {
                if (distSq(p0, p) > distSq(p0, unique.get(unique.size() - 1))) {
                    unique.set(unique.size() - 1, p);
                }
            } else {
                unique.add(p);
            }
        }
 
        if (unique.size() < 2) {
            List<Point> res = new ArrayList<>();
            res.add(p0);
            res.addAll(unique);
            return res;
        }
 
        Stack<Point> stack = new Stack<>();
        stack.push(p0);
        stack.push(unique.get(0));
        stack.push(unique.get(1));
 
        for (int i = 2; i < unique.size(); i++) {
            Point next = unique.get(i);
            while (stack.size() > 1) {
                Point top = stack.pop();
                Point prev = stack.peek();
                if (orientation(prev, top, next) == 2) {
                    stack.push(top);
                    break;
                }
            }
            stack.push(next);
        }
 
        return new ArrayList<>(stack);
    }
 
    public static void main(String[] args) {
        List<Point> points = new ArrayList<>(Arrays.asList(
            new Point(0, 3), new Point(1, 1), new Point(2, 2), new Point(4, 4),
            new Point(0, 0), new Point(1, 2), new Point(3, 1), new Point(3, 3)
        ));
        List<Point> hull = grahamScan(points);
        System.out.println("Convex Hull:");
        for (Point p : hull)
            System.out.println("(" + p.x + ", " + p.y + ")");
    }
}

When to Use Graham Scan

✅ Use Graham Scan When:

  • You want guaranteed worst-case execution time.
  • The coordinates are integer values, allowing exact cross products without floating-point errors.
  • The hull is large, making Jarvis March’s worst case unacceptable.

❌ Avoid Graham Scan When:

  • The points arrive dynamically in a stream — use dynamic convex hull structures instead.
  • The hull is tiny (), where Jarvis March or Chan’s Algorithm would run faster.

Key Takeaways

  • Polar Angle Ordering — Sorting points around a pivot allows us to scan them in a single counter-clockwise direction.
  • Stack-based Traversal — Eliminating right turns keeps the polygon boundary strictly convex.
  • Determinism — The sort is the bottleneck, yielding a robust overall complexity.
  • Collinear Filtering — Handled by picking only the furthest point when points share a polar angle, preventing collinear edge overlap.

More Learn

GitHub & Webs