三数之和的优化艺术:从暴力到双指针的蜕变之路

86 阅读4分钟

三数之和的优化艺术:从暴力到双指针的蜕变之路

如何优雅地解决“三数之和”这个经典算法题?今天我们将深入探讨双指针解法的精妙之处,并分享一些实用的优化技巧。

一、问题重述:三数之和

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a、b、c,使得 a + b + c = 0?找出所有满足条件且不重复的三元组。

示例:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

二、解题思路演进

1. 暴力解法(三重循环)❌

// 时间复杂度 O(n³),不可接受
for(let i=0; i a - b);
    let ans = [];

    for(let i = 0; i < nums.length - 2; i++){
        let min = nums[i];
        let l = i + 1;
        let r = nums.length - 1;

        // 2. 跳过重复的起始元素
        if(i > 0 && min == nums[i - 1]){
            continue;
        }
        
        // 3. 剪枝优化
        if(min + nums[i+1] + nums[i+2] > 0){
            break; // 最小组合都大于0,后面的更大
        }
        if(min + nums[r] + nums[r-1] < 0){
            continue; // 最大组合都小于0,当前min太小
        }
        
        // 4. 双指针寻找另外两个数
        while(l < r){
            let sum = nums[l] + nums[r] + min;
            
            if(sum > 0){
                r--;
            } else if(sum < 0){
                l++;
            } else {
                // 找到解
                ans.push([min, nums[l], nums[r]]);
                l++;
                r--;
                
                // 5. 跳过重复元素
                while(l < r && nums[l] === nums[l-1]) l++;
                while(l < r && nums[r] === nums[r+1]) r--;
            }
        }
    }
    return ans;
};

四、关键技巧详解

技巧1:排序是双指针的基础

nums.sort((a, b) => a - b);
// 排序后:[-4, -1, -1, 0, 1, 2]

为什么需要排序?

  1. 方便去重
  2. 双指针移动有依据(和大了右指针左移,和小了左指针右移)
  3. 便于剪枝优化

技巧2:三重去重策略

去重是本题最大的难点!

// 1. 外层循环去重
if(i > 0 && nums[i] === nums[i-1]) continue;

// 2. 找到解后左指针去重
while(l < r && nums[l] === nums[l-1]) l++;

// 3. 找到解后右指针去重
while(l < r && nums[r] === nums[r+1]) r--;

技巧3:智能剪枝优化

这两处剪枝能大幅提升性能:

// 剪枝1:当前最小组合已经>0,后面不可能有解
// 示例:nums[i] = 1, nums[i+1] = 2, nums[i+2] = 3
// 1+2+3 = 6 > 0,后面的数字更大,直接break
if(min + nums[i+1] + nums[i+2] > 0){
    break;
}

// 剪枝2:当前最大组合仍然 0){
        r--;        // 和太大,需要减小
    } else if(sum < 0){
        l++;        // 和太小,需要增大
    } else {
        // 找到解
        // 注意:找到解后两个指针都要移动!
        l++;
        r--;
    }
}

五、算法复杂度分析

操作时间复杂度空间复杂度
排序O(n log n)O(1)
双指针遍历O(n²)O(1)
总体O(n²)O(1)

优化效果:

  • 原始暴力解法:O(n³)
  • 优化后:O(n²)
  • 在 n=3000 时,速度提升约 1000 倍!

六、实际测试与验证

// 测试用例
const testCases = [
    [-1, 0, 1, 2, -1, -4],
    [0, 0, 0, 0],
    [-2, 0, 1, 1, 2],
    [],
    [0, 1, 1]
];

testCases.forEach((nums, idx) => {
    console.log(`测试用例 ${idx + 1}:`, nums);
    console.log('结果:', threeSum(nums));
    console.log('---');
});

七、常见错误与陷阱

错误1:忘记排序

// ❌ 错误:未排序直接使用双指针
// ✅ 正确:必须先排序

错误2:去重逻辑不完整

// ❌ 只在外层去重,内层不去重
// 会导致 [[-1,-1,2],[-1,0,1],[-1,0,1]] 这样的重复结果

错误3:指针移动时机错误

// ❌ 找到解后只移动一个指针
// ✅ 必须同时移动两个指针

八、举一反三:四数之和

掌握了三数之和,四数之和就迎刃而解:

var fourSum = function(nums, target) {
    nums.sort((a, b) => a - b);
    let ans = [];
    
    for(let i = 0; i < nums.length - 3; i++){
        // 去重
        if(i > 0 && nums[i] === nums[i-1]) continue;
        
        for(let j = i + 1; j < nums.length - 2; j++){
            // 去重
            if(j > i + 1 && nums[j] === nums[j-1]) continue;
            
            let l = j + 1, r = nums.length - 1;
            
            while(l < r){
                const sum = nums[i] + nums[j] + nums[l] + nums[r];
                
                if(sum > target){
                    r--;
                } else if(sum < target){
                    l++;
                } else {
                    ans.push([nums[i], nums[j], nums[l], nums[r]]);
                    l++; r--;
                    
                    // 去重
                    while(l < r && nums[l] === nums[l-1]) l++;
                    while(l < r && nums[r] === nums[r+1]) r--;
                }
            }
        }
    }
    
    return ans;
};

九、总结与启示

核心思想:

  1. 排序是前提 - 为双指针和去重奠定基础
  2. 双指针是核心 - 将 O(n³) 降为 O(n²)
  3. 去重是关键 - 多层级去重保证结果唯一性
  4. 剪枝是优化 - 提前终止不可能的分支

算法哲学:

  • 好的算法不仅是解决问题,更是优雅地解决问题
  • 空间换时间,时间换空间,找到平衡点
  • 边界条件处理是区分普通和优秀程序员的标志

思考题:

  1. 如果要求返回三元组的索引而不是值,如何修改?
  2. 如果数组非常大(n>10^5),内存有限怎么办?
  3. 如何扩展到 k 数之和问题?

算法之美,在于其简洁与高效。 三数之和问题完美展示了如何通过巧妙的思路,将复杂问题简单化。掌握这种思维模式,你就能解决一大类"多个数之和"的问题了!


互动问题: 你还能想到哪些优化三数之和的方法?欢迎在评论区分享你的想法!