LeetCode 149: Max Points on a Line - 解题思路详解
题目描述
给定一个二维平面上的点数组 points,其中 points[i] = [xi, yi] 代表一个点的坐标,返回在同一条直线上的最多点数 。 leetcode
示例
示例 1:
输入:points = [[1,1],[2,2],[3,3]]
输出:3
示例 2:
输入:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
输出:4
约束条件
1 <= points.length <= 300points[i].length == 2-10^4 <= xi, yi <= 10^4- 所有点都是唯一的 leetcode
难度与标签
- 难度: Hard
- 通过率: 30.6%
- 标签: Array, Hash Table, Math, Geometry leetcode
解题思路
核心观察
-
如何判断三个点共线?
- 数学上:两个点的斜率 = (y2 - y1) / (x2 - x1)
- 斜率相同的点在同一条直线上 leetcode
-
基本思路
- 固定一个点作为"基准点"
- 计算其他所有点与该基准点的斜率
- 统计相同斜率的点的数量
- 对所有基准点重复此过程,取最大值 leetcode
-
时间复杂度
- O(n²):外层遍历每个点作为基准,内层遍历其他点
- 在点数最多 300 的约束下可以接受 leetcode
关键问题与解决方案
问题 1:如何避免浮点数精度问题?
问题: 直接计算斜率会有精度误差 leetcode
c
// ❌ 错误做法
double slope1 = 4.0 / 2.0; // 2.0
double slope2 = 6.0 / 3.0; // 2.0
// 可能因为精度问题被判定为不同
解决方案: 使用最简分数形式(dy, dx)表示斜率 leetcode
c
// ✅ 正确做法
// 斜率 4/2 → 约分为 (2, 1)
// 斜率 6/3 → 约分为 (2, 1)
// 用元组 (dy, dx) 作为哈希表的 key
问题 2:如何规范化斜率?
需要解决两个子问题 : leetcode
2.1 约分到最简分数
使用 GCD(最大公约数)
c
dy = y2 - y1;
dx = x2 - x1;
int g = gcd(abs(dy), abs(dx));
dy = dy / g;
dx = dx / g;
例子:
- dy = 4, dx = 2 → gcd(4, 2) = 2 → 约分后 (2, 1)
- dy = 6, dx = 3 → gcd(6, 3) = 3 → 约分后 (2, 1)
- 两者现在是同一个 key ✓ leetcode
2.2 统一符号
规则:dx 始终为正数 leetcode
c
// 如果 dx < 0,将 dy 和 dx 都取反
if (dx < 0) {
dy = -dy;
dx = -dx;
}
// 如果 dx = 0(垂直线),规定 dy 为正数
if (dx == 0 && dy < 0) {
dy = -dy;
}
例子:
(2, -3)→ 取反 →(-2, 3)(-2, 3)→ 保持不变 →(-2, 3)- 现在统一了 ✓ leetcode
问题 3:特殊情况处理
垂直线(x 坐标相同)
- dx = 0,斜率不存在(无穷大)
- 约分后表示为
(1, 0)或保持(dy/gcd, 0)leetcode
水平线(y 坐标相同)
- dy = 0,斜率为 0
- 约分后表示为
(0, 1)或保持(0, dx/gcd)leetcode
边界情况
c
if (pointsSize <= 2) return pointsSize;
GCD(最大公约数)详解
什么是 GCD?
定义: 两个数的最大公约数是能同时整除这两个数的最大正整数 。 leetcode
例子:
- gcd(12, 8) = 4
- gcd(15, 10) = 5
- gcd(7, 3) = 1(互质)
辗转相除法(欧几里得算法)
原理: gcd(a, b) = gcd(b, a % b) leetcode
执行过程示例:
gcd(48, 18)
= gcd(18, 48%18)
= gcd(18, 12)
= gcd(12, 18%12)
= gcd(12, 6)
= gcd(6, 12%6)
= gcd(6, 0)
= 6 ← 当 b=0 时,返回 a
为什么 b=0 时返回 a?
- gcd(a, 0) = a(数学性质)
- 0 可以被任何数整除
- 所以 a 和 0 的最大公约数就是 a 本身 leetcode
代码实现
递归版本:
c
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
迭代版本(推荐):
c
int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
数据结构选择
C 语言中的哈希表实现
C 语言没有内置哈希表,有以下几种选择 : troydhanson.github
方案 1:使用 uthash 库(推荐)
LeetCode 的 C 环境支持 uthash,可以直接使用 : reddit
c
#include "uthash.h"
typedef struct {
int dy; // key 的第一部分
int dx; // key 的第二部分
int count; // 这个斜率出现的次数
UT_hash_handle hh; // 必须包含
} SlopeHash;
关键操作:
添加/更新:
c
typedef struct {
int dy;
int dx;
} SlopeKey;
SlopeKey key = {dy, dx};
SlopeHash *item;
HASH_FIND(hh, hash_table, &key, sizeof(SlopeKey), item);
if (item == NULL) {
item = malloc(sizeof(SlopeHash));
item->dy = dy;
item->dx = dx;
item->count = 1;
HASH_ADD(hh, hash_table, dy, sizeof(int)*2, item);
} else {
item->count++;
}
清理:
c
SlopeHash *current, *tmp;
HASH_ITER(hh, hash_table, current, tmp) {
HASH_DEL(hash_table, current);
free(current);
}
注意事项:
HASH_ADD的第三个参数是 key 的起始字段名sizeof(int)*2表示 key 的总大小(dy + dx)- dy 和 dx 必须在结构体中连续排列 troydhanson.github
方案 2:数组 + 线性查找
c
struct Slope {
int dy;
int dx;
int count;
};
// 对于每个基准点,创建 Slope 数组
// 线性查找匹配的斜率并更新计数
特点:
- 实现简单,容易理解
- 每个基准点最多 300 个其他点,线性查找可接受
- 不需要额外的库 leetcode
完整算法流程
基础版本(遍历所有点对)
if pointsSize <= 2:
return pointsSize
max_result = 0
for 每个点 i 作为基准点 (0 到 n-1):
创建新的哈希表 slope_count
for 每个其他点 j (0 到 n-1, j != i):
计算 dy = points[j] [leetcode](https://leetcode.com/problems/max-points-on-a-line/submissions/1991436334/?envType=study-plan-v2&envId=top-interview-150) - points[i] [leetcode](https://leetcode.com/problems/max-points-on-a-line/submissions/1991436334/?envType=study-plan-v2&envId=top-interview-150)
计算 dx = points[j][0] - points[i][0]
计算 g = gcd(abs(dy), abs(dx))
dy = dy / g
dx = dx / g
统一符号(dx 为正)
slope_count[(dy, dx)] += 1
找到 slope_count 中的最大值 max_count
当前最大值 = max_count + 1 // 加上基准点自己
更新 max_result
清理哈希表
return max_result
优化版本(只遍历后续点)
for 每个点 i (0 到 n-1):
创建新哈希表
for 每个点 j (i+1 到 n-1): // 只看后面的点
计算斜率并统计
更新最大值
清理哈希表
优化说明:
- 点 i 和点 j 的关系在遍历点 j 时已经计算过
- 避免重复计算,减少常数时间
- 时间复杂度仍然是 O(n²) leetcode
关键理解点
为什么每个基准点需要独立的哈希表?
错误理解: 用一个全局哈希表统计所有斜率 leetcode
问题:
hash[(1,1)] = 4 // 这个 4 没有意义!
这 4 次出现可能来自:
- 2 次是 A→B 和 A→C(同一条直线)
- 2 次是 B→A 和 B→C(同一条直线)
- 它们实际上是同一条直线,不能简单相加 leetcode
正确理解:
- 每个基准点的哈希表只记录"其他点相对于这个基准点的斜率"
- hash_A 记录:有多少点与 A 形成斜率 (1,1)
- hash_B 记录:有多少点与 B 形成斜率 (1,1)
- 它们是完全不同的统计维度 leetcode
为什么最后要 +1?
c
当前基准点的最大共线点数 = max_count + 1
因为 slope_count 只统计了与基准点共线的其他点 的数量,没有包括基准点自己 。 leetcode
例子:
- 点 A, B, C 三点共线
- 以 A 为基准点时,slope_count 统计到 B 和 C,值为 2
- 但实际共线的点数是 3(A + B + C)
具体例子演示
输入:points = [, ] leetcode
第 1 轮:以点 (1,1) 为基准
| 目标点 | dy | dx | gcd | 约分后 | 计数 |
|---|---|---|---|---|---|
| (2,2) | 1 | 1 | 1 | (1,1) | 1 |
| (3,3) | 2 | 2 | 2 | (1,1) | 2 |
| (1,4) | 3 | 0 | 3 | (1,0) | 1 |
哈希表:{(1,1): 2, (1,0): 1}
最大值:2 + 1 = 3 个点 leetcode
第 2 轮:以点 (2,2) 为基准
| 目标点 | dy | dx | gcd | 约分后 | 计数 |
|---|---|---|---|---|---|
| (3,3) | 1 | 1 | 1 | (1,1) | 1 |
| (1,4) | 2 | -1 | 1 | (-2,1) | 1 |
哈希表:{(1,1): 1, (-2,1): 1}
最大值:1 + 1 = 2 个点 leetcode
全局最大值:3
面试建议
思路展示顺序
- 暴力思路: 枚举所有三点组合,检查是否共线 - O(n³)
- 优化思路: 固定一个点,用哈希表统计斜率 - O(n²)
- 细节处理:
- 浮点数精度问题 → GCD 约分
- 符号统一
- 特殊情况(垂直线、水平线) leetcode
代码实现选择
如果熟悉 uthash:
- 优先使用,展示专业性
- 代码简洁,逻辑清晰 reddit
如果不熟悉:
- 先说明知道可以用哈希表优化
- 使用数组 + 线性查找实现
- 保证正确性优先 leetcode
时间空间复杂度
-
时间复杂度: O(n²)
- 外层循环 n 次
- 内层循环 n 次
- GCD 计算 O(log(max(dy, dx))),可忽略 leetcode
-
空间复杂度: O(n)
- 每个基准点的哈希表最多存储 n-1 个斜率
总结
这道题的核心挑战在于 : leetcode
- ✅ 算法设计: 用斜率统计共线点
- ✅ 精度处理: 避免浮点数误差(GCD 约分)
- ✅ 数据结构: 哈希表的正确使用(每个基准点独立)
- ✅ 边界情况: 垂直线、符号统一、点数 ≤ 2
掌握这些要点,就能优雅地解决这道 Hard 难度的几何问题!