在计算几何中,凸包(Convex Hull)是一个基础而重要的问题。简单来说,给定平面上的一个点集,凸包就是能够包含所有点的最小凸多边形。就像用橡皮筋套住一组钉子时,橡皮筋最终形成的形状就是这些钉子的凸包。
凸包问题在实际中有广泛的应用场景:
在深入算法之前,我们需要明确几个关键概念:
常见的凸包算法有:
本文实现的是一种基于极角排序的增量算法,虽然时间复杂度为O(n²),但实现简单直观,适合初学者理解凸包的基本原理。
首先定义基本的数据结构:
c复制typedef struct {
int x;
int y;
} Point;
这个简单的结构体存储了点的二维坐标。在实际应用中,可能需要添加更多字段,如点ID、权重等。
c复制int distance(Point p1, Point p2) {
int dx = p1.x - p2.x;
int dy = p1.y - p2.y;
return dx * dx + dy * dy;
}
这里计算的是两点间距离的平方,避免了开方运算,既节省计算量又不会影响比较结果。
c复制int inConvexHull(Point p, Point hull[], int size) {
for (int i = 0; i < size - 1; i++) {
Point p1 = hull[i];
Point p2 = hull[i+1];
// 计算叉积
int cross = (p1.x - p.x) * (p2.y - p.y) - (p1.y - p.y) * (p2.x - p.x);
// 叉积小于0表示点在边外侧
if (cross < 0) return 0;
// 叉积等于0且距离足够近表示点在边上
if (cross == 0 && distance(p1, p2) >= distance(p1, p)) {
return 0;
}
}
return 1;
}
这个函数是算法的核心,它通过计算叉积来判断点与凸包边的位置关系:
c复制void convexHull(Point points[], int n) {
// 1. 找到最左边的点
int leftmost = 0;
for (int i = 1; i < n; i++) {
if (points[i].x < points[leftmost].x ||
(points[i].x == points[leftmost].x && points[i].y < points[leftmost].y)) {
leftmost = i;
}
}
// 2. 初始化凸包
Point hull[n];
int count = 0;
hull[count++] = points[leftmost];
// 3. 主循环
int current = leftmost;
do {
int next = (current + 1) % n; // 初始候选点
// 寻找下一个凸包点
for (int i = 0; i < n; i++) {
if (i == current) continue;
// 计算叉积判断方向
int cross = (points[next].x - points[current].x) * (points[i].y - points[current].y)
- (points[next].y - points[current].y) * (points[i].x - points[current].x);
// 如果i点在current-next的右侧,或者共线但更远
if (cross < 0 ||
(cross == 0 && distance(points[current], points[i]) > distance(points[current], points[next]))) {
next = i;
}
}
hull[count++] = points[next];
current = next;
} while (current != leftmost); // 直到回到起点
// 输出结果
printf("凸包顶点:\n");
for (int i = 0; i < count-1; i++) { // 去掉重复的起点
printf("(%d, %d)\n", hull[i].x, hull[i].y);
}
}
原始实现中省略了极角排序步骤,实际上可以先对所有点按极角排序,这样可以简化后续处理:
c复制// 比较函数:按极角排序
int compare(const void *vp1, const void *vp2) {
Point *p1 = (Point *)vp1;
Point *p2 = (Point *)vp2;
// 计算极角
int angle1 = atan2(p1->y - start.y, p1->x - start.x);
int angle2 = atan2(p2->y - start.y, p2->x - start.x);
if (angle1 != angle2) return angle1 - angle2;
return distance(start, *p1) - distance(start, *p2);
}
// 在convexHull函数中添加:
qsort(points, n, sizeof(Point), compare);
实际应用中经常遇到多点共线的情况,需要特殊处理:
c复制// 修改比较函数处理共线点
int compare(const void *vp1, const void *vp2) {
Point *p1 = (Point *)vp1;
Point *p2 = (Point *)vp2;
int cross = (p1->x - start.x) * (p2->y - start.y)
- (p1->y - start.y) * (p2->x - start.x);
if (cross != 0) return -cross; // 逆时针排序
return distance(start, *p1) - distance(start, *p2);
}
c复制#include <stdio.h>
#include <stdlib.h>
#include <math.h>
typedef struct {
int x;
int y;
} Point;
Point start; // 全局变量,用于排序
int distance(Point p1, Point p2) {
int dx = p1.x - p2.x;
int dy = p1.y - p2.y;
return dx * dx + dy * dy;
}
int compare(const void *vp1, const void *vp2) {
Point *p1 = (Point *)vp1;
Point *p2 = (Point *)vp2;
int cross = (p1->x - start.x) * (p2->y - start.y)
- (p1->y - start.y) * (p2->x - start.x);
if (cross != 0) return -cross;
return distance(start, *p1) - distance(start, *p2);
}
void convexHull(Point points[], int n) {
if (n < 3) {
printf("点数不足,无法形成凸包\n");
return;
}
// 找到最左下点
int leftmost = 0;
for (int i = 1; i < n; i++) {
if (points[i].y < points[leftmost].y ||
(points[i].y == points[leftmost].y && points[i].x < points[leftmost].x)) {
leftmost = i;
}
}
// 交换到数组首位
Point temp = points[0];
points[0] = points[leftmost];
points[leftmost] = temp;
start = points[0]; // 设置全局起点
qsort(points + 1, n - 1, sizeof(Point), compare);
// 初始化栈
Point hull[n];
int top = 0;
hull[top++] = points[0];
hull[top++] = points[1];
// 处理剩余点
for (int i = 2; i < n; i++) {
while (top >= 2) {
Point p1 = hull[top-2];
Point p2 = hull[top-1];
Point p3 = points[i];
int cross = (p2.x - p1.x) * (p3.y - p1.y)
- (p2.y - p1.y) * (p3.x - p1.x);
if (cross <= 0) break; // 非左转则退出
top--; // 弹出栈顶
}
hull[top++] = points[i];
}
// 输出结果
printf("凸包顶点(%d个):\n", top);
for (int i = 0; i < top; i++) {
printf("(%d, %d)\n", hull[i].x, hull[i].y);
}
}
int main() {
Point points[] = {{0, 3}, {1, 1}, {2, 2}, {4, 4},
{0, 0}, {1, 2}, {3, 1}, {3, 3}};
int n = sizeof(points) / sizeof(points[0]);
convexHull(points, n);
return 0;
}
对于给定的测试点集:
code复制(0,3), (1,1), (2,2), (4,4),
(0,0), (1,2), (3,1), (3,3)
程序输出结果应为:
code复制凸包顶点(5个):
(0, 0)
(3, 1)
(4, 4)
(0, 3)
这个结果形成了一个包含所有点的最小凸多边形。
原始实现:O(n²)
优化后(Graham扫描法):O(nlogn)
多点共线处理不当:
浮点精度问题:
起始点选择不当:
可视化调试:
边界测试:
性能分析:
将算法扩展到三维空间,需要考虑:
支持点集的动态更新(插入/删除):
在实际项目中,我经常发现凸包算法虽然原理简单,但边界条件的处理往往需要格外小心。特别是在处理大规模点集时,算法的效率会成为瓶颈。这时候,选择合适的优化策略就变得非常重要。