现场编码中的反例与测试
现场编码需要同时展示问题澄清、算法推导、实现和自检。写出能通过示例的代码只是第一步,边界、反例和复杂度决定答案是否可靠。
1. 写代码前确认题目边界
先用简短问题确认:
- 输入可以为 null 或空吗?
- 元素是否重复、已排序、可能为负吗?
- 是否保证有解,可能有多个解吗?
- 数量和值域上限是多少?
- 可以修改输入吗?
- 返回任意解、全部解还是稳定顺序?
- 字符串字符集是什么?
- 时间和额外空间限制是什么?
这些问题直接影响算法。两数之和若数组已排序,可以双指针;未排序通常用哈希;要求全部组合还要定义重复答案怎样处理。
不要把所有极端假设一次问完。先确认会改变方案或接口的条件,再在实现时说明普通边界。
2. 先陈述不变量和复杂度
例如二分查找可以说:
我维护半开区间
[left, right),目标如果存在,一直位于该区间。每次比较后至少排除一半,时间 O(log n),额外空间 O(1)。
这句话确定了:
- right 初始值应为数组长度。
- 循环条件是
left < right。 - 更新边界时是否加一。
- 返回时如何判断没找到。
如果无法用一句话说明状态不变量,代码中的边界通常也还没想清楚。
3. 选择能直接表达意图的 Java 类型
- 栈和队列使用
ArrayDeque。 - 频率与最近位置使用 HashMap 或在值域小时使用数组。
- 大和、距离和乘积使用 long。
- 比较使用
Integer.compare,不做减法。 - 需要稳定输出时显式选择 LinkedHashMap 或排序。
- 递归深度可能接近 n 时改用显式栈。
面试中不要为了展示熟悉框架引入不必要的 Stream、反射或并发工具。最短的代码不一定最容易验证。
4. 从等价类和边界值构造用例
一组有效测试至少覆盖:
4.1 最小输入
- 空数组或空字符串。
- 单个元素。
- 两个元素刚好触发一次比较。
4.2 结构边界
- 答案在开头、结尾和中间。
- 没有答案。
- 所有元素都相同或都不同。
- 已排序、逆序和交错顺序。
4.3 数值边界
- 0、负数。
Integer.MIN_VALUE、Integer.MAX_VALUE。- 加法、乘法或 Comparator 差值溢出。
4.4 规模边界
- 最大 n 是否导致 O(n²) 超时。
- 深链是否导致递归栈溢出。
- 输出是否本身过大。
用例应针对算法可能失效的假设,不是随机列一堆输入。
5. 用最小反例检查错误方案
反例越小,越容易说明根因。
5.1 贪心反例
若方案总选当前最大值,尝试构造一个较小当前收益能换取更大后续收益的三步输入。
5.2 边界反例
二分死循环通常用长度 1 或 2 的数组暴露;滑动窗口 left 倒退可用同一字符多次出现在窗口外的短串暴露。
5.3 相等性反例
HashMap 键问题可以用两个 equals 相等但哈希不同的对象;Comparator 问题可以用两个不同对象比较为 0。
5.4 溢出反例
Comparator 使用减法时,取一个最大正数和一个负数即可让符号错误。
反例不是等代码失败后再补。提出方案时主动寻找最小失败输入,可以在实现前淘汰错误方向。
6. 手动执行关键路径
完成代码后选择一个小输入,逐步记录核心变量:
| 步骤 | left | right | 当前状态 | 答案 |
|---|---|---|---|---|
| 初始化 | 0 | 0 | 空窗口 | 0 |
| 加入 A | 0 | 1 | A | 1 |
| 加入 B | 0 | 2 | AB | 2 |
| 加入 A | 1 | 3 | BA | 2 |
重点检查:
- 循环是否一定推进。
- 索引访问前是否在范围内。
- 更新答案发生在正确时机。
- 清理状态是否与加入操作对称。
- 返回值是否符合空输入协议。
面试官能看到推导过程,比沉默地多跑几个样例更有价值。
7. 随机测试与性质测试
本地准备时,可以把优化算法与小规模暴力实现对拍:
for (int round = 0; round < 10_000; round++) {
int[] input = randomSmallArray();
assertEquals(bruteForce(input), optimized(input));
}
暴力版本只处理小输入,但逻辑直观。随机生成能发现人没有想到的组合,再把失败输入缩小成固定回归用例。
还可以检查性质:
- 排序后元素多重集不变且非递减。
- 反转两次得到原序列。
- 最短路径结果不超过任何已知可行路径。
- 去重结果不含重复,且每个元素来自输入。
性质测试不能证明全部正确,但能扩大验证范围。
8. 修改代码后重新检查假设
优化往往改变边界:
- 二维 DP 压成一维后,循环方向是否仍使用旧状态。
- int 改 long 后,中间乘法是否也已提升类型。
- 递归改迭代后,访问顺序是否一致。
- HashSet 改数组后,字符值域是否仍合法。
- 并行化后,累积函数是否有关联副作用。
不要只重跑原示例。每次改动都应针对它改变的假设增加一个用例。
9. 面试中的完成顺序
一个可执行的节奏:
- 复述输入、输出和关键约束。
- 给出直接方案和复杂度;需要时再优化。
- 说明核心不变量与选用的数据结构。
- 编写主路径,再补边界。
- 用最小正常例和一个反例手动执行。
- 重新报告时间、空间和可能的工程限制。
卡住时把已知条件和不成立的方向说出来。主动修正一个有证据的错误,比继续默写不确定模板更能展示推理能力。
10. 常见问题
10.1 面试题没说明 null,要主动处理吗
先确认接口约定。面试官若认为输入合法,不必为了 null 分支污染核心算法;可以口头说明生产 API 会按契约选择拒绝、返回空结果或抛参数异常。
10.2 可以先写暴力解吗
可以,尤其在它能帮助建立正确性基线时。先说明复杂度为何不满足最终约束,再复用观察优化。不要把大部分时间用在明知不可交付的实现上。
10.3 测试用例越多越好吗
不是。优先覆盖不同等价类、边界和算法假设。十个重复正常样例不如一个能击穿错误不变量的最小反例。
10.4 代码通过给定样例后还要做什么
检查空与最小输入、最大数值、无解和重复值,手动验证循环推进与索引边界,再说明复杂度和对输入的修改。给定样例通常只覆盖主路径。
11. 面试题
11.1 收到一道信息不完整的现场编码题,应该先确认什么
出现公司:华为 OD、美团
考察重点
- 哪些约束会改变算法或 API。
- 怎样在沟通和编码时间之间取舍。
- 无解、重复、顺序和输入修改边界。
相关内容:第 1 节“写代码前确认题目边界”、第 9 节“面试中的完成顺序”。
参考回答
先复述输入输出,再确认会改变方案的条件:数据规模和值域、是否排序、重复与无解是否允许、返回任意还是全部结果、能否修改输入,以及字符集或图边权等领域条件。由这些约束选择复杂度和数据结构。
普通的 null 处理、异常类型等可以按面试官给出的合法输入约定简化,并口头说明生产接口会补契约。确认完后先讲不变量和复杂度,再实现主路径并用边界反例检查。
11.2 怎样为算法主动构造反例
出现公司:字节跳动、华为 OD
考察重点
- 从方案依赖的假设反向构造输入。
- 最小反例为什么更容易定位根因。
- 边界、重复、溢出和状态恢复的典型错误。
相关内容:第 4 节“从等价类和边界值构造用例”、第 5 节“用最小反例检查错误方案”。
参考回答
先写出方案成立依赖的条件,再尝试让其中一个条件失效。贪心就构造局部最优妨碍后续的短输入;窗口检查单调性被负数破坏;二分用长度一、二验证边界推进;哈希和排序用重复键、比较为零和整数溢出。
优先寻找最小失败输入,因为变量少、执行路径短,更容易说明错误来自哪条不变量。修复后把这个输入保留为回归用例,再补最大规模和普通路径。