刚在云栈社区看到有人分享了一个帖子,挺有感触的。说的是有位网友去面试,碰到的候选人比他大8岁,简历上写着是前大厂P7,曾经年薪百万。但现在失业快一年了,面试时只敢开口要20k的月薪,还一再表示不挑活、能加班。发帖的网友说,那一瞬间,仿佛看到了几年后的自己。

这件事最扎心的地方在哪?或许在于,我们总以为站得足够高就能一劳永逸,但现实会告诉你,职场如流水线,谁都有掉队的可能。
不过换个角度想,这位前辈的经历也给了我们两点提醒:第一,过去的功劳簿不能吃一辈子,千万别把“年薪百万”当成永远的护身符;第二,再辉煌的履历,在遭遇低谷时,也只能放下身段,咬牙向前。
所以,与其被这种故事吓到,陷入焦虑,不如早点行动起来,给自己多准备几条路:技能别太单一、手头存点钱、也别过分迷信所谓的大厂光环。
说到技能,面试求职过程中绕不开的,就是算法题。比如下面这个经典的“三数之和”。
“三数之和”到底难在哪?
题目就一句话:给你一个整数数组 nums,找出所有和为 0 的三元组,并且三元组不能重复。
很多人第一反应就是用三个 for 循环暴力枚举:
def three_sum_bruteforce(nums):
n = len(nums)
res = set()
for i in range(n):
for j in range(i + 1, n):
for k in range(j + 1, n):
if nums[i] + nums[j] + nums[k] == 0:
triplet = tuple(sorted([nums[i], nums[j], nums[k]]))
res.add(triplet)
return [list(t) for t in res]
这个思路没问题,但效率太低,时间复杂度是 O(n³),数据量一大必然超时。当然,这个版本有两个点值得注意:
- 用
set 来去重,因为 [1, -1, 0] 和 [-1, 0, 1] 是同一个三元组。
- 把找到的三元组排序后转成
tuple,才能存入 set。
这种写法对付小样例可以,但面试中基本过不了关。
高效解法:排序 + 双指针
更常见的解法是“排序后使用双指针”,核心思路如下:
- 排序:先把数组从小到大排好序。这是后续所有操作的基础。
- 固定一个数:外层循环固定一个数
nums[i],问题就转化为在 i 右侧的区间里,寻找两个数,使它们的和等于 -nums[i]。
- 双指针搜索:在
i 右侧的区间设置左指针 left 和右指针 right。
- 如果
nums[left] + nums[right] < -nums[i],说明和太小,left 右移。
- 如果
nums[left] + nums[right] > -nums[i],说明和太大,right 左移。
- 如果正好相等,就找到一组解,记录下来,然后左右指针同时向中间移动,继续寻找。
这样一来,外层循环 O(n),内层双指针总共 O(n),整体时间复杂度就降到了 O(n²),比 O(n³) 高效得多。
真正的难点:去重
这题最“恶心”的地方不在于算法思路,而在于“不重复”这个条件。重复主要来自三个方面:
- 外层固定的数
nums[i] 重复。
- 左指针
nums[left] 重复。
- 右指针
nums[right] 重复。
所以,我们需要在代码的三个地方加入“跳过重复值”的逻辑。
完整代码与细节
下面是考虑了所有细节的 Python 实现:
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
n = len(nums)
res: List[List[int]] = []
for i in range(n):
# 剪枝:因为数组已排序,如果最小的数都大于0,后面不可能再有三数之和为0
if nums[i] > 0:
break
# 跳过相同的起点,避免重复三元组(去重点1)
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, n - 1
target = -nums[i]
while left < right:
s = nums[left] + nums[right]
if s == target:
res.append([nums[i], nums[left], nums[right]])
# 左指针去重(去重点2)
left_val = nums[left]
while left < right and nums[left] == left_val:
left += 1
# 右指针去重(去重点3)
right_val = nums[right]
while left < right and nums[right] == right_val:
right -= 1
elif s < target:
left += 1
else:
right -= 1
return res
可以测试一下:
print(Solution().threeSum([-1, 0, 1, 2, -1, -4]))
# 输出:[[-1, -1, 2], [-1, 0, 1]]
关键点梳理
- 排序是前提:不排序,双指针的逻辑就不成立。
- 剪枝优化:
if nums[i] > 0: break 是因为数组有序,nums[i] 作为三元组中最小的数如果已经大于0,和不可能为0。
- 去重逻辑:三处去重必须写对,且顺序很重要。找到答案后的去重,一定要先记录当前值,再用 while 循环跳过所有相同的值,否则容易出错。
题目考察的本质
表面上是求三数之和,实际上考察的是:
- 优化意识:能否从 O(n³) 的暴力法,想到 O(n²) 的“固定一个数+双指针”套路。
- 利用有序性:排序后,能否大胆使用剪枝条件。
- 代码严谨性:处理边界条件和重复数据时是否细心。
刷题到一定阶段会发现,很多题目都可以归入“排序+双指针”这个模式,比如两数之和 II(有序数组)、最接近的三数之和、盛最多水的容器等,思路都是相通的。掌握这个模式,能帮你解决一大类问题。