1. 问题背景与核心挑战
给定一个二维平面上的点集,如何找出位于同一条直线上点的最大数量?这是LeetCode第149题"直线上最多的点数"要解决的核心问题。乍看简单,实则暗藏玄机。
我在第一次遇到这个问题时,以为只需要计算两两点之间的斜率就能轻松解决。但实际编码时发现,浮点数精度问题、垂直线特殊情况、重复点处理等细节会让代码迅速变得复杂。经过多次调试和优化,最终总结出一套可靠的C语言解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础解法思路分析
2.1 暴力枚举法
最直观的解法是三重循环:对于每个点,计算它与其他所有点组成的直线,然后统计每条直线上的点数。这种方法时间复杂度高达O(n³),在LeetCode上会直接超时。
c复制// 伪代码示例(实际不可行)
int maxPoints = 0;
for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
int count = 2;
for (int k = j+1; k < n; k++) {
if (isCollinear(points[i], points[j], points[k])) {
count++;
}
}
maxPoints = max(maxPoints, count);
}
}
2.2 斜率统计优化
更聪明的做法是:对于每个点,计算它与其他所有点的斜率,用哈希表统计相同斜率的频次。这样可以将时间复杂度降到O(n²)。
关键观察点:
- 同一个点会被多次计算,需要特殊处理
- 垂直线(斜率无穷大)需要单独处理
- 浮点数精度问题可能导致相同的斜率被误判为不同
3. C语言实现详解
3.1 数据结构定义
c复制typedef struct {
int x;
int y;
} Point;
// 简化分数表示(避免浮点精度问题)
typedef struct {
int dx;
int dy;
} Fraction;
3.2 斜率计算函数
c复制Fraction getSlope(Point p1, Point p2) {
int dx = p2.x - p1.x;
int dy = p2.y - p1.y;
// 处理垂直线
if (dx == 0) return (Fraction){0, 1};
// 处理水平线
if (dy == 0) return (Fraction){1, 0};
// 计算最大公约数
int gcd_val = gcd(dx, dy);
// 返回最简分数形式
return (Fraction){dx/gcd_val, dy/gcd_val};
}
3.3 最大公约数实现
c复制int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
4. 核心算法实现
4.1 主函数逻辑
c复制int maxPoints(Point* points, int pointsSize) {
if (pointsSize <= 2) return pointsSize;
int max_count = 0;
for (int i = 0; i < pointsSize; i++) {
// 用于统计相同斜率的点数
Fraction* slopes = malloc((pointsSize - 1) * sizeof(Fraction));
int slope_count = 0;
int same_point = 0;
int current_max = 0;
for (int j = 0; j < pointsSize; j++) {
if (i == j) continue;
if (points[i].x == points[j].x && points[i].y == points[j].y) {
same_point++;
continue;
}
slopes[slope_count++] = getSlope(points[i], points[j]);
}
// 统计斜率频次
qsort(slopes, slope_count, sizeof(Fraction), compareFractions);
if (slope_count > 0) {
int count = 1;
for (int k = 1; k < slope_count; k++) {
if (slopes[k].dx == slopes[k-1].dx &&
slopes[k].dy == slopes[k-1].dy) {
count++;
} else {
current_max = current_max > count ? current_max : count;
count = 1;
}
}
current_max = current_max > count ? current_max : count;
}
// 加上当前点本身和重复点
current_max = current_max + 1 + same_point;
max_count = max_count > current_max ? max_count : current_max;
free(slopes);
}
return max_count;
}
4.2 分数比较函数
c复制int compareFractions(const void* a, const void* b) {
Fraction* fa = (Fraction*)a;
Fraction* fb = (Fraction*)b;
if (fa->dx == fb->dx && fa->dy == fb->dy) return 0;
if (fa->dx * fb->dy < fb->dx * fa->dy) return -1;
return 1;
}
5. 关键问题与优化技巧
5.1 浮点数精度问题
直接使用浮点数存储斜率会导致精度问题:
c复制double slope = (double)(p2.y - p1.y) / (p2.x - p1.x); // 不推荐
解决方案:
- 使用分数表示法(分子分母约分后存储)
- 比较斜率时使用交叉相乘避免除法
5.2 重复点处理
当输入中包含多个相同坐标的点时:
- 这些点自然在同一条直线上
- 需要在统计时特殊处理
5.3 垂直线特殊情况
当两点x坐标相同时:
- 斜率为无穷大
- 需要单独表示为(0,1)或其他标记值
5.4 哈希表优化
上述实现使用了排序+遍历统计频次,更高效的做法是使用哈希表:
c复制// 伪代码示例(需要实现哈希函数)
HashMap* map = createHashMap();
for (每个点) {
Fraction slope = getSlope(p1, p2);
int count = getFromHashMap(map, slope);
putToHashMap(map, slope, count + 1);
}
6. 复杂度分析
-
时间复杂度:O(n²)
- 外层循环:O(n)
- 内层循环:O(n)
- 斜率统计:O(n log n)(排序)
-
空间复杂度:O(n)
- 存储斜率需要O(n)空间
7. 测试用例设计
验证算法正确性的关键测试场景:
- 常规情况:
c复制[(1,1),(2,2),(3,3)] → 3
- 包含重复点:
c复制[(1,1),(1,1),(2,2)] → 3
- 垂直线:
c复制[(1,1),(1,2),(1,3)] → 3
- 混合情况:
c复制[(1,1),(2,2),(3,3),(0,0),(1,2)] → 4
- 所有点相同:
c复制[(0,0),(0,0),(0,0)] → 3
8. 实际编码中的坑
-
内存管理:
- 每次外层循环都需要重新分配斜率数组
- 记得释放内存避免泄漏
-
边界条件:
- 点数≤2时直接返回
- 所有点相同时的特殊处理
-
比较函数实现:
- 分数比较需要正确处理符号
- 避免整数溢出(使用long long)
-
斜率哈希:
- 自定义分数结构体的哈希函数需要保证:
- 相同斜率产生相同哈希值
- 不同斜率尽量产生不同哈希值
- 自定义分数结构体的哈希函数需要保证:
9. 性能优化进阶
对于大规模点集(虽然LeetCode不需要):
-
随机采样:
- 随机选择部分点计算,近似最大共线点数
-
空间分割:
- 将平面划分为网格,先粗粒度筛选可能共线的点集
-
并行计算:
- 不同起始点的斜率统计可以并行处理
10. 算法扩展思考
这个问题可以延伸到:
- 三维空间中的共面点检测
- 图像处理中的直线检测(Hough变换)
- 计算几何中的共线性检测
在实际工程中,类似的思路可用于:
- 图像中边缘检测
- 点云数据处理
- 轨迹分析中的异常检测
