LeetCode 中等难度题目「149. 直线上最多的点数」,这道题核心考察对"直线斜率"的理解和哈希表的运用,看似简单但细节超多,一不小心就会踩坑。下面结合完整代码,一步步讲透解题逻辑,新手也能轻松看懂。
题目回顾
题目很直白:给你一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。
举个例子:如果 points = [[1,1],[2,2],[3,3]],那么这三个点在同一条直线上,答案就是 3;如果 points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]],答案则是 4(有4个点共线)。
核心难点:如何表示"同一条直线"?如何避免重复计数?如何处理斜率的精度问题?
解题核心思路
直线的核心特征是「斜率」------ 同一平面内,两点确定一条直线,而斜率相同(且经过同一点)的点,必然在同一条直线上。
基于这个原理,我们可以用「固定一点,遍历其他点」的思路,具体步骤如下:
-
边界处理:如果点的数量 ≤ 2,直接返回点的数量(因为两点必然共线);
-
遍历每个点 points[i],将其作为「基准点」;
-
计算基准点与其他所有点 points[j](j > i)的斜率,用哈希表记录「斜率对应的点的数量」;
-
统计当前基准点对应的最大共线点数,更新全局最大值;
-
优化剪枝:如果当前全局最大值已经 ≥ 剩余未遍历的点的数量,或者超过总点数的一半,直接终止循环(无需继续计算,因为不可能出现更大值)。
关键细节:斜率的表示(避坑重点)
这道题最容易踩坑的地方,就是「斜率的表示」。直接用 dy/dx (即两点纵坐标差除以横坐标差)会有两个问题:
-
精度问题:浮点数计算会有误差(比如 1/3 和 2/6 本是同一个斜率,但浮点数表示可能不同);
-
特殊情况:垂直直线(dx=0,斜率不存在)、水平直线(dy=0,斜率为0),无法用常规除法表示。
解决方案:用「最简整数比」表示斜率,将 dy 和 dx 化简为互质的整数,再用一个唯一的key表示这个比值。
具体做法(对应代码中的gcd函数和key计算):
-
计算两点的横坐标差 dx = x_i - x_j,纵坐标差 dy = y_i - y_j;
-
特殊处理:
-
dx=0(垂直直线):令 dy=1(统一表示所有垂直直线的斜率);
-
dy=0(水平直线):令 dx=1(统一表示所有水平直线的斜率);
-
-
符号统一:如果 dy 为负,将 dx 和 dy 同时取反(保证斜率的符号一致,比如 2/-3 和 -2/3 是同一个斜率,统一为 2/3);
-
化简:用最大公约数(gcd)将 dx 和 dy 化简为互质的整数(比如 dx=4,dy=2,化简为 dx=2,dy=1);
-
生成key:将二维的 (dy, dx) 转化为一维key,避免哈希表的key冲突。代码中用「dy + dx * 20001」,因为题目中坐标的范围是 [-10^4, 10^4],dx的最大绝对值是 20000,乘以20001后,再加上dy(范围 [-20000, 20000]),可以保证每个 (dy, dx) 对应唯一的key。
完整代码+逐行解析
先贴完整代码(TypeScript版本),再逐行拆解核心逻辑:
typescript
function maxPoints(points: number[][]): number {
const n = points.length;
if (n <= 2) return n; // 边界处理:2个及以下点必共线
let res = 0;
// 最大公约数函数:用于化简dx和dy
const gcd = (a: number, b: number): number => {
return b != 0 ? gcd(b, a % b) : a;
}
// 遍历每个点作为基准点i
for (let i = 0; i < n; i++) {
// 剪枝:如果当前最大结果已经≥剩余点数量,或超过总点数的一半,无需继续
if (res >= n - i || res > n / 2) {
break;
}
const map = new Map(); // 记录当前基准点下,斜率对应的点的数量
// 遍历所有在i之后的点j(避免重复计算,因为i和j与j和i的斜率相同)
for (let j = i + 1; j < n; j++) {
let dx = points[i][0] - points[j][0];
let dy = points[i][1] - points[j][1];
// 特殊处理:垂直/水平直线,统一斜率表示
if (dx === 0) {
dy = 1; // 垂直直线,斜率统一用(1,0)表示
} else if (dy === 0) {
dx = 1; // 水平直线,斜率统一用(0,1)表示
} else {
// 符号统一:dy为负时,dx和dy同时取反
if (dy < 0) {
dx = -dx;
dy = -dy;
}
// 化简dx和dy为互质整数
const gcdXY = gcd(Math.abs(dx), Math.abs(dy));
dx /= gcdXY;
dy /= gcdXY;
}
// 生成唯一key,存入哈希表
const key = dy + dx * 20001;
map.set(key, (map.get(key) || 0) + 1);
}
// 统计当前基准点下,最多的共线点数(map的值是"与基准点共线的点的数量",需+1包含基准点本身)
let maxn = 0;
for (const num of map.values()) {
maxn = Math.max(maxn, num + 1);
}
// 更新全局最大值
res = Math.max(res, maxn);
}
return res;
};
逐行解析核心代码
-
边界处理:
if (n <= 2) return n;------ 这是最基础的优化,因为1个点返回1,2个点返回2,都无需后续计算。 -
gcd函数:求两个数的最大公约数,用于化简dx和dy。比如gcd(4,2)=2,gcd(3,5)=1,核心是递归实现"辗转相除法"。
-
外层循环(基准点遍历):
for (let i = 0; i < n; i++),每个i作为基准点,后续只遍历j > i的点,避免重复计算(比如i=0、j=1和i=1、j=0是同一个斜率,无需重复统计)。 -
剪枝逻辑:
if (res >= n - i || res > n / 2) break;------ 比如总共有5个点,当前res=3,剩余未遍历的点只有2个(n-i=5-3=2),不可能超过3,直接终止循环;另外,最多共线点数不可能超过总点数的一半(如果超过,早就在之前的基准点中统计到了),这一步能大幅提升效率。 -
哈希表map:key是斜率的唯一标识,value是"与基准点i共线且在i之后的点的数量"。
-
内层循环(计算斜率):
for (let j = i + 1; j < n; j++),计算基准点i和点j的dx和dy,然后进行化简和符号统一,生成key存入map。 -
统计当前基准点的最大共线点数:
num + 1是因为map的value是"除基准点外的共线点数",加上基准点本身才是总共线点数。 -
更新全局最大值res:每次遍历完一个基准点,就用当前的maxn更新res,最终res就是答案。
常见坑点&优化建议
坑点1:斜率精度问题
千万不要用 dy/dx 计算斜率(比如用浮点数存储),会出现精度误差。比如 dx=1、dy=3 和 dx=2、dy=6,斜率都是1/3,但浮点数表示可能有微小差异,导致哈希表认为是两个不同的斜率。
坑点2:符号不统一
比如 dx=2、dy=-3 和 dx=-2、dy=3,其实是同一个斜率,但如果不统一符号,会生成两个不同的key。所以代码中才会判断"如果dy<0,dx和dy同时取反",保证斜率符号一致。
坑点3:重复计算
如果内层循环遍历所有j(j从0到n-1,j≠i),会导致i和j、j和i重复计算,浪费时间。所以内层循环只遍历j > i的点,既避免重复,又提升效率。
优化建议
剪枝逻辑一定要加!尤其是当n较大时(比如n=1000),剪枝能大幅减少循环次数,避免超时。另外,哈希表的key生成方式可以灵活调整,只要能保证"不同斜率对应不同key,相同斜率对应相同key"即可,代码中的「dy + dx * 20001」是结合题目坐标范围的最优选择。
测试用例验证
我们用两个典型测试用例验证代码:
-
测试用例1:points = [[1,1],[2,2],[3,3]]
-
i=0(基准点[1,1]),j=1:dx=-1,dy=-1 → 符号统一后dx=1,dy=1 → key=1+1*20001=20002,map={20002:1};
-
j=2:dx=-2,dy=-2 → 化简后dx=1,dy=1 → key=20002,map={20002:2};
-
maxn=2+1=3,res=3;后续循环剪枝,最终返回3。
-
-
测试用例2:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
-
i=0(基准点[1,1]),遍历j=1~5,计算各个斜率,最终map中最大value为3(对应4个点共线),maxn=4,res=4;
-
后续循环无法超过4,最终返回4。
-
总结
这道题的核心是「用最简整数比表示斜率」,避免精度和符号问题,再通过「固定基准点+哈希表计数」的思路,统计每个基准点对应的最大共线点数,最后结合剪枝优化提升效率。
整体难度中等,重点在于细节处理------斜率的化简、符号统一、key的生成,这些都是避坑的关键。理解之后会发现,这道题本质是"哈希表的应用+直线斜率的数学理解",掌握后可以举一反三,应对类似的几何计数问题。