两数之和的升级版,题目链接:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != 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)。
- 可以用左右指针 left 和 right 从两边向中间逼近,最开始left是0,right是3,
nums[left]+nums[right]=6,小于目标值7。 - 那就应该left++,因为right是最后一个了,right减小的话就更不可能有答案了,所以left++会使他们的和变大。
- 这时,left是1,
nums[left]+nums[right]=8比7大了,所以要right--,这样才有可能是7 - 最终 left 是 1,right是2的时候就是答案了
这种解法其实也就是两数之和的O(n)解法。
这种解法理论上也是 O(n^2) 但实际上要比使用Map更快(使用Map耗时 3600ms,使用双指针耗时 172ms)