题目难度: 中等
今天继续更新程序员面试金典系列, 大家在公众号 算法精选 里回复 面试金典 就能看到该系列当前连载的所有文章了, 记得关注哦~
题目描述
给定一个二维平面及平面上的 N 个点列表 Points,其中第 i 个点的坐标为 Points[i]=[Xi,Yi]。请找出一条直线,其通过的点的数目最多。
设穿过最多点的直线所穿过的全部点编号从小到大排序的列表为 S,你仅需返回[S[0],S[1]]作为答案,若有多条直线穿过了相同数量的点,则选择 S[0]值较小的直线返回,S[0]相同则选择 S[1]值较小的直线返回。
示例:
- 输入: [[0,0],[1,1],[1,0],[2,0]]
- 输出: [0,2]
- 解释: 所求直线穿过的 3 个点的编号为[0,2,3]
提示:
- 2 <= len(Points) <= 300
- len(Points[i]) = 2
题目思考
- 如何优化时间复杂度?
解决方案
思路
- 分析题目, 一个比较容易想到的思路就是暴力模拟, 即双重循环固定前两个点形成一条直线, 然后再遍历后面的其他点, 统计有多少个点在当前直线上, 最后返回穿过最多点的直线即可
- 不过这种做法的时间复杂度达到了 O(N^3), 效率过低, 有没有更高效的做法呢?
- 答案是肯定的, 我们可以只使用一个外层循环来固定起点, 然后内层再遍历后面其他节点, 计算它和起点构成的直线的斜截式
- 与此同时, 我们使用一个字典来记录当前直线穿过的点的数目和第二个点的坐标 (用于更新最终结果), 并使用一个变量存储点数目最大值, 如果当前直线穿过点的数目更多, 就更新该最大值和最终结果
- 这样优化后就降为了两重循环, 即 O(N^2)时间复杂度
- 实现细节方面, 字典的 key 就对应于直线斜截式的斜率 k 和截距 b, 注意当直线平行于 y 轴时斜率不存在, 我们需要单独处理这种特殊情况, 即将 k 设为 None 而 b 是对应 x 的值
- 下面代码就对应了上面的整个过程, 且每一步有详细的注释, 方便大家理解
复杂度
- 时间复杂度
O(N^2)
: 只需要两重循环, 每次遍历 N 个点 - 空间复杂度
O(1)
: 只使用了几个常数空间的变量
代码
class Solution:
def bestLine(self, points: List[List[int]]) -> List[int]:
# 两重循环+计算斜截式+特殊处理平行坐标轴+字典存储第二个下标和点数目
n = len(points)
# mx存储直线经过点的最大数目
mx = 0
# res存储最佳直线的前两个点
res = []
for i in range(n):
# 固定i作为直线的起点
x1, y1 = points[i]
# 字典d的key是斜截式k和b, value是第二个点的下标和点数目
# 存储第二个点的下标的目的是为了更新最终结果
d = {}
for j in range(i + 1, len(points)):
# 注意这里的点从i+1开始遍历, 因为小于i-1的部分在之前的遍历中已经当过起点了, 已经计算过它和i的组合了, 这里不需要重复计算
x2, y2 = points[j]
if x1 == x2:
# 特殊处理平行于y轴的情况
key = (x1, None)
elif y1 == y2:
# 特殊处理平行于x轴的情况
key = (None, y1)
else:
# 一般情况, 可以求出斜截式方程的k和b
key = ((y2 - y1) / (x2 - x1), (y1 * x2 - x1 * y2) / (x2 - x1))
if key not in d:
# 当前直线首次出现, 第二个点的下标是j, 点数目为2
d[key] = [j, 2]
else:
# 当前直线再次出现, 只更新点数目
d[key][1] += 1
if d[key][1] > mx:
# 当前直线经过的点数目更多, 更新mx和res
res = [i, d[key][0]]
mx = d[key][1]
return res
大家可以在下面这些地方找到我~😊
我的公众号: 算法精选, 欢迎大家扫码关注~😊