Gift-Wrapping算法解决凸包问题
凸包问题描述
给定二维平面上的点集,凸包就是将最外层的点连接起来构成的凸多边形,使得所有的点都在凸多边形内部或边界上,并且需要保证构成凸包的点的个数最少。
Gift-Wrapping算法描述
从上图可以发现从点p12开始,点p10是相对于p12偏转角度最小的那个点;点p3又是相对于p10来说偏转角度最小的点……可以发现凸包上的点都满足这个规律。算法思路:
1. 遍历全部的点,找到最左侧的点,则这个点一定是凸包上的一个点,以这个点为凸包上的第一个点,初始角度为0°(相对于y轴正向的顺时针偏移)。
2. 寻找凸包上的下一个点:遍历全部的点,找到从当前点的当前角度转向该点所需旋转最小角度的点,可以使用calculateBearingToPoint来完成计算。
3. 以寻找到的下一个点为当前点,当前角度更新为原角度加上旋转角度的和,然后继续去寻找下一个点,直到找到的下一个点为第一个时结束。此时就找到了凸包上的全部点。
注:在这里我们假定处于每一个点时还会有一个方向,具体大小是相对于y轴正向的偏移角度。
先计算calculateBearingToPoint:代码实现(Java)
- 先利用Math.atan2()函数计算两点的连线与x轴正向逆时针所成的夹角degree。
- 然后degree -= 90,计算出相对于y轴正向的逆时针偏移角度。
- 然后区相反数,即相对于y轴正向的顺时针偏移角度。
- 最后用计算出的degree与当前的角度做减法,然后处理小于0°和大于360°的情况即可计算出从一个点的某一个角度转向另一个点需要顺时针旋转的角度。
/**
* 从第一个点(currentX,currentY)且当前的角度偏移为currentBearing转向第二个点(targetX,targetY)
* 所需要顺时针旋转的角度
*
* @param currentBearing 当前相对于y轴正向的偏移
* @param currentX 第一个点的x坐标
* @param currentY 第一个点的y坐标
* @param targetX 第二个点的x坐标
* @param targetY 第二个点的y坐标
* @return 需要顺时针旋转的角度
*/
public static double calculateBearingToPoint(double currentBearing, int currentX, int currentY,
int targetX, int targetY) {
double degrees = Math.toDegrees(Math.atan2(targetY-currentY, targetX-currentX)); // 两点相对于x轴正向的逆时针角度
if(degrees < 0) {
degrees += 360;
}
degrees -= 90; // 相对于y轴正向逆时针角度
degrees = -degrees; // 相对于y轴正向顺时针角度
if(degrees < 0) {
degrees += 360;
}
if(currentBearing <= degrees) {
return degrees-currentBearing;
}
return 360.0 + degrees - currentBearing;
}
/**
* 计算凸包
*
* @param points 平面上的点集合,需保证至少有三个不共线的点
* @return 点的数量最少的凸包
*/
public static Set<Point> convexHull(Set<Point> points) {
if(points.size() < 3) {
return points;
}
Set<Point> set = new HashSet<>();
Point xmin = new Point(Double.MAX_VALUE, Double.MAX_VALUE);
// 寻找最左的点
for(Point item : points) {
if (item.x() < xmin.x() || (item.x() == xmin.x() && item.y() < xmin.y()))
xmin = item;
}
Point nowPoint = xmin, tempPoint = xmin;
double nowAngle = 0, minAngle = 360, tempAngle = 0;
double distance;
double maxdistance = -1;
do{
set.add(nowPoint);
// 遍历全部点,寻找下一个在凸包上的点
for(Point item : points) {
if ((!set.contains(item) || item == xmin) ) {
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;
}
}
}
minAngle = 360;
nowAngle = minAngle;
nowPoint = tempPoint;
} while(nowPoint != xmin); // 当下一个点为第一个点时找到了凸包上的全部点,退出循环
return set;
}
代码中Point的定义如下:
/**
* An immutable point in floating-point pixel space.
*/
public class Point {
private final double x;
private final double y;
/**
* Construct a point at the given coordinates.
* @param x x-coordinate
* @param y y-coordinate
*/
public Point(double x, double y) {
this.x = x;
this.y = y;
}
/**
* @return x-coordinate of the point
*/
public double x() {
return x;
}
/**
* @return y-coordinate of the point
*/
public double y() {
return y;
}
}
因为每次的起点都是上次找到的凸包点,因此外层循环的复杂度为O(H),H为凸包上的点,内层循环每次都会全部遍历点,因此时间复杂度为 O(n) ,因此总的是间复杂度为 O(nH) ,在一般情况下 凸包上的点的期望为logn ,算法复杂度为 O(nlogn) ,极端情况下,如下所示,所有点都在类似圆弧上的话,外层循环也是n,因此会达到O(n^2)。复杂度分析
如果你想了解更多我对编程和人生的思考,请关注公众号:青云学斋