算法面试不是会不会,而是多快能写对。给你15分钟,一道真实LeetCode题,没有提示、没有调试器、只有白板心态。
本文还原一场解LeetCode的全过程:从读题、破题、写代码到边界检查,完整呈现一个工程师的思维速度与工程落地能力。
一、规则
| 项目 | 规则 |
|---|---|
| 时间 | 15分钟倒计时 |
| 题目 | LeetCode真实中等难度题 |
| 环境 | 纯文本编辑器,无智能提示 |
| 目标 | 写出可运行的、覆盖边界的代码 |
| 评分 | 正确性(60%) + 代码质量(20%) + 完成时间(20%) |
二、题目
给你一个整数数组
nums,判断是否存在三元组[nums[i], nums[j], nums[k]]满足i != j != k,且nums[i] + nums[j] + nums[k] == 0。返回所有和为0且不重复的三元组。示例:
nums = [-1,0,1,2,-1,-4]→ 输出[[-1,-1,2],[-1,0,1]]约束:
3 <= nums.length <= 3000,-10^5 <= nums[i] <= 10^5
难度:中等
核心考察:双指针 + 去重 + 边界条件
三、全程实录
00:00 - 02:00 | 读题 + 破题
扫读关键信息:
数组长度最大3000 → O(n²)可接受,O(n³)不行
有重复元素 → 需要去重
返回三元组,不是下标 → 排序不影响结果
思维路径:
三数之和 = 固定一个数 + 两数之和问题
两数之和可以用双指针 O(n) 解决
外层固定一个数,遍历 O(n)
总复杂度 O(n²),可接受
选择方案:排序 + 双指针
排序 → 固定i → 双指针l=i+1, r=n-1 → 移动指针寻找和为 -nums[i]
02:00 - 05:00 | 手写代码框架
Go
func threeSum(nums []int) [][]int {
sort.Ints(nums)
result := [][]int{}
n := len(nums)
for i := 0; i < n-2; i++ {
// 去重:跳过重复的固定元素
if i > 0 && nums[i] == nums[i-1] {
continue
}
target := -nums[i]
l, r := i+1, n-1
for l < r {
sum := nums[l] + nums[r]
if sum == target {
result = append(result, []int{nums[i], nums[l], nums[r]})
// 去重:跳过重复的左右指针
for l < r && nums[l] == nums[l+1] { l++ }
for l < r && nums[r] == nums[r-1] { r-- }
l++
r--
} else if sum < target {
l++
} else {
r--
}
}
}
return result
}
05:00 - 10:00 | 边界测试与修复
测试用例思维跑表:
| 用例 | 预期 | 代码能否通过 |
|---|---|---|
[-1,0,1,2,-1,-4] |
[[-1,-1,2],[-1,0,1]] |
✅ |
[0,0,0,0] |
[[0,0,0]] |
✅ 去重逻辑正确 |
[-2,0,1,1,2] |
[[-2,0,2],[-2,1,1]] |
✅ |
[1,2,3,4] |
[] |
✅ |
[] |
[] |
✅ n<3返回空 |
发现潜在问题:当nums[i]已经大于0时,后面的数都大于0,三数之和不可能为0。
优化:添加提前终止条件
Go
for i := 0; i < n-2; i++ {
if nums[i] > 0 {
break // 后面的数都>=当前数,和不可能为0
}
// ... 原有逻辑
}
10:00 - 12:00 | 复杂度分析与Edge Cases文档化
复杂度:
时间:O(n²) --- 外层循环O(n) × 内层双指针O(n)
空间:O(1) --- 不计输出结果
Edge Cases清单:
-
数组长度 < 3 → 直接返回空
-
所有数 > 0 → 提前终止
-
所有数 < 0 → 需要遍历完
-
全0 → 只返回一组
[0,0,0] -
重复元素 → 三重去重(i层、l层、r层)
12:00 - 15:00 | 最终代码优化与提交
最终版本:
Go
func threeSum(nums []int) [][]int {
if len(nums) < 3 {
return [][]int{}
}
sort.Ints(nums)
result := make([][]int, 0)
n := len(nums)
for i := 0; i < n-2; i++ {
// 剪枝:最小的数都大于0,后面不可能有解
if nums[i] > 0 {
break
}
// i层去重
if i > 0 && nums[i] == nums[i-1] {
continue
}
target := -nums[i]
l, r := i+1, n-1
for l < r {
sum := nums[l] + nums[r]
if sum == target {
result = append(result, []int{nums[i], nums[l], nums[r]})
// l层去重
for l < r && nums[l] == nums[l+1] {
l++
}
// r层去重
for l < r && nums[r] == nums[r-1] {
r--
}
l++
r--
} else if sum < target {
l++
} else {
r--
}
}
}
return result
}
四、复盘
4.1 时间分布
| 阶段 | 用时 | 关键产出 |
|---|---|---|
| 读题破题 | 2分钟 | 确定排序+双指针方案 |
| 写代码框架 | 3分钟 | 核心循环+双指针逻辑 |
| 边界测试与修复 | 5分钟 | 三重去重+剪枝优化 |
| 复杂度分析 | 2分钟 | 文档化Edge Cases |
| 最终优化 | 3分钟 | 提交前检查 |
4.2 关键决策记录
| 决策点 | 选择 | 理由 |
|---|---|---|
| 是否排序 | 排序 | 不改变结果,双指针前提 |
| 去重位置 | i层、l层、r层三处 | 避免重复三元组 |
| 剪枝条件 | nums[i] > 0 |
提前终止,常数优化 |
| 数据结构 | 切片[][]int |
Go惯用法 |
4.3 易错点对照
| 易错点 | 错误写法 | 正确写法 |
|---|---|---|
| 去重位置错误 | 只在最后去重 | 在移动指针时同步去重 |
| 越界访问 | nums[i] == nums[i+1] |
i > 0 && nums[i] == nums[i-1] |
| 漏掉零解 | 跳过所有重复 | 保留[0,0,0]这种情况 |
五、自测表
完成一道题后,用以下维度自评:
| 维度 | 优秀(5分) | 及格(3分) | 不及格(1分) | 自评 |
|---|---|---|---|---|
| 破题速度 | 2分钟内出方案 | 5分钟内 | >5分钟 | 5 |
| 代码正确性 | 一次通过 | 1-2次调试 | >2次 | 4 |
| 边界覆盖 | 全部考虑 | 主要边界 | 遗漏关键边界 | 5 |
| 代码可读性 | 命名清晰、有注释 | 基本清晰 | 变量名混乱 | 4 |
| 复杂度分析 | 准确且优化 | 仅给出复杂度 | 不会分析 | 5 |
总分:23 → 可优化点:提交前快速跑3个测试用例
六、训练法
6.1 每日训练计划
| 阶段 | 内容 | 时长 |
|---|---|---|
| 热身 | 看一道已做过的题,口述思路 | 5分钟 |
| 快闪 | 新题限时15分钟 | 15分钟 |
| 复盘 | 对比最优解,记录差距 | 10分钟 |
| 归档 | 存入个人题库,标注易错点 | 5分钟 |
6.2 题型优先级
| 优先级 | 题型 | 考察重点 | 刷题占比 |
|---|---|---|---|
| P0 | 数组、哈希表、双指针 | 基础数据结构 | 30% |
| P0 | 二叉树遍历 | 递归/迭代 | 20% |
| P1 | 动态规划 | 状态转移 | 20% |
| P1 | 滑动窗口、前缀和 | 连续子数组 | 15% |
| P2 | 图论、回溯 | 搜索剪枝 | 15% |
6.3 15分钟倒计时心态法则
0-3分钟:读题+确定方案
3-10分钟:写代码框架 + 核心逻辑
10-13分钟:补边界 + 去重 + 剪枝
13-15分钟:跑脑内测试用例 + 修正
铁律:超过3分钟还没思路,先写暴力解拿部分分,不要空着。
七、写在最后
算法快闪的核心不是难题怪题,而是在有限时间内把一道中等题完整地、高质量地写出来。
这道三数之和,涵盖了:
排序预处理
双指针算法
三重去重逻辑
剪枝优化
边界条件全覆盖
如果你能在15分钟内独立完成,说明你的思维速度和工程落地能力已经通过了实战检验。