凸壳问题:
在平面上,给定n个点组成的集合,其凸壳就是包含这些点的最小凸多边形。这个凸多边形的任何一条边所在的直线都会把凸多边形全部划在同一个半平面内。举个简单的例子,在木板是随机钉入多个钉子,用个橡皮筋将所有钉子包起来,这即为包凸问题,显而易见橡皮筋最后所形成的形状即为凸壳问题的解。
Graham扫描法(格雷厄姆算法):
格雷厄姆算法基于分治策略,通过递归的方式将问题分解为更小的子问题,最终将所有子问题的解合并为原始问题的解。在实际应用中先找到一个在包凸上的点(极点),逆时针方向找到包凸上的所有点。格雷厄姆的两个关键知识点:
角排量:对所有点按照该点与极点形成的线段与x轴正方向所形成的夹角升序排序
叉积:向量积,OA与OB向量积小于0时,OA位于OB左侧
代码实现:
//凸包算法(Graham算法)
public static List<Point> GrahamScan(List<Point> points){
if (points.size() < 3){
return points;
}
//对点坐标冒泡排序 先y再x,找到左下角的点
Collections.sort(points, new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
if (o1.y != o2.y) {
return Double.compare(o1.y, o2.y);
} else {
return Double.compare(o1.x,o2.x);
}
}
});
Point point = points.get(0);
points.remove(0);
//极点与任意点连线,与x轴正方向形成夹角从小到大排序,角度相同近小远大
Collections.sort(points, new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
double angle_X1 = Math.atan2(o1.y - point.y,o1.x - point.x);
double angle_X2 = Math.atan2(o2.y - point.y,o2.x - point.x);
if (angle_X1 != angle_X2){
return Double.compare(angle_X1,angle_X2);
}else {
return Double.compare(o1.y,o2.y);
}
}
});
points.add(0,point);
//创建栈(栈顶操作,先进后出),放入坐标
Deque<Point> stack = new ArrayDeque<>();//双端队列
stack.add(points.get(1));
stack.add(points.get(0));//top
//实现格雷厄姆算法,执行格雷厄姆扫描:从极点出发,按照标号连线
for (int i = 2; i < points.size();i++){
Point p = points.get(i);
while (stack.size() > 1){
Point top = stack.pop();//获取头节点,不放回
Point prev = stack.peek();//获取第二个节点,仅查看
Double R = point_Place(prev,top,p);
//p在向量左侧或三点共线且p点在两点之间
if (R > 0 ||(R == 0 && top.x > p.x)){
stack.push(top);//放回栈顶
break;
}
}
stack.push(p);
}
//从栈中提取凸包的顶点
List<Point> hull = new ArrayList<>();
while (!stack.isEmpty()) {
hull.add(stack.pollLast());
}
//Collections.reverse(hull);
return hull;
}
//辅助算法:判断点C与向量AB的位置
public static double point_Place(Point A,Point B,Point C){
return A.crossProduct(B,C);
}
class Point {
//坐标类
double x,y;
public Point(double X,double Y){
this.x = X;
this.y = Y;
}
//计算向量叉积,判断点位置 向量A*向量B >0 A在B右
public double crossProduct(Point A,Point B){
return (A.x - this.x)*(B.y - this.y) - (B.x - this.x)*(A.y - this.y);
}
@Override
public String toString() {
return "(" + x +
", " + y +
')';
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
}
注意:
Math.atan2()方法返回值,在一二象限为正,而在该实现方法中设定的极点的y坐标是最小值,所以以极点为原点排序时,所有点均在一二象限,不考虑为负的情况,若改为其他排序方式需考虑这个问题。