Cairn
algorithms · computational geometry · O(n log n)

back to Convex Hull

Monotone Chain (Andrew's Algorithm)

The site's fifth Convex Hull entry, and the second one that reaches a guaranteed O(n log n), alongside Graham Scan — but by a genuinely different sort. Graham Scan sorts every point by the angle it makes from a chosen pivot, which needs the pivot picked first and an atan2 call per comparison. Monotone Chain, published by A. M. Andrew in 1979, skips the angle entirely: sort all the points by plain (x, y) coordinate, exactly once, and then sweep that flat order twice — once left to right building what's called the lower hull, once right to left building the upper hull — using the same cross-product turn test and popping stack Graham Scan uses for its single sweep. Same asymptotic class, same primitive, no pivot to choose and no trigonometry anywhere in it.

Try it

The same eleven points as every other Convex Hull entry on this site, including the same collinear trio — A, B, and C — along the hull's bottom edge. Press Step or Run to watch both passes: the points are sorted by x (ties broken by y) once up front, then swept left to right to build the lower chain, then swept right to left to build the upper chain — each pass uses the identical push/pop rule, just in opposite directions over the same sorted list. The checkbox switches whether a dead-straight (collinear) triple counts as "wrong way" during either pass — see Pitfalls for what that changes.

hull size: 0
Press Step or Run.

Why it works

Same cross product as every other entry in this category: cross(o, a, b) = (a.x−o.x)(b.y−o.y) − (a.y−o.y)(b.x−o.x), positive one way, negative the other, exactly zero on a straight line. What's different here is what gets sorted before the sweep even starts. Sorting by coordinate instead of angle means every point in the sorted list has a well-defined, unambiguous position — no pivot to single out, no risk of two points landing at the identical angle from some chosen center (the exact tiebreak Graham Scan's own page has to handle with a secondary distance sort). The tradeoff is that one sorted pass isn't enough on its own: an angle sort visits points in the order they'd appear walking the whole hull boundary, but an x, y sort only ever visits them left to right, which can trace at most one side of a convex shape. So the algorithm runs the identical push/pop sweep twice — once building the chain that stays on one side as points are added left to right (the lower hull), once building the chain on the other side scanning right to left (the upper hull) — and the union of both chains, each end trimmed of its one duplicate point, is the whole polygon.

A naming surprise, checked directly against this page's own points, not assumed from the textbook terms: "lower" and "upper" describe how each chain is built (which pass keeps which turns), not where either one ends up on screen. On this page's own point set, the chain built by the standard "lower hull" rule comes out to I → H → G → F → E — and every one of those points (H at y=70, G at y=40, F at y=60) sits near the top of the canvas, not the bottom. The "upper hull" pass produces E → D → C → A → I, sitting near the bottom instead (C and A both at y=300). The names are exactly backwards from what they'd suggest on this page's own canvas — the same y-increases-downward flip Graham Scan's own page already found turns its clockwise-looking hull into a counterclockwise one by the textbook cross-product convention, just showing up here as a flipped label instead of a flipped turn direction. The algorithm doesn't care what either chain is called, only that both get built and stitched together — but it's exactly the kind of mismatch that's easy to get backwards from memory the moment a coordinate system doesn't match the textbook diagram it was named from.

Reference implementation

function convexHull(points) {
  const pts = Array.from(new Set(points.map(p => JSON.stringify(p))), s => JSON.parse(s));
  if (pts.length < 3) return pts; // fewer than 3 points: no polygon, see Pitfalls

  pts.sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y); // ties broken by y, see Pitfalls

  function cross(o, a, b) {
    return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
  }

  function buildChain(seq) {
    const stack = [];
    for (const p of seq) {
      while (stack.length >= 2 && cross(stack[stack.length - 2], stack[stack.length - 1], p) <= 0) {
        stack.pop(); // wrong way, or dead straight — the middle point isn't a hull vertex
      }
      stack.push(p);
    }
    return stack;
  }

  const lower = buildChain(pts);
  const upper = buildChain(pts.slice().reverse());
  lower.pop(); // each chain's last point duplicates the other chain's first point
  upper.pop();
  return lower.concat(upper);
}

Pitfalls

A tie on x needs a secondary sort key, or the hull can silently drop a real vertex — not just reorder — depending on what order the input happened to arrive in. This is the same shape as Graham Scan's own angle-tie pitfall, but the failure is sharper here. Checked directly: take four points forming a vertical segment plus one point off to the side — (0,0), (0,5), (0,10), all sharing x = 0, plus (10,5) — whose true hull is the triangle (0,0) → (10,5) → (0,10), with (0,5) sitting exactly on its left edge. Sorted with the y tiebreak, all three tested input orderings agree on that same 3-vertex triangle every time. Sorted without it — comparing only by x, leaving same-x points in whatever order a stable sort happened to receive them — two of the three orderings still land on a valid (if differently ordered) triangle by luck, but the third produces (0,5) → (10,5) → (0,10): a hull that keeps the interior boundary point (0,5) and drops the genuine corner (0,0) entirely. That's not a stylistic difference, it's a wrong answer — a direct containment check confirms (0,0) ends up outside the returned "hull" — and it's exactly the kind of bug that looks fine on two out of three tries. See Jarvis March's own tiebreak pitfall for the same failure shape (a correct-looking result that depends on input order, not on the geometry) in a completely different algorithm.

Collinear boundary points are a real design choice here too, and it's checked to land on the same answer as every other entry in this category. The reference implementation above pops on cross(...) <= 0, same as Graham Scan's strict mode. On this page's own point set that pops B — sitting exactly on the line from A to C — during the upper-hull pass, for the same 8-vertex hull (I → H → G → F → E → D → C → A) Graham Scan and Jarvis March both land on. Loosen the test to < 0 and B survives instead, for the identical 9-vertex result (I → H → G → F → E → D → C → B → A) both other pages also produce in loose mode. See Choosing a Convex Hull Algorithm for the same question answered across every entry in the category.

Fewer than three points, or every point exactly collinear, isn't a polygon — same edge case as Graham Scan and Jarvis March, same fix (the reference implementation's early return for the first case). The second is handled with no special-casing at all: five collinear points, (0,0), (10,0), (15,0), (20,0), (30,0), run through both passes unmodified, each correctly collapse to the same two extremes, (0,0) and (30,0) — checked directly. Same as the other two entries, that's the right answer but a 2-element result from code that otherwise returns 3-or-more, worth guarding for explicitly downstream.

Complexity

Time: O(n log n), dominated entirely by the one up-front coordinate sort — same asymptotic shape as Graham Scan, and the practical case for reaching for this page over that one: a numeric (x, y) comparison has a cheaper constant factor than a per-pair atan2 call, and there's no separate pivot-selection pass first. Both hull-building sweeps are O(n) each by the identical amortized argument as Graham Scan's single sweep — every point is pushed onto its chain exactly once, so the total pops across an entire pass can never exceed the total pushes. Space: O(n) for the sorted list and the two chains. This page reaches the identical guaranteed bound as Graham Scan by a different route — Jarvis March trades that guarantee for an output-sensitive O(nh), Quickhull trades it for a typically-faster but worst-case O(n²) divide-and-conquer, and Chan's Algorithm combines pieces of both for O(n log h) when the true hull size isn't known ahead of time. Divide-and-Conquer Convex Hull reaches this page's own guaranteed bound by a third route: the identical up-front coordinate sort, but merging per-point hulls upward through tangent lines afterward instead of sweeping twice. See Choosing a Convex Hull Algorithm for a side-by-side comparison of all six.