Graham scan算法主要步驟:
-
找出所有已知點的y值最小,如果相同,取x值最小的點,作爲基準點s。
-
以s爲基準,所有的點按照與X軸夾角從小到大排序。
-
使用兩個棧,一個記錄已訪問的點,一個記錄未訪問的點,使用已訪問點的最後兩個入棧元素p,q判斷未訪問點元素c的位置,如果點c在pq向量的左邊,則將c壓入已訪問棧,如果c在pq向量的右邊,則已訪問棧出pop()一個元素。再次判斷。
1,實現:
(1.1)Point的數據結構爲:
public class Point {
public double x;
public double y;
public Point() {
}
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public static Point of(double x, double y){
return new Point(x, y);
}
public static Point Y = new Point(0, 1);
public static Point X = new Point(1, 0);
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
(1.2)是要找到最下,最左的點s,這裏只需要通過一次遍歷就可以得到
private static int LTL(Point[] s, int n) {
int rank = 0;
for (int i = 0; i < s.length; i++)
if (s[rank].y > s[i].y || (s[rank].y == s[i].y && s[rank].x > s[i].x))
rank = i;
return rank;
}
(1.3)以s點爲基準,按照與x軸形成的夾角從小到大排序。排序算法的複雜度O(nlogn)
private static Point[] sort(Point[] s, int n, Point base) {
// cos = a*
return Arrays.stream(s).sorted((o1, o2) -> {
double v1 = calculateDegree(new Point(o1.x - base.x, o1.y - base.y), Point.X);
double v2 = calculateDegree(new Point(o2.x - base.x, o2.y - base.y), Point.X);
return Double.compare(v1, v2);
}).toArray(Point[]::new);
}
計算角度的方法
/**
* -> ->
* a b
* cosα=ab/|a||b|=(x1y1+x2,y2)/(根號(x1^2+y1^2)根號(x2^2+y1^2))
*/
private static double calculateDegree(Point a, Point b) {
double v = (a.x * b.x + a.y * b.y) / (Math.pow(Math.pow(a.x, 2) + Math.pow(a.y, 2), 0.5) * Math.pow(Math.pow(b.x, 2) + Math.pow(b.y, 2), 0.5));
return Math.acos(v);
}
(1.4)主方法體
public static Stack<Point> grahamScan(Point[] s, int n) {
int start = LTL(s, n);
Point sp = s[start];
Point[] sorted = sort(s, n, sp);
Stack<Point> visited = new Stack<>();
Stack<Point> unvisited = new Stack<>();
visited.push(sp);
visited.push(sorted[0]);
// attention: sort by degree base on start point
// cut head and tail, the smallest and the biggest
for (int i = 1; i <= s.length - 1; i++) {
unvisited.push(sorted[i]);
}
while (!unvisited.empty()) {
Point p1 = visited.get(visited.size() - 2);
Point p2 = visited.get(visited.size() - 1);
System.out.println(String.format("(%s, %s)->(%s, %s)", p1.x, p1.y, p2.x, p2.y));
if (isLeft(visited.get(visited.size() - 2), visited.get(visited.size() - 1), unvisited.get(0))) {
visited.push(unvisited.remove(0));
} else {
visited.pop();
}
}
return visited;
}
2,測試數據以及測試代碼
public static void test() throws URISyntaxException, IOException {
URL resource = Thread.currentThread().getContextClassLoader().getResource("convex/convex.txt");
Path of = Path.of(Objects.requireNonNull(resource).toURI());
Point[] points = Files.lines(of).skip(1).map(e -> {
String[] s = e.split(" ");
return new Point(Integer.parseInt(s[0]), Integer.parseInt(s[1]));
}).toArray(Point[]::new);
List<Point> hull = PointPosition.grahamScan(points, points.length);
for (Point point : hull) {
System.out.println(String.format("(%s, %s)", point.x, point.y));
}
}
這裏是測試點集,convex.txt內容如下
10
7 9
-8 -1
-3 -1
1 4
-3 9
6 -4
7 5
6 6
-6 10
0 8
程序運行結果爲,也就是構成凸包的點集
"C:\Program Files\Java\jdk-11.0.7\bin\java.exe" "-javaagent: computationalgeometry.ConvexHull
(6.0, -4.0)
(7.0, 5.0)
(7.0, 9.0)
(-6.0, 10.0)
(-8.0, -1.0)
(6.0, -4.0)
3,結果驗證
(3.1)利用python3.7畫的圖:很遺憾不知道怎麼把凸包上的點鏈接起來
Python源碼爲:
# coding=utf-8
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot()
# 設置標題
ax1.set_title('convex hull')
# 設置X軸標籤
plt.xlabel('X')
# 設置Y軸標籤
plt.ylabel('Y')
# 畫散點圖
for i, j in [(7, 9), (-8, -1), (-3, -1), (1, 4), (-3, 9), (6, -4), (7, 5), (6, 6), (-6, 10), (0, 8)]:
ax1.scatter(i, j, c='r', marker='o')
plt.text(i, j, (i, j), ha='center', va='bottom', fontsize=10)
# 設置圖標
plt.legend('x1')
# 顯示所畫的圖
plt.show()
手動將程序輸出點集鏈接起來。
、
目測結果是正確的。
最後附上CAD2016畫的圖,這個圖畫的不大好,但是由於是矢量圖,所以縮放不會失真哦!!