程序员老鬼

某大厂程序员:大裁员消息来了,希望至少挺过这个夏天,因为这个夏天很热,空调实在太贵了

某大厂要裁员,办公室里大家表面上还在敲代码、开会、改需求,其实心里都在算日子。结果有个程序员来了一句:别的先不说,能不能让我熬过这个夏天。

因为天太热了,家里开空调电费顶不住。

Image

以前说大厂人焦虑,大家还觉得是不是年包太高了凡尔赛。现在你看,焦虑已经不是买不买房、跳不跳槽了,是能不能多蹭几个月公司的冷气。

打工人有时候真不是多贪,就想这个月工资正常发,下个月社保别断,夏天有个凉快地方坐着干活。

听起来很没出息,但这就是很多人的现实。挺心酸的。

今日面试题

数组里一堆数,要求找出所有 a + b + c = 0 的三元组。

这题最烦的地方不是三数相加,而是去重。 我看很多人第一次写,会直接三层循环怼上去:

for i
for j
for k

能不能跑?小数据当然能跑。 但数据一大,马上就开始磨机器,时间复杂度 O(n^3),这种代码我一般不会往下看,面试里也基本没救。

这题真正要盯住两个点:

一个是数组先排序。 另一个是固定一个数,剩下两个数用双指针往中间夹。

排序以后,数组从小到大排好,比如:

[-4, -1, -1, 0, 1, 2]

固定第一个数 nums[i]。

然后左指针放在 i + 1,右指针放在数组最后。

如果三个数加起来小于 0,说明左边这个数太小了,左指针往右挪。

如果三个数加起来大于 0,说明右边这个数太大了,右指针往左挪。

如果刚好等于 0,记录答案,然后两边都要继续挪,并且跳过重复值。

代码我会这么写:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

publicclassThreeSumChecker{

public List<List<Integer>> collectZeroTriples(int[] nums) {
        List<List<Integer>> triples = new ArrayList<>();
if (nums == null || nums.length < 3) {
return triples;
        }

        Arrays.sort(nums);

for (int fixed = 0; fixed < nums.length - 2; fixed++) {
if (nums[fixed] > 0) {
break;
            }

if (fixed > 0 && nums[fixed] == nums[fixed - 1]) {
continue;
            }

int left = fixed + 1;
int right = nums.length - 1;

while (left < right) {
int sum = nums[fixed] + nums[left] + nums[right];

if (sum == 0) {
                    triples.add(Arrays.asList(nums[fixed], nums[left], nums[right]));

int leftValue = nums[left];
int rightValue = nums[right];

while (left < right && nums[left] == leftValue) {
                        left++;
                    }
while (left < right && nums[right] == rightValue) {
                        right--;
                    }
continue;
                }

if (sum < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }

return triples;
    }
}

这里有几个地方不能随手改。

第一,fixed > 0 && nums[fixed] == nums[fixed - 1] 这个判断是给第一个数去重的。

比如数组里有两个 -1,如果不跳过第二个 -1,同样的三元组会被算两次。

第二,找到一组答案以后,不能只写:

left++;
right--;

这地方我以前见过不少 bug。

比如:

[-2, 0, 0, 0, 2, 2]

固定 -2,左边多个 0,右边多个 2。 如果不跳过重复值,结果里会塞进重复的 [-2, 0, 2]。

所以代码里我先把当前的 leftValue 和 rightValue 存下来,再用 while 一口气跳过去。这个写法不花哨,但不容易出脏数据。

第三,nums[fixed] > 0 可以直接 break。

因为数组已经排好序了。当前固定值都大于 0,后面的数只会更大,三个正数加起来不可能等于 0。继续扫就是浪费。

这题的时间复杂度是 O(n^2)。 排序是 O(n log n),外层一层循环,里面左右指针最多扫一遍,所以主要耗时还是 O(n^2)。

空间上不算返回结果,基本就是 O(1)。当然 Java 的排序内部实现会有一些额外开销,这个没必要在业务代码里硬抠。

三数之和看起来是数组题,其实考的是排序以后怎么减少无效枚举。 别一上来就三层循环,也别只顾着凑出答案,去重没处理干净,这题就等于没写完。