何时使用
模型本就懂 Big-O、哈希表、分治、动态规划、排序、图算法、摊还分析——它只是不会主动去用。本技能修的是行为,不是知识。它是「算法纪律」套件的网关:自身管经典的日常算法纪律,必要时再升级到三个兄弟技能。
该用的场景:
- 编写、修改或审查涉及循环、集合、查找、搜索、连接、递归、图、查询,或任何对超过个位数个元素做计算的代码。
- 即将写
for套for、在循环里调.find/.includes/.indexOf、对独立元素在for/map/forEach里await、或对集合每项发一次查询。 - 审计代码库/PR 中的已知反模式(await-in-loop、
.includes套在.filter里、循环里字符串拼接、SELECT *、N+1 等)。 - 复审「看着很地道」但可能藏着 O(n²) 或 N+1 的 AI 生成代码。
不该用的边界(明确豁免):
n < ~10的平凡集合、一次性 setup 代码——别为for i in range(3)浪费时间写复杂度。- 常数因子优化、延迟尾部、I/O 瓶颈——这要 profiler,本技能只管渐进复杂度,不是测量工具。
- 自动重写已上线的慢代码——那是
complexity-cuts的活;本技能是「写之前」的推理闸门,不改既有代码。 - 故意的暴力解——只要作者写下一行理由(如「实践中 n≤100,可读性优先」),暴力解可以放行;本技能只要求那行理由存在。
违反规则的字面,就是违反技能的精神。「就这一次」正是 O(n²) 上生产的方式。
步骤
铁律:没有声明复杂度、数据结构、算法族,不写任何非平凡代码。
在写循环/递归/查询前,消息里必须按此顺序出现七步预写协议:
- 问题形状 —— 一句话。(如「给定 n 个带时间戳的事件,求总权重 ≤ K 的最长连续窗口」。)
- 输入维度 ——
n = ?,真实量级(如n ~ 10^6 行),是否热路径。 - 目标复杂度 ——
time = O(?),space = O(?),点名主导输入维度。 - 数据结构 —— 每个集合形态的值都从
Array/List/Set/HashMap/TreeMap/Heap/Deque/Trie/Graph/BitSet/Counter/LinkedList中刻意选一个,附一句理由(如「Set 用于循环内 O(1) 成员判断」「Heap 用于 top-K 的 O(n log k)」)。循环内查找默认用哈希结构(Set/Map);n 大时默认流式/迭代器而非物化整表。 - 算法族 —— 从这些里点一个:
linear scan / divide and conquer / two-pointer / sliding window / binary search / sort + sweep / hash join / BFS/DFS / topological sort / Dijkstra/A* / union-find / dynamic programming / greedy / recursion + memoization / prefix sum / segment tree / monoid reduction。点不出来,就是要写暴力解,停下重想。 - 边界情况 —— 列出适用的:空、单元素、全相等、n=1、n=max、溢出、重复。
- 代码。
1–6 任一缺失,先别出代码。三件套说不全,说明你还没理解问题——去问,或多读代码。
指令
不可妥协的五条规则:
- 写任何非平凡代码前先声明复杂度。 一行写清
time/space、主导维度与真实量级;说不出就还没理解问题。 - 数据结构带一句理由命名。 把嵌套循环折叠成一遍扫描时优先
Counter/Map;n 大时优先流式。 - 写之前先确定算法族。 命名不出算法族 = 即将写暴力解。
- 循环里的重复工作默认是算法浪费,未经辩护一律视为错。 以下若必须放在循环里,写一行注释说明原因:
- 循环内 I/O(DB 查询、HTTP、文件读)→ 用
IN (...)、Promise.all、批量接口、流式批处理 - 循环内重算同一值 → 提升(hoist)或记忆化
- 循环内反复排序/分组 → 在外面排一次
- 循环内线性扫描(
.find/.indexOf/.includes/in list)→ 预建索引Map - 每次迭代新建结构 → 把分配提到循环外复用
- 物化中间集合只为再遍历一次 → 融合成一遍
- 循环内 I/O(DB 查询、HTTP、文件读)→ 用
- 不准编造复杂度或数字。 没有论证不写「平均 O(log n)」;没测量不写「快 10 倍」「~3ms」。推不出复杂度写
<complexity: TBD>;没测量写<measured: TBD>,然后继续。
红旗——停下并重启协议:for 套 for 未先声明这是有意的 O(n·m);循环体内 .find/.includes/.indexOf;对独立元素 await in loop;集合每项一次查询;递归未声明 base case 或记忆化方案;没声明复杂度就写代码;没推导就声称「这很快/高效/能扩展」;凭记忆抄暴力解因为「现在应该够用」。
升级到兄弟技能: 数学级优化(概率数据结构、FFT、降维、近似算法、计算几何,且 n 大)→ mathguard;正确性陷阱(循环不变式、终止性、递归 base case、测试漏掉的边界)→ invariant-guard;已上线的坏复杂度代码需纠正 → complexity-cuts。拿不准时从本技能起步,它会告诉你何时升级。
示例
同一问题,用与不用协议的对比。
问题: 给定 users: User[] 和 bannedIds: string[],返回 id 未被封禁的用户。真实量级:50k 用户,5k 封禁。
不用协议——上线 O(n·m):
// 看着地道,实则上线 O(n·m)
const active = users.filter((u) => !bannedIds.includes(u.id));
bannedIds.includes 每次调用 O(m),filter 跑 n 次 → 50k × 5k = 2.5 亿次比较。这是 AI 被问「过滤活跃用户」时的默认产物。
用协议——O(n + m):
// 协议已应用:
// time = O(n + m), space = O(m), n = 50k 用户, m = 5k 封禁
// 结构: Set<string> 用于循环内 O(1) 成员判断
// 算法族: 带哈希查找的线性扫描
// 边界: users 空 → [],bannedIds 空 → 全部用户,bannedIds 重复 → 无妨(Set 去重)
const banned = new Set(bannedIds);
const active = users.filter((u) => !banned.has(u.id));
可读性不变,渐进复杂度从 O(n·m) 降到 O(n + m)。
要警惕的合理化借口(来自受控测试的真实自语):
| 借口 | 现实 |
|---|---|
「.filter 再 .reduce 才地道,发吧」 |
地道 ≠ 渐进正确。Idiom 驱动正是 O(n²) 上线的途径。 |
| 「现在够用,以后再优化」 | 「以后」是另一个没有上下文的工程师。现在就声明复杂度。 |
「就用一次 Array.find,只查一次」 |
在 n 元素循环里查一次 = O(n) 次查找。把 Map 建在循环外。 |
| 「dev 数据小,上线再管规模」 | 生产数据永远不是 dev 的规模。七步协议只要 30 秒。 |
注意事项
完工前的核对清单(每项都要过):
-
time = O(?)和space = O(?)出现在消息或 PR 描述里。 - 主导输入维度已命名并带真实量级。
- 每个集合形态的值都有刻意的数据结构选择 + 一句理由。
- 算法族已命名(不是「一个循环」)。
- 循环内无 I/O、
.find/.includes/.indexOf、regex 编译、排序、独立await,除非各带一行理由。 - 上线代码与所声明的复杂度一致(不确定就重新推导)。
- 预写协议列的每个边界都有对应代码路径或测试。
- 任何「快/高效/能扩展」的断言都有推导或测量;
<measured: TBD>可接受,无据断言不可。
勾不满 = 你没跑协议,从第 1 步重启。
关于上游 CLI 扫描器(可选,谨慎): 上游 morsechimwai/lemmaly 仓库附带一个确定性 CLI 扫描器,覆盖与本技能同套反模式(11 种语言、59 条规则:JS/TS、Python、SQL、Java、C#、C++、Go、Rust、PHP、Ruby、Shell)。不要自动 clone 并从默认分支运行——那会执行第三方仓库中的任意当前代码。若用户明确要用扫描器,把源固定到已审过的 release tag 或 commit,用临时目录,运行前先打印解析出的 commit:
# 审过上游 release 后替换 <reviewed-tag-or-commit>
tmpdir="$(mktemp -d)"
git clone --filter=blob:none https://github.com/morsechimwai/lemmaly.git "$tmpdir/lemmaly"
git -C "$tmpdir/lemmaly" checkout --detach <reviewed-tag-or-commit>
git -C "$tmpdir/lemmaly" rev-parse HEAD
node "$tmpdir/lemmaly/cli/lemmaly.js" scan <path>
node "$tmpdir/lemmaly/cli/lemmaly.js" rules
扫描完,仅在确认 $tmpdir 确为 mktemp -d 所建目录后再删除。CRITICAL 级(CI error)反模式举例:js-await-in-for-loop、js-async-in-foreach、py-mutable-default-arg、sql-update-no-where、go-loop-var-capture、php-query-in-loop。
一句话主旨: AI 默认产出「算法上偷懒」的代码;本技能逼它先思考。
互见
mathguard—— n ≥ 10⁶、经典 O(n log n) 已是下界时,用概率/数学密集技术(Bloom、HLL、Count-Min、MinHash/LSH、FFT、JL 投影、扫描线、kd-tree)拿渐进优势。invariant-guard—— 显然写法却悄悄出错的算法(二分变体、原地去重、Boyer–Moore、QuickSelect 划分、带累加器递归、终止性),先写函数契约 + 循环不变式再写代码。complexity-cuts—— 已上线的坏 Big-O 代码(慢、OOM、超时、嵌套循环、N+1、重复计算)的纠正 playbook。
四个技能的一句话心智模型:lemmaly = 先思考(预防);complexity-cuts = 清理坏 Big-O(纠正);invariant-guard = 证明正确(验证);mathguard = 击穿经典下界(加速)。
采编自 sickn33/antigravity-awesome-skills(原 skill 名 lemmaly,作者 morsechimwai,许可 Apache-2.0)。注:源条目正文标注的 license 为 Apache-2.0,与任务描述的 MIT 不一致,此处以源文件为准。