三数之和:从暴力到双指针的优雅优化

38 阅读3分钟

在算法面试中,“三数之和”是一道经典题目:给定一个整数数组,找出所有不重复的三元组,使其和为零。初看之下,最容易想到的是三层嵌套循环的暴力解法,时间复杂度高达 O(n³),在数据量稍大时便难以接受。而通过巧妙利用排序 + 双指针策略,我们可以将复杂度降至 O(n²),同时高效处理重复结果。

排序是优化的第一步

解决三数之和的关键在于有序性。通过对数组升序排列,我们不仅能提前终止无效搜索(如当前固定值已大于0,则后续不可能凑出和为0的组合),还能方便地跳过重复元素,避免结果重复。

nums.sort((a, b) => a - b);

JavaScript 的 sort 方法默认按字符串比较,因此需传入比较函数 (a, b) => a - b 实现数值升序。这一步的时间复杂度为 O(n log n),远低于后续的主逻辑开销。

固定一数,双指针夹逼

排序后,我们遍历数组,将每个元素 nums[i] 视为三元组的第一个数。若 nums[i] > 0,由于数组已升序,后续所有数均为正,三数之和不可能为零,可直接跳出循环。

for (let i = 0; i < nums.length; i++) {
  if (nums[i] > 0) break;
  // 跳过重复的起始值
  if (i > 0 && nums[i] === nums[i - 1]) continue;
}

为避免重复三元组,当 nums[i] 与前一个值相同时,直接跳过。这确保了每个“起点”只被处理一次。

双指针高效搜索

固定 nums[i] 后,问题转化为在剩余子数组中寻找两数之和等于 -nums[i]。此时,设置左指针 L = i + 1,右指针 R = nums.length - 1,进行夹逼搜索:

let L = i + 1, R = nums.length - 1;
while (L < R) {
  const sum = nums[i] + nums[L] + nums[R];
  if (sum === 0) {
    result.push([nums[i], nums[L], nums[R]]);
    // 跳过左右重复值
    while (L < R && nums[L] === nums[L + 1]) L++;
    while (L < R && nums[R] === nums[R - 1]) R--;
    L++; R--;
  } else if (sum < 0) {
    L++; // 和太小,左指针右移增大
  } else {
    R--; // 和太大,右指针左移减小
  }
}

当三数之和等于0时,将结果加入列表,并继续移动指针跳过重复值,防止生成相同三元组。若和小于0,说明需要更大的数,左指针右移;反之,右指针左移。这种双向逼近策略,使得每对 (L, R) 仅被访问一次,效率极高。

总结

三数之和问题展示了算法优化的核心思想:利用数据特性(有序性)减少冗余计算。通过排序预处理,将无序搜索转化为有序夹逼;通过双指针,将内层两重循环压缩为线性扫描;通过重复值跳过,保证结果唯一性。整个过程时间复杂度为 O(n²),空间复杂度 O(1)(不计输出),是处理此类“K 数之和”问题的标准范式。掌握这一模式,不仅能应对面试,也为解决更复杂的组合优化问题打下坚实基础。