Skip to content

Commit 28c553e

Browse files
authored
Add graham scan algorithm (TheAlgorithms#510)
1 parent 1979e05 commit 28c553e

File tree

3 files changed

+232
-0
lines changed

3 files changed

+232
-0
lines changed

src/geometry/graham_scan.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
use crate::geometry::Point;
2+
use std::cmp::Ordering;
3+
4+
fn point_min(a: &&Point, b: &&Point) -> Ordering {
5+
// Find the bottom-most point. In the case of a tie, find the left-most.
6+
if a.y == b.y {
7+
a.x.partial_cmp(&b.x).unwrap()
8+
} else {
9+
a.y.partial_cmp(&b.y).unwrap()
10+
}
11+
}
12+
13+
// Returns a Vec of Points that make up the convex hull of `points`. Returns an empty Vec of there
14+
// is no convex hull.
15+
pub fn graham_scan(mut points: Vec<Point>) -> Vec<Point> {
16+
if points.len() <= 2 {
17+
return vec![];
18+
}
19+
20+
let min_point = points.iter().min_by(point_min).unwrap().clone();
21+
points.retain(|p| p != &min_point);
22+
if points.is_empty() {
23+
// edge case where all the points are the same
24+
return vec![];
25+
}
26+
27+
let point_cmp = |a: &Point, b: &Point| -> Ordering {
28+
// Sort points in counter-clockwise direction relative to the min point. We can this by
29+
// checking the orientation of consecutive vectors (min_point, a) and (a, b).
30+
let orientation = min_point.consecutive_orientation(a, b);
31+
if orientation < 0.0 {
32+
Ordering::Greater
33+
} else if orientation > 0.0 {
34+
Ordering::Less
35+
} else {
36+
let a_dist = min_point.euclidean_distance(a);
37+
let b_dist = min_point.euclidean_distance(b);
38+
// When two points have the same relative angle to the min point, we should only
39+
// include the further point in the convex hull. We sort further points into a lower
40+
// index, and in the algorithm, remove all consecutive points with the same relative
41+
// angle.
42+
b_dist.partial_cmp(&a_dist).unwrap()
43+
}
44+
};
45+
points.sort_by(point_cmp);
46+
let mut convex_hull: Vec<Point> = vec![];
47+
48+
// We always add the min_point, and the first two points in the sorted vec.
49+
convex_hull.push(min_point.clone());
50+
convex_hull.push(points[0].clone());
51+
let mut top = 1;
52+
for point in points.iter().skip(1) {
53+
if min_point.consecutive_orientation(point, &convex_hull[top]) == 0.0 {
54+
// Remove consecutive points with the same angle. We make sure include the furthest
55+
// point in the convex hull in the sort comparator.
56+
continue;
57+
}
58+
loop {
59+
// In this loop, we remove points that we determine are no longer part of the convex
60+
// hull.
61+
if top <= 1 {
62+
break;
63+
}
64+
// If there is a segment(i+1, i+2) turns right relative to segment(i, i+1), point(i+1)
65+
// is not part of the convex hull.
66+
let orientation =
67+
convex_hull[top - 1].consecutive_orientation(&convex_hull[top], point);
68+
if orientation <= 0.0 {
69+
top -= 1;
70+
convex_hull.pop();
71+
} else {
72+
break;
73+
}
74+
}
75+
convex_hull.push(point.clone());
76+
top += 1;
77+
}
78+
if convex_hull.len() <= 2 {
79+
return vec![];
80+
}
81+
convex_hull
82+
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::graham_scan;
87+
use super::Point;
88+
89+
fn test_graham(convex_hull: Vec<Point>, others: Vec<Point>) {
90+
let mut points = convex_hull.clone();
91+
points.append(&mut others.clone());
92+
let graham = graham_scan(points);
93+
for point in convex_hull {
94+
assert!(graham.contains(&point));
95+
}
96+
for point in others {
97+
assert!(!graham.contains(&point));
98+
}
99+
}
100+
101+
#[test]
102+
fn too_few_points() {
103+
test_graham(vec![], vec![]);
104+
test_graham(vec![], vec![Point::new(0.0, 0.0)]);
105+
}
106+
107+
#[test]
108+
fn duplicate_point() {
109+
let p = Point::new(0.0, 0.0);
110+
test_graham(vec![], vec![p.clone(), p.clone(), p.clone(), p.clone(), p]);
111+
}
112+
113+
#[test]
114+
fn points_same_line() {
115+
let p1 = Point::new(1.0, 0.0);
116+
let p2 = Point::new(2.0, 0.0);
117+
let p3 = Point::new(3.0, 0.0);
118+
let p4 = Point::new(4.0, 0.0);
119+
let p5 = Point::new(5.0, 0.0);
120+
// let p6 = Point::new(1.0, 1.0);
121+
test_graham(vec![], vec![p1, p2, p3, p4, p5]);
122+
}
123+
124+
#[test]
125+
fn triangle() {
126+
let p1 = Point::new(1.0, 1.0);
127+
let p2 = Point::new(2.0, 1.0);
128+
let p3 = Point::new(1.5, 2.0);
129+
let points = vec![p1, p2, p3];
130+
test_graham(points, vec![]);
131+
}
132+
133+
#[test]
134+
fn rectangle() {
135+
let p1 = Point::new(1.0, 1.0);
136+
let p2 = Point::new(2.0, 1.0);
137+
let p3 = Point::new(2.0, 2.0);
138+
let p4 = Point::new(1.0, 2.0);
139+
let points = vec![p1, p2, p3, p4];
140+
test_graham(points, vec![]);
141+
}
142+
143+
#[test]
144+
fn triangle_with_points_in_middle() {
145+
let p1 = Point::new(1.0, 1.0);
146+
let p2 = Point::new(2.0, 1.0);
147+
let p3 = Point::new(1.5, 2.0);
148+
let p4 = Point::new(1.5, 1.5);
149+
let p5 = Point::new(1.2, 1.3);
150+
let p6 = Point::new(1.8, 1.2);
151+
let p7 = Point::new(1.5, 1.9);
152+
let hull = vec![p1, p2, p3];
153+
let others = vec![p4, p5, p6, p7];
154+
test_graham(hull, others);
155+
}
156+
157+
#[test]
158+
fn rectangle_with_points_in_middle() {
159+
let p1 = Point::new(1.0, 1.0);
160+
let p2 = Point::new(2.0, 1.0);
161+
let p3 = Point::new(2.0, 2.0);
162+
let p4 = Point::new(1.0, 2.0);
163+
let p5 = Point::new(1.5, 1.5);
164+
let p6 = Point::new(1.2, 1.3);
165+
let p7 = Point::new(1.8, 1.2);
166+
let p8 = Point::new(1.9, 1.7);
167+
let p9 = Point::new(1.4, 1.9);
168+
let hull = vec![p1, p2, p3, p4];
169+
let others = vec![p5, p6, p7, p8, p9];
170+
test_graham(hull, others);
171+
}
172+
173+
#[test]
174+
fn star() {
175+
// A single stroke star shape (kind of). Only the tips(p1-5) are part of the convex hull. The
176+
// other points would create angles >180 degrees if they were part of the polygon.
177+
let p1 = Point::new(-5.0, 6.0);
178+
let p2 = Point::new(-11.0, 0.0);
179+
let p3 = Point::new(-9.0, -8.0);
180+
let p4 = Point::new(4.0, 4.0);
181+
let p5 = Point::new(6.0, -7.0);
182+
let p6 = Point::new(-7.0, -2.0);
183+
let p7 = Point::new(-2.0, -4.0);
184+
let p8 = Point::new(0.0, 1.0);
185+
let p9 = Point::new(1.0, 0.0);
186+
let p10 = Point::new(-6.0, 1.0);
187+
let hull = vec![p1, p2, p3, p4, p5];
188+
let others = vec![p6, p7, p8, p9, p10];
189+
test_graham(hull, others);
190+
}
191+
192+
#[test]
193+
fn rectangle_with_points_on_same_line() {
194+
let p1 = Point::new(1.0, 1.0);
195+
let p2 = Point::new(2.0, 1.0);
196+
let p3 = Point::new(2.0, 2.0);
197+
let p4 = Point::new(1.0, 2.0);
198+
let p5 = Point::new(1.5, 1.0);
199+
let p6 = Point::new(1.0, 1.5);
200+
let p7 = Point::new(2.0, 1.5);
201+
let p8 = Point::new(1.5, 2.0);
202+
let hull = vec![p1, p2, p3, p4];
203+
let others = vec![p5, p6, p7, p8];
204+
test_graham(hull, others);
205+
}
206+
}

src/geometry/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
mod closest_points;
2+
mod graham_scan;
23
mod point;
34
mod segment;
45

56
pub use self::closest_points::closest_points;
7+
pub use graham_scan::graham_scan;
68
pub use point::Point;
79
pub use segment::Segment;

src/geometry/point.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::ops::Sub;
2+
3+
#[derive(Clone, Debug, PartialEq)]
14
pub struct Point {
25
pub x: f64,
36
pub y: f64,
@@ -8,7 +11,28 @@ impl Point {
811
Point { x, y }
912
}
1013

14+
// Returns the orientation of consecutive segments ab and bc.
15+
pub fn consecutive_orientation(&self, b: &Point, c: &Point) -> f64 {
16+
let p1 = b - self;
17+
let p2 = c - self;
18+
p1.cross_prod(&p2)
19+
}
20+
1121
pub fn cross_prod(&self, other: &Point) -> f64 {
1222
self.x * other.y - self.y * other.x
1323
}
24+
25+
pub fn euclidean_distance(&self, other: &Point) -> f64 {
26+
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
27+
}
28+
}
29+
30+
impl Sub for &Point {
31+
type Output = Point;
32+
33+
fn sub(self, other: Self) -> Point {
34+
let x = self.x - other.x;
35+
let y = self.y - other.y;
36+
Point::new(x, y)
37+
}
1438
}

0 commit comments

Comments
 (0)