TikTok OA 的算法题,最近几轮考下来有一个很反直觉的现象:真正卡人的往往不是标着 Hard 的难题,而是那些看起来只有 Medium,但数据范围和边界条件设计得非常刁钻的题目。这段时间我帮不少候选人复盘过在线笔试记录,发现高频题型的分布其实非常集中,数组哈希、滑动窗口、区间合并、网格图 DFS/BFS 这几类占了绝大多数。这篇文章就把这些高频考点的完整思路、可运行代码和变体扩展整理出来,适合正在准备 TikTok 算法笔试、尤其是时间紧想按优先级刷题的人参考。文章不会去赌某一场的具体考题,而是把这段周期内出现频率最高的题型和考法拆开讲透,让你拿到新题也能快速套上正确的解法框架。
1. 先说清楚:TikTok OA 到底考什么、怎么判分
1.1 题目构成与时间限制
TikTok 的 OA 一般控制在 70 到 90 分钟,题目数量在 2 到 4 道之间。常见组合是两道 Medium 加一道 Hard,偶尔会出现一道 Easy 热身题。这里的 Medium 和 Hard 是相对 LeetCode 难度而言的,但 OA 里的 Medium 通常比 LeetCode 上同标签的题要稍微绕一点,主要绕在两点:一是题目描述会包裹一层业务场景,比如推荐列表去重、视频标签合并、用户关注关系统计;二是输入输出格式不按 LeetCode 的标准函数签名给,需要你自己处理标准输入。
时间分配上,我的建议是每道题先花 2 到 3 分钟读题,想清楚数据范围再动手。很多人挂在第二道题上,就是因为第一道题写完觉得稳了,第二道题就开始着急,结果读题不仔细,把“至少 k 个不同字符”看成了“恰好 k 个”。OA 没有面试官在旁边提醒你,读错一个条件可能整道题全错。
1.2 判分逻辑:全部测试用例通过才算满分
OA 的判分不是只看最后答案对不对,而是按测试用例给分。平台会跑一组不可见的测试用例,你通过的用例数占总用例数的比例就是你的得分比例。也就是说,如果你写了暴力解法,在小数据用例上能过一部分,也能拿到一点分,但如果你想要通过简历筛选,必须尽量把隐藏的大数据用例也过掉。
这意味着两个关键点:第一,复杂度的优化直接决定你能拿多少分,O(n^2) 的代码如果遇到 n = 10^5 的用例,基本就是超时零分;第二,边界情况必须覆盖清楚,空输入、单元素、重复元素、负数、超大值这些用例往往就是拉开差距的地方。我见过很多候选人思路完全正确,但因为数组越界或者没处理空数组,导致 3 个用例失败,最后只拿了 70% 的分数。
1.3 环境与输入输出要求:函数签名还是标准输入
不同的 OA 平台要求不一样,有的给你一个函数签名让你补全,有的直接要求你从标准输入读取并打印结果。TikTok 的 OA 两种形式都出现过,但近几年更偏向标准输入输出。这里给一个 Python 的输入模板,可以应付绝大多数情况:
python复制import sys
def main():
data = sys.stdin.read().strip().split()
if not data:
return
# 假设第一行是 n 和一个 target
n = int(data[0])
target = int(data[1])
nums = list(map(int, data[2:2 + n]))
# 你的算法逻辑
result = your_function(nums, target)
print(result)
if __name__ == "__main__":
main()
这个模板的好处是避免用 input() 逐行读可能出现的读不到数据或读超时问题。如果 OA 给了函数签名,那就直接用函数签名,不用管输入输出。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频题型分布:按出现频率排出刷题优先级
2.1 近几轮考情观察:各题型大致占比
我把最近这段周期内出现过的 OA 题目按考点归了个类,得到一个粗略的占比分布。要说明的是,这不是官方的统计,只是基于我看到的题目和候选人反馈做的经验性归纳,但用来做刷题优先级参考是够的。
| 考点方向 | 大致占比 | 典型难度 |
|---|---|---|
| 数组与哈希表 | 25% | Medium |
| 滑动窗口与双指针 | 20% | Medium |
| 区间合并与贪心 | 15% | Medium |
| 网格图 DFS/BFS | 15% | Medium |
| 二分查找与堆 | 15% | Medium 到 Hard |
| 其他(动态规划、栈) | 10% | Hard |
看出规律了吗?数组哈希、滑动窗口、区间、网格图这四类加起来占了大约 75%。这意味着你不需要把 LeetCode 上所有 Hard 题都刷一遍,只要把这四个方向练熟,OA 的基本盘就稳了。
2.2 为什么这几个方向是重点
从出题者的角度看,这几个方向最容易设计出区分度高的题目。它们有明确的数学结构,能保证答案唯一,而且可以用不大不小的数据范围(比如 n = 10^5)来卡掉暴力解法。更重要的是,这些数据结构在真实业务场景里都有直接对应:哈希表对应推荐系统的去重和快速查询,滑动窗口对应时间窗口内的统计分析,区间合并对应内容排期和标签聚合,网格图对应社交关系或内容传播路径的建模。所以它们不只是在 OA 里高频,在实际工程面试里也经常出现。
有候选人问我要不要花大量时间刷动态规划。我的看法是:动态规划确实会考,但频率和难度都更适合在基础扎实之后再去突破。如果你的目标是短期内通过 OA 筛选,优先把上面四类做到“闭着眼能写”的程度,收益比最高。DP 只需要掌握最经典的背包、打家劫舍、最长递增子序列这类题,再结合题目条件判断是否需要状态压缩就够了。
3. 第一类必考:数组与哈希表,两数之和变体精讲
3.1 题目原型:输出所有不重复的数字对
先看一道非常典型的变体题,它在几轮 OA 里反复出现,考的是哈希表在数组问题里的灵活运用。
题目描述:给定一个无序整数数组 nums 和一个目标值 target,请找出所有和等于 target 的不重复数字对。数字对中的两个数按升序排列,所有数字对再按第一关键字升序排列。数组中如果有重复数字,同一个数字对只输出一次。
示例输入:
code复制nums = [1, 4, 2, 3, 0, 5], target = 5
示例输出:
code复制(0, 5), (1, 4), (2, 3)
注意:[1, 4] 和 [4, 1] 算同一个数字对;如果数组是 [2, 2, 3, 3],也只输出一个 (2, 3)。
3.2 思路推导:为什么用哈希表而不是排序加双指针
拿到这道题,最容易想到的是两层循环枚举所有组合,时间复杂度 O(n^2)。如果 n 只有几百,这个写法没问题,但 OA 的数据范围通常给到 10^4 甚至 10^5,O(n^2) 会直接超时。
第二个方案是先把数组排序,再用双指针从两端往中间扫。排序是 O(n log n),双指针是 O(n),整体复杂度是 O(n log n),能过。但这道题要求输出不重复的数字对,排序之后双指针虽然也能处理,但去重逻辑写起来稍显繁琐。
第三个方案就是哈希表。遍历数组,对于每个数字 num,检查 target - num 之前是否出现过。这个思路的核心是:我们不需要关心数字出现的位置,只关心某个数值是否出现过。用 set 就可以做到 O(1) 查询,整体复杂度降到 O(n)。对 OA 来说,这是最优解。
去重怎么处理?这里不能用“每个数字只能用一次”的错误直觉,因为题目要的是“不重复的数字对”,而不是“不重复的下标组合”。比如数组 [2, 2, 3, 3],下标组合有 4 种,但数字对只有 (2, 3) 一种。正确的做法是:在配对成功时,把这对数字记录到 used set 里,之后遇到相同数字直接跳过。
3.3 完整代码与逐行说明
python复制def find_pairs(nums, target):
seen = set() # 记录已经出现过的数字
used = set() # 记录已经配对成功的数字
result = []
for num in nums:
complement = target - num
if complement in seen and num not in used and complement not in used:
result.append((min(num, complement), max(num, complement)))
used.add(num)
used.add(complement)
seen.add(num)
result.sort()
return result
逐行逻辑:
seen保存已经遍历过的数字,用于查找 complement 是否出现。used保存已经配对的数字,防止重复数字对。只有当 num 和 complement 都还没被用过时,才构成一个新的有效配对。- 配对成功后把 num 和 complement 都加入 used,这样后续重复出现的数字就不会再生成同样的对了。
result.sort()满足题目要求的输出顺序。
测试一下:
python复制print(find_pairs([1, 4, 2, 3, 0, 5], 5))
# 输出: [(0, 5), (1, 4), (2, 3)]
print(find_pairs([2, 2, 3, 3], 5))
# 输出: [(2, 3)]
print(find_pairs([1, 2, 3], 10))
# 输出: []
3.4 变体扩展:三数之和、返回索引、元素可重复
这道题最常见的变体有三个方向。
第一个方向是返回下标而不是数字对。这时候 set 就不够用了,要用 dict 存“数字 -> 下标”。注意如果数组里有重复数字,要处理下标覆盖的问题,通常只存最后一次出现的下标即可,因为更早的下标已经匹配过了。
第二个方向是三数之和。本质是固定一个数,然后对剩下的部分做两数之和。先排序,然后外层循环固定第一个数,内层用双指针处理后面两个数。这样可以把时间复杂度从 O(n^3) 降到 O(n^2)。重点是排序后的去重,比如固定的数如果和前一个相同就跳过,避免生成重复的三元组。
第三个方向是元素可以重复使用。有些题目会说明同一个元素可以使用多次,这时候双指针方案里的去重逻辑要改,哈希表方案也要留意。建议在写代码前先确认题目到底说的是“不同的下标”还是“不同的数字值”,这决定了整个去重逻辑的写法。
提示:遇到“不重复”“去重”“无序”这些关键词,优先考虑用哈希表做去重,用控制变量法降低循环层数。我在实际写题时习惯先把去重条件用注释写在代码上方,避免写到一半逻辑混乱。
4. 第二类必考:滑动窗口与双指针,至多包含 k 个不同字符的最长子串精讲
4.1 题目原型:最长子串长度
滑动窗口是 OA 里另一大类高频题,它通常和字符串、数组的连续子区间绑定出现。看这道典型的变体题。
题目描述:给定一个字符串 s 和一个整数 k,请你找出 s 中最长的子串长度,要求该子串中至多包含 k 个不同字符。
示例输入:
code复制s = "eceba", k = 2
示例输出:
code复制3
因为最长的合法子串是 "ece",包含 e 和 c 两个不同字符,长度为 3。如果把 k 换成 3,答案就变成 4,也就是整个字符串 "eceb"。
4.2 思路推导:为什么必须用滑动窗口
最直接的做法是枚举所有子串,检查每个子串的不同字符数是否不超过 k,复杂度 O(n^2)。这个方案在小数据下可行,但 OA 的字符串长度通常给到 10^5,O(n^2) 必挂。
滑动窗口的思路是:用两个指针 left 和 right 维护一个窗口,右指针向右扩展窗口,每次加入一个新字符;当窗口内不同字符数超过 k 时,左指针向右收缩窗口,直到窗口重新合法。整个过程只用遍历一次字符串,每个字符最多被 left 和 right 各访问一次,时间复杂度 O(n)。
这个思路的本质是维护一个“当前窗口内每个字符的出现次数”的哈希表。扩展时更新频率,收缩时减少频率,当某个字符频率降到 0 就把它从哈希表里删除。这样哈希表的 size 就是窗口内不同字符的数量。
为什么收缩条件用 while 而不是 if?因为只移动一次 left 可能还不够,窗口可能仍然包含超过 k 个不同字符。比如 s = "abc",k = 2,当 right 走到 'c' 时窗口是 "abc",left 只移动一格到 "bc" 仍然包含 2 个不同字符,恰好合法;但如果 s = "aabbc",k = 2,加入 'c' 后窗口是 "aabbc",有 3 个不同字符,left 需要连续移动三格才能删掉所有 'a',使窗口变成 "bc"。所以必须用 while 循环持续收缩。
4.3 完整代码与逐行说明
python复制def longest_substring_with_k_distinct(s, k):
if not s or k == 0:
return 0
left = 0
freq = {}
max_len = 0
for right, char in enumerate(s):
freq[char] = freq.get(char, 0) + 1
while len(freq) > k:
left_char = s[left]
freq[left_char] -= 1
if freq[left_char] == 0:
del freq[left_char]
left += 1
max_len = max(max_len, right - left + 1)
return max_len
逐行逻辑:
freq记录窗口内每个字符的出现次数,len(freq)就是窗口内不同字符数。- 每次扩展 right 后,检查窗口是否合法。不合法就移动 left,同时更新 freq。
left_char的频次减到 0 时,必须从字典里删除,否则 len(freq) 不会改变,while 会一直走下去。- 每次窗口合法后,用
right - left + 1更新答案。
测试:
python复制print(longest_substring_with_k_distinct("eceba", 2))
# 输出: 3
print(longest_substring_with_k_distinct("eceba", 3))
# 输出: 4
print(longest_substring_with_k_distinct("aabbc", 2))
# 输出: 4 ("aabb" 或 "abbc")
4.4 变体扩展:最长无重复子串、最小覆盖子串、处理字符全相等的边界
这类题在 OA 里的变形非常多,但核心模板是一致的。最长无重复子串本质上就是题目的 k 等于无穷大(或者 k 等于字符集大小),实现时不用管 k,直接维护 freq 字典,当某个字符出现次数大于 1 时收缩窗口即可。
最小覆盖子串则是一个反向操作:不是限制不同字符数,而是要求窗口内包含目标串的所有字符。这里需要维护一个“还需要多少种字符”的计数器,right 扩展时满足条件,left 收缩时寻找最小长度。它仍然是同一套滑动窗口模板,变化的是窗口合法的判定条件。
还有一个我踩过坑的边界:如果 k 大于等于字符串的不同字符总数,答案就是整个字符串的长度。比如 s = "abc", k = 5,窗口永远不会因为不同字符过多而收缩,最终 max_len 会是 3。你的代码要能自然处理这种情况,不需要特判,但如果用了 k == 0 的特判,记着 0 长度时返回 0 而不是返回空串。
提示:滑动窗口的题目,关键不是背代码,而是理解“什么时候扩展 right、什么时候收缩 left、用什么条件判定窗口合法”。把这三个问题想清楚,所有变体都能套那个模板。
5. 第三类必考:区间合并问题,合并区间精讲
5.1 题目原型:合并所有重叠区间
区间类问题在 OA 里出现的频率不低,因为它能很好地考察排序加贪心的组合能力。最经典的一道就是合并区间。
题目描述:给定一个区间的集合 intervals,其中每个区间为 [start, end],请合并所有重叠区间,返回一个不重叠的区间数组。
示例输入:
code复制intervals = [[1, 3], [2, 6], [8, 10], [15, 18]]
示例输出:
code复制[[1, 6], [8, 10], [15, 18]]
因为 [1, 3] 和 [2, 6] 有重叠,合并为 [1, 6]。
5.2 思路推导:排序是解区间题的前提
区间重叠的判断很简单:如果两个区间 [a, b] 和 [c, d] 满足 c <= b,说明它们有重叠。但问题是,如果区间数组是无序的,你无法高效判断一个区间能和哪些区间合并。比如 [10, 20] 和 [1, 2],如果不排序,你可能会在遍历到后面时才看到 [1, 2],前面的合并结果就要反复更新。
所以解区间题的第一步永远是按 start 排序。排序之后,所有可合并的区间一定在位置上连续,这保证了你只需要遍历一次,就能把整个数组扫完。合并逻辑是:用 result 里的最后一个区间 current 和当前遍历的区间 interval 比较,如果 interval.start <= current.end,说明重叠,把 current.end 更新为两者的最大值;否则把 interval 作为一个新区间加入 result。
这里有一个容易忽略的点:为什么更新 end 的时候要用 max(current.end, interval.end) 而不是直接用 interval.end?因为区间可能完全被包含,比如当前区间 [1, 10],新来的区间 [2, 3],如果直接用 3 覆盖 10,就漏掉了 [1, 3] 之外的 [4, 10]。所以必须取最大值。
5.3 完整代码与逐行说明
python复制def merge(intervals):
if not intervals:
return []
intervals.sort(key=lambda x: x[0])
merged = [intervals[0][:]] # 用切片拷贝,避免修改原数组
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
逐行逻辑:
- 排序按 start 升序,保证待合并的区间在位置上连续。
merged的最后一个元素就是我们正在维护的“当前区间”。- 判断条件
start <= merged[-1][1]是标准的重叠判定。 - 更新时取
max,处理完全包含的情况。
测试:
python复制print(merge([[1, 3], [2, 6], [8, 10], [15, 18]]))
# 输出: [[1, 6], [8, 10], [15, 18]]
print(merge([[1, 4], [4, 5]]))
# 输出: [[1, 5]] 注意 start == end 也算重叠
print(merge([[1, 10], [2, 3]]))
# 输出: [[1, 10]]
5.4 变体扩展:插入区间、会议室、区间交集与最少移除
区间题的变体通常围绕“重叠”这个核心概念做文章。
插入区间是合并区间的高频变体:给定一个已经按 start 排序且不重叠的区间列表,再给一个新区间,要求把新区间插入并合并。思路是先找到插入位置,再复用合并逻辑。实现时可以先把新区间加入列表再排序再合并,虽然简单,但如果列表很长,排序会多一个 O(n log n);最优解是利用有序性在 O(n) 内完成。
会议室问题是另一个高频变体:给定一组会议的开始和结束时间,判断一个人能否参加所有会议。本质上就是判断区间是否两两不重叠。排序后遍历,看当前会议的开始时间是否早于上一个会议的结束时间,如果是就冲突。这个题在 OA 里经常被套上“视频会议”“内容排期”的业务场景。
还有一类贪心变体,比如“给定区间列表,求最少移除多少个区间才能使剩余区间互不重叠”。解法是先按 end 排序,然后贪心地选取结束时间最早的区间,跳过与它重叠的区间。这类题不要用合并区间的模板硬套,因为目标不同:合并是为了找出覆盖范围,贪心是为了保留最多不相交区间。
提示:区间题的关键是先判断题目问的是“覆盖”还是“冲突”。覆盖问题用合并区间模板,冲突问题用排序加贪心。这两个方向方向相反,用错模板会直接零分。
6. 第四类必考:网格图上的 DFS/BFS,岛屿数量精讲
6.1 题目原型:统计二维网格中的岛屿数量
网格图上的 DFS/BFS 是 TikTok OA 里反复出现的一类题目,因为它的代码量不大,但很考验对“状态标记”和“遍历顺序”的理解。
题目描述:给你一个由 '1'(陆地)和 '0'(水域)组成的二维网格 grid,请你计算网格中岛屿的数量。岛屿由四面相连的 '1' 组成,斜角连接不算。
示例输入:
code复制grid = [
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1']
]
示例输出:
code复制3
格子里有三块陆地,分别分布在左上、中间和右下。
6.2 思路推导:为什么要在入栈时立即标记已访问
网格图问题的核心思路是:遍历每个格子,当遇到一个陆地时,说明发现了一个新的岛屿,计数加一;然后从这个格子出发,把所有和它相连的陆地全部标记成已访问,这样后续遍历到这些陆地时不会重复计数。
标记方式有两种:一是额外开一个 visited 二维数组,二是直接修改原数组把 '1' 改成 '0'。OA 场景下直接改原数组通常没问题,还省空间。但这里有一个非常关键的经验:一定要在把格子放入栈(或递归调用)的瞬间就标记为已访问,而不是在从栈里弹出时才标记。
我见过很多候选人在这里踩坑。如果他们用的是队列/栈实现 BFS/DFS,先出队再标记,会导致同一个格子被多次加入队列。比如 A 的右邻居 B 和 下邻居 C 都指向同一个格子 D,D 还没被标记时,B 和 C 会分别把 D 加入队列。在小数据上可能侥幸不超时,一旦数据稍大就会超时或者计数错误。
解决这个问题的原则是:入队即标记。每次把一个邻居加入队列时,立刻把它的值改成 '0',这样其他邻居再看到它时会发现它不是陆地,就不会重复入队。
6.3 完整代码与逐行说明
这里我用栈实现迭代版 DFS,因为不需要递归开销,也避免了 Python 递归深度超限的问题。
python复制def num_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
stack = [(r, c)]
grid[r][c] = '0'
while stack:
cr, cc = stack.pop()
for dr, dc in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
nr, nc = cr + dr, cc + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
grid[nr][nc] = '0'
stack.append((nr, nc))
return count
逐行逻辑:
- 外层两层循环负责扫描所有格子,遇到 '1' 就开启一个新的搜索过程。
stack模拟 DFS 的调用栈,grid[r][c] = '0'在入栈时立即标记。- 方向数组
[(1, 0), (-1, 0), (0, 1), (0, -1)]表示上下左右四个方向。 - 边界判断
0 <= nr < rows and 0 <= nc < cols必不可少,漏掉它会直接越界报错。
测试:
python复制grid = [
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1']
]
print(num_islands(grid))
# 输出: 3
6.4 变体扩展:岛屿最大面积、被围绕的区域、腐烂的橘子
岛屿数量几乎是网格图题型的“母题”,大部分变体都只是改了一下计数规则或者搜索目标。
岛屿最大面积:同样遍历每个陆地,但遇到 '1' 时不是简单地计数加一,而是用 DFS/BFS 统计出当前连通块的大小,然后取最大值。核心逻辑完全一样,只要在搜索过程中用一个变量累加格子的数量即可。
被围绕的区域:如果要把所有被 'X' 包围的 'O' 改成 'X',思路是先遍历边界上的所有 'O',从它们出发做 DFS/BFS,把能到达的 'O' 标记成特殊字符,比如 '#';然后再遍历整个网格,把剩余的 'O' 改成 'X',把 '#' 恢复成 'O'。这个题的技巧是反向思考:先标记所有不会被包围的 O。
腐烂的橘子:这是 BFS 的层次遍历变体,需要计算多少分钟能把所有新鲜橘子感染。思路是把所有初始腐烂的橘子同时放入队列,用分层遍历模拟每一分钟的扩散。它考的不是简单的连通性计数,而是“扩散时间”,需要用到层数记录。
提示:网格图题目对 Python 递归有深度限制,如果网格很大,递归 DFS 可能直接报 RecursionError。迭代版 DFS/BFS 或直接用栈/队列是最稳妥的写法。我在 OA 里一律用迭代实现,省去不必要的风险。
7. 从思路到 AC:边界条件、输入输出和调试习惯
7.1 输入输出处理:别让读入格式拖垮整场笔试
很多候选人算法思路没问题,却在输入输出上吃了大亏。比如列表形式的输入,可能给的是 [1,4,2,3,0,5] 这种带方括号和逗号的格式,而不是单纯的数字序列。如果你直接用 split 读,会读进来方括号字符,String 转 int 直接报错。
这里给一个通用的处理技巧:先用正则把所有非数字字符替换成空格,再按空格切分。
python复制import re
import sys
def parse_ints_from_line(line):
# 把所有非数字字符替换为空格,再分割
return list(map(int, re.findall(r'-?\d+', line)))
注意 -?\d+ 里的 -?,这是为了匹配负数。OA 里经常出现负数测试用例,漏掉负号会读错数据。
如果题目给了函数签名,那就直接用函数签名,不要自己写输入输出。但如果你不确定平台是函数签名还是标准输入,最好两种写法都准备一下,考试时看到再选。
7.2 边界条件检查清单:每道题写完后必须过一遍
我总结了一个边界条件清单,每次写完代码后会按顺序过一遍,能避免大部分丢分:
- 空输入:数组为空、字符串为空、网格为空,返回结果是什么?
- 单元素:只有一个元素的数组、只有一个字符的字符串、只有一行或一列的网格。
- 重复元素:数组中大量重复数字时,去重逻辑是否稳定?
- 负数与零:数组包含负数,target 为负数或零时,排序和取 min/max 的位置是否正确?
- 最大值与最小值:区间端点出现很大或很小的整数,比较时是否会溢出(Python 不会,但 C++/Java 需要注意)?
- 极端 k 值:滑动窗口里 k 为 0 或大于字符总数时的情况。
- 数据范围导致的超时:没想清楚复杂度就提交,是大忌。
这些边界条件不是每个都会出现在测试用例里,但只要你漏掉其中一个,对应用例就会挂掉,而且这类错误往往在本地测试时不容易发现,因为你会默认输入都是理想情况。
7.3 调试技巧:构造样例、暴力对拍、双人校验
写题过程中最有效的调试方法是构造一个小规模的测试样例,手算一遍期望输出,然后跑代码。如果结果不一致,用 print 在关键位置输出中间状态,比如两个指针的位置、哈希表的内容、队列里的元素,一目了然。
更进阶的方法是暴力对拍:写一个简单的 O(n^2) 暴力版本,再用随机数据跑你的最优解,两边结果对比。这个方法在本地做开发时极其好用,能快速找到一个隐蔽的边界 bug。我在准备 OA 时,每道题都会用这个方法跑一遍,数据规模设成 1000 左右,随机跑几百次,能覆盖到人脑想不到的输入组合。
提示:如果你用的是 Python,写代码时充分利用
8. 最后一周的冲刺安排与个人经验
8.1 七天刷题计划:分类练习、混合练习、模拟考试
如果你的时间只剩一周,不建议盲目刷题。我的建议是把七天分成三个阶段。
前三天做分类练习,每天只刷一个方向:第一天刷数组与哈希表,刷 8 到 10 道题,覆盖两数之和、三数之和、字母异位词分组、最长连续序列这些经典题;第二天刷滑动窗口与双指针,覆盖无重复字符的最长子串、最小覆盖子串、盛最多水的容器;第三天刷区间和网格图,覆盖合并区间、会议室、岛屿数量、腐烂的橘子。
第四天和第五天做混合练习,从题库里随机抽题,每道题限制在 25 分钟内完成。这是为了模拟 OA 里“突然切换题型”的节奏,训练你快速识别题目类型的能力。识别题型比直接想解法更重要,因为每道题浪费 5 分钟在错误的解法方向上,代价是致命的。
第六天和第七天做完整模拟,挑一个固定时间段,用 75 分钟完成一套 3 道题的组合,做完对答案、复盘、修改代码。这个阶段不要查资料,完全模拟真实考试环境。如果你在模拟中发现自己某类题型超时严重,就回头补那类题的基础题。
8.2 OA 现场的时间分配策略
进了 OA 界面,先在草稿纸上写清楚每道题的时间预算。我的习惯是:先通读所有题目,把题分成“一眼会做”“需要想一下”“完全没思路”三档。先做第一档,保证这些题满分;再做第二档;最后有时间再碰第三档。
如果一道题卡了 15 分钟,果断跳过。OA 是看总分的,不是看单题是否完美的。不要为了一道 Medium 跟它死磕,结果后面有一道很简单的题没时间写,那种懊恼你会有很深的体会。
还有一个小技巧:先写暴力解拿分,再优化。如果你一眼看不出最优解,先把暴力解写上,确保能过一部分测试用例,然后再在剩下的时间里优化。这样就算最终没有优化成功,你也不至于拿零分。
8.3 个人注意事项:环境、键盘、心态
最后说几个容易忽略的细节。考试前一定检查网络环境稳定,OA 平台不同,有的断线重连会中断计时导致白丢时间。准备一个本地的 Python 环境,因为有时候平台自带的编辑器调试不方便,本地跑通再粘贴上去效率更高。
心态方面,不要把 OA 当成一场“不能失败的战争”。我见过太多候选人因为第一题卡住,后面整场心态崩了。遇到不会的题很正常,真正拉开差距的是你如何处理劣势局面:跳过它、保底分、留足时间做后面的题,这是成年人的做题方式。
还有一个我的个人习惯:每道题写完代码后,先自己构造两到三个样例测试,再提交。花 2 分钟自测能避免 50% 以上的低级失误。这 2 分钟花得比什么都值。
