LeetCode 149题要求我们找出二维平面上最多有多少个点位于同一条直线上。给定一个由点组成的数组points,其中points[i] = [xi, yi],我们需要返回位于同一直线上的最大点数。
这个问题看似简单,但实际需要考虑多种边界情况:
解决这个问题的关键在于如何高效准确地判断多个点是否共线。直接思路是三重循环检查所有可能的直线,但时间复杂度会达到O(n³),这在LeetCode上显然无法通过。
更聪明的做法是利用斜率和截距来判断共线性。对于每个点,我们计算它与其他所有点形成的直线斜率,统计相同斜率的出现次数。但这里有几个技术难点需要解决:
为了避免浮点数精度问题,我们不用实际计算斜率值,而是用分数的最简形式来表示斜率。具体来说:
对于垂直直线(Δx=0),我们可以用特殊字符串"vertical"表示。
对于每个点points[i],执行以下步骤:
需要特别注意以下几种边界情况:
首先定义一些辅助数据结构和函数:
c复制typedef struct {
int x;
int y;
} Point;
// 计算最大公约数的函数
int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
c复制int maxPoints(Point* points, int pointsSize) {
if (pointsSize < 3) return pointsSize;
int maxCount = 0;
for (int i = 0; i < pointsSize; i++) {
// 用于统计相同斜率的出现次数
struct HashEntry {
char* key;
int value;
UT_hash_handle hh;
} *hashMap = NULL;
int duplicate = 1; // 记录与当前点重复的点数
for (int j = i + 1; j < pointsSize; j++) {
if (points[i].x == points[j].x && points[i].y == points[j].y) {
duplicate++;
continue;
}
int dx = points[j].x - points[i].x;
int dy = points[j].y - points[i].y;
int g = gcd(dx, dy);
// 构造斜率key
char* key = (char*)malloc(20 * sizeof(char));
sprintf(key, "%d/%d", dy/g, dx/g);
// 更新哈希表
struct HashEntry *entry;
HASH_FIND_STR(hashMap, key, entry);
if (entry) {
entry->value++;
} else {
entry = (struct HashEntry*)malloc(sizeof(struct HashEntry));
entry->key = key;
entry->value = 1;
HASH_ADD_KEYPTR(hh, hashMap, entry->key, strlen(entry->key), entry);
}
}
// 找出当前点的最大共线点数
int currentMax = duplicate;
struct HashEntry *entry, *tmp;
HASH_ITER(hh, hashMap, entry, tmp) {
if (entry->value + duplicate > currentMax) {
currentMax = entry->value + duplicate;
}
HASH_DEL(hashMap, entry);
free(entry->key);
free(entry);
}
if (currentMax > maxCount) {
maxCount = currentMax;
}
}
return maxCount;
}
算法的主要时间消耗在于双重循环和哈希表操作:
因此总体时间复杂度为O(n²),这在LeetCode的约束条件下是可以接受的。
空间复杂度主要来自哈希表存储:
因此空间复杂度为O(n)。
c复制void testMaxPoints() {
// 测试用例1:普通情况
Point points1[] = {{1,1},{2,2},{3,3}};
assert(maxPoints(points1, 3) == 3);
// 测试用例2:包含重复点
Point points2[] = {{1,1},{1,1},{2,2},{3,3}};
assert(maxPoints(points2, 4) == 4);
// 测试用例3:垂直直线
Point points3[] = {{1,1},{1,2},{1,3},{2,2}};
assert(maxPoints(points3, 4) == 3);
// 测试用例4:所有点相同
Point points4[] = {{0,0},{0,0},{0,0}};
assert(maxPoints(points4, 3) == 3);
// 测试用例5:空输入
assert(maxPoints(NULL, 0) == 0);
printf("All test cases passed!\n");
}
特别需要注意以下几种边界条件的测试:
重要提示:直接使用浮点数计算斜率会导致精度问题,从而影响比较结果。例如,计算1/3和2/6的浮点表示可能不同,但实际上它们是相同的斜率。
解决方案:
当使用哈希表统计斜率时,可能会遇到哈希冲突问题。可以通过以下方式优化:
由于算法中动态分配了内存(斜率key和哈希表entry),需要注意内存泄漏问题。可以使用以下方法检查:
如果问题扩展到三维空间,判断共线性的方法类似,但需要考虑三个维度的差值。斜率计算需要处理z坐标,或者使用向量叉积来判断共线性。
更一般的扩展是找出三维空间中共面的最多点数。这可以通过计算平面方程或使用行列式来判断四点共面。
在实际应用中,有时需要找出近似共线的点集。这时可以定义某种距离度量(如点到直线的距离),然后寻找满足阈值条件的最大点集。
虽然这个问题看起来是纯数学问题,但它在许多实际应用中都有价值:
在实现这个算法的过程中,我最初直接使用了浮点数斜率,结果在一些测试用例上失败了。后来改用分数表示法后,问题得到了解决。这让我深刻认识到在算法设计中,数值精度问题的重要性。
另一个教训是关于哈希表的使用。最初我尝试自己实现哈希表,结果遇到了很多边界条件问题。后来改用成熟的uthash库后,代码简洁性和可靠性都大大提升。这告诉我,在时间允许的情况下,使用经过验证的库通常是更明智的选择。
最后,对于这类几何问题,画图真的很有帮助。当我遇到难以理解的测试用例时,把点画在纸上,往往能很快发现问题所在。