LeetCode 149: Max Points on a Line - 解题思路详解

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 <= 300
  • points[i].length == 2
  • -10^4 <= xi, yi <= 10^4
  • 所有点都是唯一的 leetcode

难度与标签

  • 难度: Hard
  • 通过率: 30.6%
  • 标签: Array, Hash Table, Math, Geometry leetcode

解题思路

核心观察

  1. 如何判断三个点共线?

    • 数学上:两个点的斜率 = (y2 - y1) / (x2 - x1)
    • 斜率相同的点在同一条直线上 leetcode
  2. 基本思路

    • 固定一个点作为"基准点"
    • 计算其他所有点与该基准点的斜率
    • 统计相同斜率的点的数量
    • 对所有基准点重复此过程,取最大值 leetcode
  3. 时间复杂度

    • 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


面试建议

思路展示顺序

  1. 暴力思路: 枚举所有三点组合,检查是否共线 - O(n³)
  2. 优化思路: 固定一个点,用哈希表统计斜率 - O(n²)
  3. 细节处理:
    • 浮点数精度问题 → GCD 约分
    • 符号统一
    • 特殊情况(垂直线、水平线) leetcode

代码实现选择

如果熟悉 uthash:

  • 优先使用,展示专业性
  • 代码简洁,逻辑清晰 reddit

如果不熟悉:

  • 先说明知道可以用哈希表优化
  • 使用数组 + 线性查找实现
  • 保证正确性优先 leetcode

时间空间复杂度

  • 时间复杂度: O(n²)

    • 外层循环 n 次
    • 内层循环 n 次
    • GCD 计算 O(log(max(dy, dx))),可忽略 leetcode
  • 空间复杂度: O(n)

    • 每个基准点的哈希表最多存储 n-1 个斜率

总结

这道题的核心挑战在于 : leetcode

  1. 算法设计: 用斜率统计共线点
  2. 精度处理: 避免浮点数误差(GCD 约分)
  3. 数据结构: 哈希表的正确使用(每个基准点独立)
  4. 边界情况: 垂直线、符号统一、点数 ≤ 2

掌握这些要点,就能优雅地解决这道 Hard 难度的几何问题!

相关推荐
样例过了就是过了1 小时前
LeetCode热题100 最长公共子序列
c++·算法·leetcode·动态规划
HXDGCL1 小时前
矩形环形导轨:自动化循环线的核心运动单元解析
运维·算法·自动化
谭欣辰1 小时前
C++ 排列组合完整指南
开发语言·c++·算法
代码中介商2 小时前
银行管理系统的业务血肉 —— 流程、状态机、输入校验与持久化(下篇)
c语言·算法
foundbug9992 小时前
自适应滤除直达波干扰的MATLAB实现
开发语言·算法·matlab
童园管理札记3 小时前
【续】数字时代:学前教育的新改革
经验分享·深度学习·职场和发展·微信公众平台
CN-Dust4 小时前
【C++】while语句例题专题
数据结构·c++·算法
灵智实验室4 小时前
PX4位置速度估计技术详解(四):LPE 激光雷达高度融合的实现错误
算法·无人机·px 4
CQU_JIAKE4 小时前
【A】3742,3387,并查集
算法