前言
常用凸包算法包括Graham Scan 算法和Jarvis March (Gift Wrapping) 算法,在这里要简单介绍的是Graham Scan 算法。
1、概念
凸包是一个点集所包围的最小的凸多边形。可以想象用一根绳子围绕着一群钉子,绳子所形成的轮廓便是这些钉子的凸包。在计算几何中,凸包得到了广泛的应用,涉及领域包括模式识别、图像处理和优化问题等。
2、算法原理
凸包算法的目标是从给定的点集(在二维平面中)确定出一个最小的凸多边形(即凸包)。此多边形的所有顶点是在输入点集中。
算法的核心在于以下几个基本概念:
2.1 、极角:对于一个基准点(通常是集合中最底部或最左边的点),其余每个点与基准点的连线形成的角度。通过极角的比较,我们可以确定相对位置。
2.2、栈结构:使用栈来存储凸包上的点。栈的特性允许我们很容易地回退到前一个状态,并且在确定凸包的顶点时非常有效。
2.3、方向判断(Orientation):通过计算三点(一个基准点和两个参考点)形成的向量的叉积来判断它们的相对方向,包括:
-
顺时针(Clockwise):两个参考点的连线向基准点转动的方式为顺时针。
-
逆时针(Counterclockwise):两个参考点的连线向基准点转动的方式为逆时针。
-
共线(Collinear):三点在同一条直线上。
3、算法步骤
以下是该算法的详细步骤:
3.1、选择基准点:遍历所有输入点,选择 Y 坐标最低的点作为基准点(如果有多个点具有相同的 Y 坐标,则选择 X 坐标最小的点),作为基准点(minPoint)。
3.2、极角排序:对于每个点P,计算它与基准点的极角并记录。可以使用 Math.Atan2()
函数来计算极角。排序的同时,如果有两个点的极角相同,则应该依据它们到基准点的距离进行排序,从近到远进行排列。
3.3、初始化栈:
-
创建一个栈并将基准点压入栈中。
-
将排好序的点依次压入栈中,初始先将基准点后面的两个点压入栈,以便形成开始的边。
3.4、构建凸包:排序后的点集,从第一个用于构建凸包的点开始。
-
检查栈顶的两个点和当前点的方向。使用前面提到的在
Orientation()
中实现的稳定性判断。 -
如果方向判断为“右转”,则弹出栈顶的点,直到遇到一个“左转”或“共线”的状态。
-
将当前点压入栈中,继续根据下一个点执行该步骤。
3.5、输出结果:当所有点处理完毕后,栈中包含的点即为最终的凸包。
4、代码
class MyConvexHull
{
// 定义一个点的类,包含 X 和 Y 坐标, 坐标类型为 double
public class Point2d
{
public double X { get; }
public double Y { get; }
public Point2d (double x, double y)
{
X = x;
Y = y;
}
}
// 计算两个点相对于基点的极角
private static double PolarAngle(Point2d p0, Point2d p1)
{
return Math.Atan2(p1.Y - p0.Y, p1.X - p0.X);
}
// 判断三个点的方向
private static int Orientation(Point2d p, Point2d q, Point2d r)
{
double val = (q.Y - p.Y) * (r.X - q.X) - (q.X - p.X) * (r.Y - q.Y);
if (val == 0) return 0; // collinear
return (val > 0) ? 1 : 2; // clockwise or counterclockwise
}
// Graham Scan 主函数,返回凸包的顶点列表
public static List<Point2d> GetConvexHull(List<Point2d> points)
{
// 1. 找到最低的点
Point2d minPoint = points[0];
foreach (var point in points)
{
if (point.Y < minPoint.Y || (point.Y == minPoint.Y && point.X < minPoint.X))
{
minPoint = point;
}
}
// 2. 将点按相对于基准点的极角进行排序
points.Sort((p1, p2) =>
{
double angle1 = PolarAngle(minPoint, p1);
double angle2 = PolarAngle(minPoint, p2);
return angle1.CompareTo(angle2);
});
// 3. 初始化栈
Stack<Point2d> hull = new Stack<Point2d>();
hull.Push(minPoint); // 压入基准点
hull.Push(points[0]); // 压入第一个排序后的点
// 4. 遍历剩余点,构建凸包
for (int i = 1; i < points.Count; i++)
{
while (hull.Count >= 2)
{
Point2d top = hull.Pop(); // 弹出栈顶元素
Point2d nextToTop = hull.Peek(); // 获取当前栈顶的下一个元素
// 检查当前点是否为顺时针
if (Orientation(nextToTop, top, points[i]) == 2) // 右转
{
hull.Push(top); // 若是右转,维持栈顶元素
break;
}
}
hull.Push(points[i]); // 压入当前点
}
// 结果为栈内点的列表
return new List<Point2d>(hull);
}
// 主程序
static void Main(string[] args)
{
// 示例点集
List<Point2d> points = new List<Point2d>
{
new Point2d(0, 3),
new Point2d(2, 2),
new Point2d(1, 1),
new Point2d(2, 1),
new Point2d(3, 0),
new Point2d(0, 0),
new Point2d(3, 3)
};
// 获取凸包
List<Point2d> convexHull = MyConvexHull::GetConvexHull(points);
// 输出凸包的顶点
Console.WriteLine("Convex Hull:");
foreach (var point in convexHull)
{
Console.WriteLine($"({Point2d.X}, {Point2d.Y})");
}
}
}
5、代码步骤说明
5.1、定义 Point2d
类:
-
该类用于表示二维空间中的点,包含
X
和Y
坐标,以及一个构造函数用于初始化坐标。
5.2、极角计算:
-
PolarAngle(Point p0, Point p1)
:接受两个点,计算第二个点相对于第一个点形成的极角。使用Math.Atan2()
函数来求取两点之间的角度,返回值为弧度。
5.3、方向判断:
-
Orientation(Point2d p, Point2d q, Point2d r)
:此函数通过计算三点的叉积来判断它们的相对方向。返回值表示这三点的关系,能够帮助我们判断当前点是向“左转”、“右转”还是“共线”。
5.4、获取凸包的主函数:
5.4.1、GetConvexHull(List<Point2d> points)
:该函数实现了 Graham Scan 算法的核心逻辑,输入一个点的列表并返回构成凸包的点。、
-
步骤一:
找到最底部点:遍历点集,找到 Y 坐标最低的点,将其作为基准点。
-
步骤二:
对所有点(包括基准点)按相对于基准点的极角进行排序。
-
步骤三:
初始化栈:创建栈并将基准点和第一个排序后的点推入栈中。
-
步骤四:
构建凸包:遍历剩余的点:
-
-
弹出栈顶元素,检查当前点与栈顶前两个点的方向。
-
如果是右转,保留后面的点,继续弹出直到遇到左转或栈中的点少于两个。
-
将当前点压入栈中。
-
5.4.2、返回值:栈中的点即为构成凸包的点。
5.5、主程序:
-
创建一个示例点集。
-
调用
GetConvexHull(points)
获取凸包,接着打印输出结果。
6、总结
该代码通过 Graham Scan 算法实现了一个有效的二维空间的凸包计算。利用栈结构和极角排序,能够高效地构建并返回凸包顶点。通过对代码的逐步解析,我们可以清楚地理解每个步骤的目的和意义,能够较好扩展到其他相关的几何计算中。
更多学习内容,可关注公众号:
以上内容为个人测试过程的记录,供大家参考。
内容如有错欢迎批评指正,谢谢!!!!