Convex Hull — Graham Scan
Tutorial Video
Text description (accessibility)
This video demonstrates the "Convex Hull — Graham Scan" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. The convex hull of a point set is the smallest convex polygon containing all points — the shape you'd get by stretching a rubber band around the points. Key difference from OCaml: | Aspect | Rust | OCaml |
Tutorial
The Problem
The convex hull of a point set is the smallest convex polygon containing all points — the shape you'd get by stretching a rubber band around the points. It's the foundation of computational geometry: collision detection in game engines, GIS boundary computation, robot motion planning (collision-free paths), image processing (object outline detection), and clustering algorithms all use convex hull. Graham scan computes the convex hull in O(n log n) dominated by sorting — the actual hull construction is O(n) after the sort. Understanding convex hull also introduces the cross product test for point orientation, a building block for many geometric algorithms.
🎯 Learning Outcomes
Code Example
#![allow(clippy::all)]
//! # Convex Hull (Graham Scan)
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
fn cross(o: Point, a: Point, b: Point) -> f64 {
(a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
}
pub fn convex_hull(mut points: Vec<Point>) -> Vec<Point> {
if points.len() < 3 {
return points;
}
points.sort_by(|a, b| {
a.x.partial_cmp(&b.x)
.unwrap()
.then(a.y.partial_cmp(&b.y).unwrap())
});
let mut lower = vec![];
for &p in &points {
while lower.len() >= 2 && cross(lower[lower.len() - 2], lower[lower.len() - 1], p) <= 0.0 {
lower.pop();
}
lower.push(p);
}
let mut upper = vec![];
for &p in points.iter().rev() {
while upper.len() >= 2 && cross(upper[upper.len() - 2], upper[upper.len() - 1], p) <= 0.0 {
upper.pop();
}
upper.push(p);
}
lower.pop();
upper.pop();
lower.extend(upper);
lower
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hull() {
let pts = vec![
Point { x: 0.0, y: 0.0 },
Point { x: 1.0, y: 0.0 },
Point { x: 0.5, y: 0.5 },
Point { x: 0.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
];
assert_eq!(convex_hull(pts).len(), 4);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Point type | struct Point { x: f64, y: f64 } | Record { x: float; y: float } |
| Float sort | partial_cmp().unwrap() or total_cmp | compare (handles NaN differently) |
| Stack | Vec<Point> with pop/push | list used as stack or Stack.t |
| Cross product | Returns f64 | Returns float |
| Collinear handling | <= 0.0 in condition | Same |
| Exact arithmetic | i64 coordinates or Ratio crate | Zarith.q rationals |
OCaml Approach
OCaml represents points as { x: float; y: float } or (float * float). The cross function is a pure floating-point computation. Sorting uses List.sort with a comparison function. OCaml's Stack module or a list used as a stack implements the hull construction. List.rev reverses for the upper hull pass. OCaml's compare function handles float comparison with NaN semantics differing from IEEE. For exact arithmetic, OCaml uses Zarith with rational coordinates.
Full Source
#![allow(clippy::all)]
//! # Convex Hull (Graham Scan)
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
fn cross(o: Point, a: Point, b: Point) -> f64 {
(a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
}
pub fn convex_hull(mut points: Vec<Point>) -> Vec<Point> {
if points.len() < 3 {
return points;
}
points.sort_by(|a, b| {
a.x.partial_cmp(&b.x)
.unwrap()
.then(a.y.partial_cmp(&b.y).unwrap())
});
let mut lower = vec![];
for &p in &points {
while lower.len() >= 2 && cross(lower[lower.len() - 2], lower[lower.len() - 1], p) <= 0.0 {
lower.pop();
}
lower.push(p);
}
let mut upper = vec![];
for &p in points.iter().rev() {
while upper.len() >= 2 && cross(upper[upper.len() - 2], upper[upper.len() - 1], p) <= 0.0 {
upper.pop();
}
upper.push(p);
}
lower.pop();
upper.pop();
lower.extend(upper);
lower
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hull() {
let pts = vec![
Point { x: 0.0, y: 0.0 },
Point { x: 1.0, y: 0.0 },
Point { x: 0.5, y: 0.5 },
Point { x: 0.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
];
assert_eq!(convex_hull(pts).len(), 4);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hull() {
let pts = vec![
Point { x: 0.0, y: 0.0 },
Point { x: 1.0, y: 0.0 },
Point { x: 0.5, y: 0.5 },
Point { x: 0.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
];
assert_eq!(convex_hull(pts).len(), 4);
}
}
Deep Comparison
OCaml vs Rust: Convex Hull Graham
Overview
See the example.rs and example.ml files for detailed implementations.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Type system | Hindley-Milner | Ownership + traits |
| Memory | GC | Zero-cost abstractions |
| Mutability | Explicit ref | mut keyword |
| Error handling | Option/Result | Result<T, E> |
See README.md for detailed comparison.
Exercises
point_in_convex_hull function using binary search on the hull angles in O(log n).