一、凸包
凸包(Convex Hull)是一个计算几何(图形学)中的概念。
在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。X的凸包可以用X内所有点(X1,...Xn)的凸组合来构造。
简单来讲,给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,它的内部包含了点集中其余所有的点。
二、凸包算法的基本思路
如下图所示:
①首先我们要从最左、最下的点开始,如图中的B点(‘左’的优先级高,假设点B的正下方有一点Z,则从Z点开始);
②设一个向量以B点作为根部,方向朝正上方‘↑’;
③令向量绕根部顺时针旋转,直到找到第一个在向量所指向的点(如图中点G,点F是第二个向量所能指向的点),若向量方向上有多个点,则选取距离最远的点;
④令该点成为向量新的起点,方向不变(该步中方向为向量的方向);
⑤重复③、④步,直到③中所找的点为初始点(图中的B点);
⑥整个过程中找到的所有的点构成了凸包的点集,相邻的找到的点之间的连线构成了凸包多边形,该图中找到的点的顺序为B - G - D - K - H - J - B。
三、JAVA求解凸包的一些准备工作
在最终求解凸包算法之前,我们需要准备一些辅助计算的方法:
(1)calculateRegularPolygonAngle(int sides)
①作用:输入多边形边数sides,返回多边形的内角度数总和;
②代码实现:
public static double calculateRegularPolygonAngle(int sides) {
//如果多边形边数小于3,则不是多边形,返回0
if(sides<3)return 0;
//否则,代入公式计算内角和
double angle=(double)(sides-2)*180/sides;
return angle;
}
(2)calculateBearingToPoint(double currentBearing, int currentX, int currentY, int targetX, int targetY)
①作用:输入目标点、向量根部点的坐标,以及向量方向与Y轴正方向的夹角,返回向量想要指向目标点所要转过的正角(正角:顺时针所要扫过的角度);
②代码实现:
public static double calculateBearingToPoint(double currentBearing, int currentX, int currentY,
int targetX, int targetY) {
//计算从根部点到目标点的向量的横坐标
double x=(double)(targetX-currentX);
//同上,计算向量的纵坐标
double y=(double)(targetY-currentY);
//调用Math类下的atan2方法,计算向量所要偏转的正角度
double degree=90-currentBearing-Math.toDegrees(Math.atan2(y, x));
//如果角度为负,则转为正角
if(degree<0) degree+=360;
return degree;
}
四、JAVA求解凸包的主函数
Set<Point> convexHull(Set<Point> points)
①作用:输入所有点构成的点集,返回凸包多边形的点集;
②代码实现:
public static Set<Point> convexHull(Set<Point> points) {
//判断点的总数是否小于3,小于3则不能构成多边形
if(points.size() < 3) {
return points;
}
//定义新的Set集合,其中不会有重复元素,符合我们的要求
Set<Point> set = new HashSet<>();
Point xmin = new Point(Double.MAX_VALUE, Double.MAX_VALUE);
//运用for-each遍历的方式,在所有点中寻找最左的点
for(Point item : points) {
if (item.x() < xmin.x() || (item.x() == xmin.x() && item.y() < xmin.y()))
xmin = item;
}
//设最左的点为初始起点
Point nowPoint = xmin, tempPoint = xmin;
//初始化指向角度为0
double nowAngle = 0, minAngle = 360, tempAngle = 0;
double distance;
double maxdistance = 0;
//无差别地遍历所有的点
do{
set.add(tempPoint);
// 遍历全部点,寻找下一个在凸包上的点
for(Point item : points) {
//当某一点不在点集之中或者该点为起始点
if ((!set.contains(item) || item == xmin) ) {
//调用判断calculateBearingToPoint方法计算所需要偏转的角度
tempAngle = calculateBearingToPoint(nowAngle, (int)nowPoint.x(), (int)nowPoint.y(), (int)item.x(), (int)item.y());
//计算目标点与所在点之间的距离
distance = (item.x() - nowPoint.x())*(item.x() - nowPoint.x()) + (item.y() - nowPoint.y())*(item.y() - nowPoint.y());
/*如果某一点的偏转角比之前所找到的最小角度还要小
则该角度成为了最小偏转角
多个点在同一方向上时取距离所在点最远的目标点*/
if(tempAngle < minAngle || ((tempAngle == minAngle) && (distance > maxdistance))) {
minAngle = tempAngle;
tempPoint = item;
maxdistance = distance;
}
}
}
//遍历完所有点后,初始化判断指标,从刚刚找到的目标点再次出发,重复上述步骤
nowAngle = minAngle;
minAngle = 360;
nowPoint = tempPoint;
} while(nowPoint != xmin); // 当下一个点为第一个点时找到了凸包上的全部点,退出循环
return set;
}
最终返回的Set类型的集合就是我们所要求得的凸包多边形点集。
五、实现过程中需要的Math类中的方法
(1)Math.round
①方法原型:long Math.round(double a)
②作用:输入一个浮点型小数,返回四舍五入后的值
(2)Math.toDegrees
①方法原型:double Math.toDegrees(double radian)
②作用:输入一个弧度制,返回对应的角度值
(3)Math.atan2(y,x)
①方法原型:double Math.atan2(double y,double x) (注意:输入的坐标值中纵坐标y在前面)
②作用:输入一个以原点为起点的向量所指向的一个点的坐标(x,y),返回该向量与X轴所夹正角的弧度值arctan(y/x)