Skip to content

Commit 9315ce9

Browse files
authored
Graham scan implementation in Common Lisp (algorithm-archivists#605)
1 parent 4313e7e commit 9315ce9

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
;;;; Graham scan implementation in Common Lisp
2+
3+
(defstruct (point (:constructor make-point (x y))) x y)
4+
5+
(defun ccw (p1 p2 p3)
6+
"Determines if a turn between three points is counterclockwise"
7+
(-
8+
(*
9+
(- (point-y p2) (point-y p1))
10+
(- (point-x p3) (point-x p1)))
11+
(*
12+
(- (point-y p3) (point-y p1))
13+
(- (point-x p2) (point-x p1)))))
14+
15+
(defun atan2 (y x)
16+
"Calculates the angle of a point in the euclidean plane in radians"
17+
(cond
18+
((> x 0) (atan y x))
19+
((and (< x 0) (>= y 0)) (+ (atan y x) pi))
20+
((and (< x 0) (< y 0)) (- (atan y x) pi))
21+
((and (eql x 0) (> y 0)) (/ pi 2))
22+
((and (eql x 0) (< y 0)) (- (/ pi 2)))
23+
;; The -1 signifies an exception and is usefull later for sorting by the polar angle
24+
((and (eql x 0) (eql y 0)) -1)))
25+
26+
(defun polar-angle (ref point)
27+
"Returns the polar angle from a point relative to a reference point"
28+
(atan2 (- (point-y point) (point-y ref)) (- (point-x point) (point-x ref))))
29+
30+
(defun lowest-point (gift)
31+
"Returns the lowest point of a gift"
32+
(reduce
33+
(lambda (p1 p2)
34+
(if (< (point-y p1) (point-y p2)) p1 p2))
35+
gift))
36+
37+
(defun graham-scan (gift)
38+
"Finds the convex hull of a distribution of points with a graham scan"
39+
;; An empty list evaluates to false (nil) and a non-empty list evaluates to true (t).
40+
;; We can therefore use 'gift' instead of '(> (length gift) 0)'.
41+
(if gift
42+
(labels ((wrap (sorted-points hull)
43+
(if sorted-points
44+
;; This covers the case where the hull has one or more element.
45+
;; We aren't concerned about the hull being empty, because then the gift must
46+
;; also be empty and this function is never given an empty gift.
47+
(if (rest hull)
48+
(if (<= (ccw (first sorted-points) (first hull) (second hull)) 0)
49+
(wrap sorted-points (rest hull))
50+
(wrap (rest sorted-points) (cons (first sorted-points) hull)))
51+
(wrap (rest sorted-points) (list (first sorted-points) (first hull))))
52+
hull)))
53+
;; Because 'sort' shuffles things around destructively, graham-scan is also destructive. But
54+
;; since the order of the points is generally not important, this shouldn't cause a problem.
55+
(let* ((lowest (lowest-point gift))
56+
(sorted (sort gift #'< :key (lambda (p) (polar-angle lowest p)))))
57+
(wrap sorted (list lowest))))
58+
nil))
59+
60+
(defvar gift
61+
(map
62+
'list
63+
(lambda (e) (apply #'make-point e))
64+
'((-5 2) (5 7) (-6 -12) (-14 -14) (9 9)
65+
(-1 -1) (-10 11) (-6 15) (-6 -8) (15 -9)
66+
(7 -7) (-2 -9) (6 -5) (0 14) (2 8))))
67+
68+
;; This should print out the following:
69+
;; (#S(POINT :X -10 :Y 11) #S(POINT :X -6 :Y 15) #S(POINT :X 0 :Y 14)
70+
;; #S(POINT :X 9 :Y 9) #S(POINT :X 7 :Y -7) #S(POINT :X -6 :Y -12))
71+
(print (graham-scan gift))

contents/graham_scan/graham_scan.md

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ We can find whether a rotation is counter-clockwise with trigonometric functions
2626
[import:13-15, lang:"go"](code/golang/graham.go)
2727
{% sample lang="java" %}
2828
[import:27-29, lang:"java"](code/java/GrahamScan.java)
29+
{% sample lang="lisp" %}
30+
[import:5-13, lang:"lisp"](code/clisp/graham-scan.lisp)
2931
{% sample lang="cpp" %}
3032
[import:18-20, lang="cpp"](code/c++/graham_scan.cpp)
3133
{% endmethod %}
@@ -56,6 +58,8 @@ In the end, the code should look something like this:
5658
[import:21-42, lang:"go"](code/golang/graham.go)
5759
{% sample lang="java" %}
5860
[import:35-70, lang:"java"](code/java/GrahamScan.java)
61+
{% sample lang="lisp" %}
62+
[import:15-58, lang:"lisp"](code/clisp/graham-scan.lisp)
5963
{% sample lang="cpp" %}
6064
[import:26-62, lang="cpp"](code/c++/graham_scan.cpp)
6165
{% endmethod %}
@@ -81,6 +85,8 @@ In the end, the code should look something like this:
8185
[import, lang:"go"](code/golang/graham.go)
8286
{% sample lang="java" %}
8387
[import, lang:"java"](code/java/GrahamScan.java)
88+
{% sample lang="lisp" %}
89+
[import, lang:"lisp"](code/clisp/graham-scan.lisp)
8490
{% sample lang="cpp" %}
8591
[import, lang="cpp"](code/c++/graham_scan.cpp)
8692
{% endmethod %}

0 commit comments

Comments
 (0)