凸包
首先我们解释一下什么叫做凸包。在算法导论第三版第33章中对于凸包的定义如下:点集Q的凸包是一个最小的凸多边形P,满足Q中的每个点都在P的边界上或者在P的内部。(关于凸多边形的准确定义大家可以自行百度。。)。我们假定Q中所有的点是独立的,并且Q至少包含3个不共线的点。直观地讲,可以把Q中的每个点都想象成是露在一块板外的铁钉,那么凸包就是包围了所有这些铁钉的一条拉紧了的橡皮绳所构成的形状。下图示出了一个点集及其凸包。
在算法导论这本书中将这种算法称作Jarvis步进法(Jarvis march),其运行时间为O(nh),其中h为凸包中的顶点数。当h为o(lgn)时,Jarvis步进法在渐近意义上比Graham扫描法更快。
Jarvis步进法运用了被称为“旋转扫除”的技术,根据每个顶点对一个参照顶点的极角的大小,依次进行处理。
算法描述
我们首先在平面上建立一个平面直角坐标系,使每个点都处于第一象限中(方便观察)。
算法中首先找到最左边的点,这个点一定是在凸包中的,然后遍历点集,计算与该点连接点中偏转角最小的,找到后放入点集,然后再次以这个新放入的点为计算点,再次计算偏转角中最小的点。。。依次循环下去,直到回到起始点。
Java程序实现
首先我们要先定义一个point类
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;
}
}
接下里是我们程序的实现代码,calculateBearingToPoint1函数是用来计算偏转角度的,第一个参数currentBearing保存了图保中上一个点与当前点相较于y轴的偏转角度。计算偏转角时方法并不唯一,我这里应用的是Java中自带的atan2函数,也可以用其他三角函数进行计算。
当点集中的点少于等于两个时,我们直接返回点集即可,因为此时所有的点都一定在凸包中。否则的话需要循环判断。(在这里因为老师给的凸包和算法导论中定义有一点点区别,我根据JUnit的测试对程序进行了一点修改)
public static double calculateBearingToPoint1(double currentBearing, double currentX, double currentY,
double targetX, double targetY) {
double angle = Math.atan2(targetX - currentX, targetY - currentY) * 180 / Math.PI;
double turnAngle = angle - currentBearing;
if (turnAngle < 0)
turnAngle += 360;
return turnAngle;
}
public static Set<Point> convexHull(Set<Point> points) {
LinkedHashSet<Point> convexHull = new LinkedHashSet<Point>();
if(points.size() <= 2) {
return points;
}
else {
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{
convexHull.add(nowPoint);
for(Point item : points) {
if ((!convexHull.contains(item) || item == xmin) ) {
tempAngle = calculateBearingToPoint1(nowAngle, nowPoint.x(), nowPoint.y(), item.x(), 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 convexHull;
}
}
相较于这种实现方法,我们还可以采用左右链的策略,好处就是程序看着没这么抽象,不用再写一个计算角度的方法,消除了需要显式地计算角度所带来的代码量的增加。(毕竟,少一行代码少一点出错的风险)。左右链的方法大家可以自行查阅。
时间复杂度分析
对于凸包中的h个顶点,都需要找到具有做小偏转角的顶点。采用上述方法,每次偏转角的比较操作所需的时间为O(1)。如果每次比较操作所需时间为O(1),则可以在O(n)时间内计算出n个值中的最小值。因此,Jarvis步进法的运行时间为O(nh)。
上述内容为本人根据课程要求查阅资料后的个人见解,作复习反思、交流学习之用。如果错误,感谢大家指出。