深蓝黑背景的品牌封面:中央大标题「50 万个点中找出所有最大点」,副标题「排序 + 单调扫描 · O(n log n)」,左上标注栏目「AI 工程面试题解专栏」,右上标注「面试 · Day 05」,背景为散点与帕累托前沿折线装饰 图 1:本文封面,主题为 50 万点最大点问题(经典题解析)。

想象面试后半程,面试官在屏幕上敲出一道题:平面上给 N 个点,如果不存在另一个点的横、纵坐标都严格比它大,就称它为「最大点」。请输出所有最大点,按横坐标从小到大排列。数据范围:N 最大 50 万,坐标是 0 到 10 亿之间的整数且互不重复,时间限制 1 秒。

第一步:审题,先算账再动手

先把样例看懂。题面给了 5 个点:(1,2)、(5,3)、(4,6)、(7,5)、(9,0),答案是后三个。逐个验证:(4,6) 右边 x 更大的点只有 (7,5) 和 (9,0),它们的 y 分别是 5 和 0,都压不过 6,入选;(5,3) 右边有 (4,6)……等一下,(4,6) 的 x 是 4,比 5 小,不算右边;真正的威胁是 (7,5),x 和 y 都更大,所以 (5,3) 被支配,落选。动手把样例推一遍,支配关系的方向就再也不会搞反。

再算规模的账。这道题真正的考点藏在两个数字里:50 万和 1 秒。最直觉的解法是双重循环——每个点都跟其他点比一遍,看有没有人支配它。50 万乘 50 万是 2500 亿次比较,任何语言都不可能在 1 秒内跑完。审题阶段就要下结论:O(n²) 出局,必须找到 O(n log n) 级别的做法。能做到这一点的通用武器,通常先想到排序。

第二步:基础回答,把定义翻译成扫描

支配的定义是「存在某个点,x 和 y 都更大」。换个方向看:一个点被支配,当且仅当它右边存在 y 更大的点。于是解法成型——把所有点按 x 从大到小排序,从 x 最大的点开始往左扫,过程中只维护一个值:已扫过的最大 y。当前点的 y 如果超过这个历史最大,说明右边没有点能压过它,入选;否则它被支配,跳过。因为题面保证横坐标互不重复,排序后「先扫的点 x 严格更大」天然成立,支配定义里的「严格大于」不需要额外处理。排序 O(n log n),扫描 O(n),整体 O(n log n)。

核心逻辑只有几行(纯算法片段,只用于解释算法步骤,完整可运行版见同主题 B 站专栏版):

// points 已按 x 降序排列;maxY 记录右侧历史最大 y
const picked = [];
let maxY = -Infinity;
for (const p of points) {
  if (p.y > maxY) { picked.push(p); maxY = p.y; }
}
picked.reverse(); // 恢复题面要求的 x 升序

算法图:左侧二维坐标系中,灰色点为被支配点,青色与紫色点构成「最大点」前沿折线——不存在其它点同时 x、y 都不小于它;右侧为三步解法——按 x 降序排序、扫描时维护历史最大 y、y 超过历史最大即入选;底部对比暴力 O(n²) 与排序扫描 O(n log n) 在 50 万规模下的差距 图 2:左图为最大点前沿示意,右图为排序、扫描、判定三步解法。

第三步:可能的追问

追问一:为什么只记最大 y 就够了?排序后所有已扫描点的 x 都严格更大,它们之中任何一个 y 超过当前点就构成支配,而最大 y 是这批点的代表。

追问二:坐标有重复怎么办?题面保证横、纵坐标都不重复,所以不用处理。如果去掉这个保证,注意支配要求严格大于——x 相同不构成支配,排序时要用 y 做第二关键字并处理并列。

追问三:输出顺序怎么满足?扫描得到的是 x 降序,题面要升序,收集完反转一次即可,O(n)。

追问四:空间够吗?50 万个点,每个点两个整数,加上排序开销也在几十兆以内,原题 32MB 的空间限制下放得下——用紧凑的平行数组存坐标,比给每个点挂一个对象更省内存,这在 50 万规模下是能感知的差别。

第四步:工程应用与 AI 关联

这道题的本质是求二维帕累托前沿(Pareto frontier):把「被支配的选项」全部扔掉,留下的集合在 x 和 y 两个目标上互不压倒。AI 工程里处处是这个结构——多目标模型选型时在准确率和推理成本之间找前沿,特征筛选时扔掉被全面压倒的候选特征,推荐系统给候选集去冗。日常开发中遇到「多指标下挑候选」的需求,先问一句「这是不是帕累托问题」,往往就能省掉一套复杂的加权打分。而 50 万这个数据规模本身也是考点:处理大规模样本和候选集时,先算复杂度再写代码,是 AI 工程的基本功。

四个易错点

一是排序方向写反,从左往右扫结果全错;二是忘了把结果反转回升序;三是把定义里的「严格大于」写成「大于等于」,并列坐标时被误判;四是心存侥幸先交暴力解,50 万数据直接超时。

答题节奏建议

先口头上把暴力解和它的复杂度摆出来,再给出排序加扫描的优化,主动说出 O(n log n) 和空间占用,最后动手写代码。写完别急着交:用题面样例走一遍,再补两个自己构造的边界用例——全递增、全递减各一组。这个顺序展示的是思维过程和验证习惯,而不只是背过的答案。

如果题目升到三维——支配要求三个坐标都更大,排序加扫描还能直接用吗?你会怎么改造?欢迎讨论。

参考资料

  • Node.js v22.x 官方 API 文档(代码运行环境说明,本文程序在 Node.js 22.15.0 下实测通过) — https://nodejs.org/docs/latest-v22.x/api/