leetcode 三数之和

82 阅读3分钟

两数之和的升级版,题目链接

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

应该先写一个能解决问题的方法,如果超时了,后面再优化。最简单的暴力解法:先把数组排序,之和三层循环i,j,k 最后判断 nums[i] + nums[j] + nums[k] == 0,把答案记录到 Set 中,利用其自动去重的效果,只不过这里不能直接记录数组,因为数组是引用,所以应该记录字符串,把数字中间加个#等特殊字符,变成字符串。

function threeSum(nums: number[]): number[][] {
    let len=nums.length
    nums.sort((a,b)=>a-b)
    let set = new Set<string>()
    for(let i=0;i<len;i++){
        for(let j=i+1;j<len;j++){
            for(let k=j+1;k<len;k++){
                if(nums[i]+nums[j]+nums[k]===0){
                    let tmp=[nums[i],nums[j],nums[k]]
                    set.add(tmp.join('#'))
                }
            }
        }
    }
    return [...set].map(str=>str.split('#').map(i=>+i))
};

提交后,果不其然,这种O(n^3)的算法就是会超时的。根据题目给出的范围,可以大概算一下,- 3 <= nums.length <= 3000,3000^3是9*10^9,一般计算机1*10^8就会花费1秒的时间,一般题目时间限制也是1秒。

那我们如何进行优化呢?做过两数之和的同学应该能想到,k的那层循环没有必要,因为nums[i] + nums[j] + nums[k] == 0推导出nums[i] + nums[j] == -nums[k],k可以预先存在map中,这样查找就更快了(应该能到O(1)),所以能把时间复杂度降到O(n^2),上代码:

function threeSum(nums: number[]): number[][] {
    let len=nums.length
    let set = new Set<string>()
    let map = new Map<number,number>()
    nums.forEach((i,idx)=>{
        map.set(i,idx)
    })
    for(let i=0;i<len;i++){
        for(let j=i+1;j<len;j++){
            let numk=-(nums[i]+nums[j])
            if(map.get(numk)!=null){
                let kIdx=map.get(numk)
                if(kIdx!==i&&kIdx!==j){
                    let tmp=[nums[i],nums[j],numk]
                    tmp.sort((a,b)=>a-b)
                    set.add(tmp.join('#'))
                }
            }
        }
    }
    return [...set].map(str=>str.split('#').map(i=>+i))
};

注意:kIdx!==i&&kIdx!==j 要把 k 和 i j 对比,不能是相同的,而且要tmp.sort为了能顺利去重。

虽然题目通过了,但时间复杂度并不理想,只打败了5%,作为一名有追求的coder,应该继续优化算法。

但是想了半天,也没有想到O(nlogn)的解法,最终妥协,看了题解与其他人的代码,发现时间复杂度最低就是O(n^2)了,但是还有可优化的地方,比如用双指针代替 Set 和 Map 的使用:

function threeSum(nums: number[]): number[][] {
    const len=nums.length
    const res:number[][]=[]
    nums.sort((a,b)=>a-b)
    for(let i=0;i<len-2;i++){
        while(i&&nums[i]===nums[i-1]) i++
        let l=i+1,r=len-1
        while(l<r){
            let sum=nums[i]+nums[l]+nums[r]
            if(sum===0){
                res.push([nums[i],nums[l],nums[r]])
                while(nums[l]===nums[l+1]) l++
                while(nums[r]===nums[r-1]) r--
                l++
                r--
            } else if (sum<0){
                l++
            } else {
                r--
            }
        }
    }
    return res
};

这里为什么能用双指针呢?得解释一下,比如给你nums数组(这个数字必须是排序过的),值为[1,3,4,5],让你找出2个数,让他们的和是7,这就能用双指针去找,并且复杂度能保持在O(n)。

  1. 可以用左右指针 left 和 right 从两边向中间逼近,最开始left是0,right是3,nums[left]+nums[right]=6,小于目标值7。
  2. 那就应该left++,因为right是最后一个了,right减小的话就更不可能有答案了,所以left++会使他们的和变大。
  3. 这时,left是1, nums[left]+nums[right]=8 比7大了,所以要right--,这样才有可能是7
  4. 最终 left 是 1,right是2的时候就是答案了

这种解法其实也就是两数之和的O(n)解法。

这种解法理论上也是 O(n^2) 但实际上要比使用Map更快(使用Map耗时 3600ms,使用双指针耗时 172ms)