本文对lab1中使用到的凸包算法进行阐述。
凸包:
先来看凸包的定义。此处摘自百度百科:
点集Q的凸包(convex hull)是指一个最小凸多边形,满足Q中的点或者在多边形边上或者在其内。图1由红色线段表示的多边形就是点集Q={p0,p1,...p12}的凸包。
简单来讲,一组平面上的点,求一个包含所有点的最小的凸多边形,这就是凸包问题了。这可以形象地想成这样:在地上放置一些不可移动的木桩,用一根绳子把他们尽量紧地圈起来,并且为凸边形,这就是凸包了。
凸包算法:
我在实验中使用的算法步骤如下:
- 如果点数少于等于三个,可以直接返回这些点。
- 如果点数多于三,则对这些点进行处理。首先取纵坐标最小的点,然后以这个点为原点计算出新的坐标。
- 以新的坐标计算每个角的幅角,并按照幅角由小到大的顺序对点进行排序。如果幅角相等,则将与原点距离更近的排在前面。
- 对处理完的点,易知第一个和第二个点是在凸包中的。连接这两个点,计算第三个点与这个位置的相对位置。如果点在直线的左边,跳转到5),否则跳转到6)
- 这个点是凸包中的点。连接这个点和上一个点,产生接下来作为基准的线,然后继续处理。
- 这个点的上一个点不是凸包中的点。把这个点从凸包中剔除,同时连接上上个点和上上上个点,产生接下来作为基准的线,然后继续处理
- 当处理完最后一个点后,剩下来的点就是凸包中的点。
具体实现过程中,要处理给到的点集。由于给的是Set类,这个类不能简单遍历所有元素,要调用迭代器或使用另一种for循环。我选择将Set的点集修改为List类,这样就有了固定的顺序,方便后续操作处理。
在判断凸包的点时,可以使用栈,从而记录之前处理后的数据。这样能在回退到之前的点时直接从栈中取数据,不需要再额外记录。
代码如下
public static Set<Point> convexHull(Set<Point> points) {
Set<Point> BeginPoints = new HashSet<Point>();
Set<Point> FinalPoints = new HashSet<Point>();
List<Point> TempPoints2 = new ArrayList<>();
BeginPoints.addAll(points);
int Number = BeginPoints.size();
if(Number <= 3)//少于或只有三个点可以直接返回
return BeginPoints;
else
{
Iterator<Point> it1 = BeginPoints.iterator();
Point yLowest = it1.next();
TempPoints2.add(yLowest);
Point TempPoint = null;
while(it1.hasNext())//取纵坐标最小的点
{
TempPoint = it1.next();
TempPoints2.add(TempPoint);
if(TempPoint.y() < yLowest.y())
yLowest = TempPoint;
}
List<Point> TempPoints1 = new ArrayList<>();
double[] alpha = new double[Number];//存放幅角
double[] distance = new double[Number];//存放距离
int[] index = new int[Number];//存放排序后的序列
for (int i = 0; i < Number ; i++)//把所有点的坐标平移,使得纵坐标最小的点为原点
{
TempPoint = new Point(TempPoints2.get(i).x() - yLowest.x(), TempPoints2.get(i).y() - yLowest.y());
TempPoints1.add(TempPoint);
}
for (int i = 0; i < Number ; i++)//计算幅角alpha和与原点距离distance
{
distance[i] = Math.pow(Math.pow(TempPoints1.get(i).x(),2)+Math.pow(TempPoints1.get(i).y(),2),0.5);
alpha[i] = Math.atan2(TempPoints1.get(i).y(), TempPoints1.get(i).x());
index[i] = i;
}
for (int i = 0; i < Number - 1 ; i++)//按由小到大顺序排列
{
for(int j = 0; j < Number - i - 1; j++)
{
if(alpha[j] > alpha[j+1])
{
int tempIndex = index[j];
index[j] = index[j+1];
index[j+1] = tempIndex;
double tempdegree = alpha[j];
alpha[j] = alpha [j+1];
alpha[j+1] = tempdegree;
}
else if(alpha[j] == alpha[j+1])
{
if(distance[j] > distance[j+1])//幅角相等,把距离更近的排在前面
{
int tempIndex = index[j];
index[j] = index[j+1];
index[j+1] = tempIndex;
double tempdegree = alpha[j];
alpha[j] = alpha [j+1];
alpha[j+1] = tempdegree;
}
}
}
}//得到幅角alpha从小到大排列的顺序,存放在index[Number]中
//开始确定凸包的各个点
Stack<Integer> stack = new Stack<>();
Stack<Double> DegreeStack = new Stack<>();
double beta, CurrentBearing;
CurrentBearing = alpha[1];//初始幅角
stack.push(index[0]);//前两个点入栈
stack.push(index[1]);
for (int i = 2; i < Number ; i++)
{
TempPoint = TempPoints1.get(index[i]);//取出判断栈顶要用的下一个点
//以栈顶点作为原点,计算下一个点的幅角
beta = Math.atan2(TempPoint.y() - TempPoints1.get(stack.peek()).y(), TempPoint.x() - TempPoints1.get(stack.peek()).x());
if(beta < 0) beta = 360.0 + beta / Math.PI *180;//修改幅角大小为正数
if(beta < CurrentBearing)//下一个点在右侧
{
stack.pop();//则栈顶不在凸包内,出栈
//以新的栈顶作为原点,计算下一个点的幅角
beta = Math.atan2(TempPoint.y() - TempPoints1.get(stack.peek()).y(), TempPoint.x() - TempPoints1.get(stack.peek()).x());
while(beta < DegreeStack.peek())//下一个点还在右侧
{
stack.pop();//则新栈顶仍不在凸包内,出栈
DegreeStack.pop();//修改角度栈栈顶
//以新的栈顶作为原点,计算下一个点的幅角
beta = Math.atan2(TempPoint.y() - TempPoints1.get(stack.peek()).y(), TempPoint.x() - TempPoints1.get(stack.peek()).x());
}
stack.push(index[i]);//把判断点入栈
CurrentBearing = beta;//修改新的幅角
}
else if(beta > CurrentBearing) //下一个点在左侧
{
stack.push(index[i]);//把判断点入栈
DegreeStack.push(CurrentBearing);//现有幅角变为上一个幅角
CurrentBearing = beta;//修改新的幅角
}
else //下一个点在直线上
{
stack.pop();//取出直线上的点
stack.push(index[i]);//把判断点入栈
}
}
while(stack.empty() == false)//取出栈中所有的点
{
FinalPoints.add(TempPoints2.get(stack.pop()));
}
return FinalPoints;
}
}
: