青铜三人行之三数之和

先说一个消息,为了方便互相交流学习,青铜三人行建了个微信群,感兴趣的伙伴可以扫码加下面的小助手抱你入群哦!
青铜三人行小助手

哈喽~每周一题,代码无敌。欢迎各位继续观看「青铜三人行」的刷题现场。

三数之和

青铜三人行——每周一题@三数之和

力扣题目

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

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

例如
// 给定数组 
nums = [-1, 0, 1, 2, -1, -4],

//满足要求的三元组集合为:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

最初的解法

Helen 拿到题目,心想这道题岂不是如同上周的“两数之和”一般?无非就是多加了一个数而已。按照思路,首先暴力举出所有满足条件的三个数,再去重即可,写出了如下代码:

var threeSum = function (nums) {
    const results  = [];
    for(i=0;i<nums.length;i++) {
        for(j=i+1;j<nums.length;j++) {
            for(k=j+1;k<nums.length;k++){
                if(nums[i]+nums[j] +nums[k] === 0) {
                    const strResult = [nums[i], nums[j], nums[k]].sort((a,b) => a- b).join(','); // 转换成字符串方便去重
                    results.push(strResult);
                } 
            }
        }
    }
    return Array.from(new Set(results)).map(str => str.split(','));
}

拿入测试用例执行,结果正确 :
在这里插入图片描述

于是提交,结果被现实狠狠打脸… :
在这里插入图片描述

排序解法

纳尼?这道题居然有时间限制…太阴险了吧… 看样子传统的暴力破解法,在三重循环之下,时间复杂度到达了 O(n³),时间消耗应该是远远超过了题设。

看样子想解出这道题,至少要“消灭”掉其中的一重循环。Helen 找来书香一起讨论,两人细细品味题目,发现题目要求:a+b+c == 0 ,那说明这三个在数组中的数,除开三个数都为0 的情况,必然有正有负,有大有小。

换言之,如果给定一个“最小”的数,我们只需要在比这个数“大”的剩余数组里找出"其他"两个数,看看它们加起来的结果。如果等于0,则加入结果,如果大于0,则设法调整“其他两数”,使其和边小。若小于0,则设法使“其他两数”之和变大。

而在有序数组中,调整两数相加之和的大小是只需要一次循环就可以做到的,如此一来,我们似乎就可以在 O(n²) 的时间复杂度中就可以完成题设了:

var threeSum = function (nums) {
    const funcSeq = (a, b) => a - b;
    const sortedNums = nums.sort(funcSeq);
    const length = sortedNums.length;
    const result = [];
    for (let i = 0; i < length; i++) {
        let num = sortedNums[i];
        let lIndex = i + 1;
        let rIndex = length - 1;
        while (lIndex < rIndex) {
            let lNum = sortedNums[lIndex];
            let rNum = sortedNums[rIndex];
            if (lNum + num + rNum === 0) {
                result.push([lNum, num, rNum].sort(funcSeq).join(','));
                rIndex -= 1;
                lIndex += 1;
            } else if (lNum + num + rNum < 0) {
                lIndex += 1;
            } else if (lNum + num + rNum > 0) {
                rIndex -= 1
            }
        }
    }
    return Array.from(new Set(result)).map(str => str.split(','));
};

然而在提交时,遇到了一个诡异的测试用例,导致还是超时了 :

居然还有这么奇葩的测试用例!大量的 0 构成的数组。还好这并没有难倒 Helen, 既然题设里要求没有重复的三元组,那么加上了一个跳过重复元素的条件就好了:

var threeSum = function (nums) {
    const funcSeq = (a, b) => a - b;
    const sortedNums = nums.sort(funcSeq);
    const length = sortedNums.length;
    const result = [];
    for (let i = 0; i < length; i++) {
        let num = sortedNums[i];
        if (num === sortedNums[i - 1]) continue;
        let lIndex = i + 1;
        let rIndex = length - 1;
        while (lIndex < rIndex) {
            let lNum = sortedNums[lIndex];
            let rNum = sortedNums[rIndex];
            if (lNum + num + rNum === 0) {
                result.push([lNum, num, rNum].sort(funcSeq).join(','));
                rIndex -= 1;
                lIndex += 1;
            } else if (lNum + num + rNum < 0) {
                lIndex += 1;
            } else if (lNum + num + rNum > 0) {
                rIndex -= 1
            }
        }
    }
    return Array.from(new Set(result)).map(str => str.split(','));
};

提交,代码终于顺利通过啦 :

优化

看到解题终于通过,大家欢欣鼓舞,也打开了更多的思路。书香发现,既然要相加等于0,那么除开全为0的情况,必然结果里有正有负。换言之,第一层循环选取的数字,只需要遍历“非正数”的部分就好,于是加了个条件尝试了一番:

var threeSum = function (nums) {
    const funcSeq = (a, b) => a - b;
    const sortedNums = nums.sort(funcSeq);
    const length = sortedNums.length;
    const result = [];
    for (let i = 0; i < length; i++) {
        let num = sortedNums[i];
        if (num > 0) break;
        if (num === sortedNums[i - 1]) continue;
        let lIndex = i + 1;
        let rIndex = length - 1;
        while (lIndex < rIndex) {
            let lNum = sortedNums[lIndex];
            let rNum = sortedNums[rIndex];
            if (lNum + num + rNum === 0) {
                result.push([lNum, num, rNum].sort(funcSeq).join(','));
                rIndex -= 1;
                lIndex += 1;
            } else if (lNum + num + rNum < 0) {
                lIndex += 1;
            } else if (lNum + num + rNum > 0) {
                rIndex -= 1
            }
        }
    }
    return Array.from(new Set(result)).map(str => str.split(','));
};

而 Helen 则从“去重”这一部分上进行了优化,节省了转化成字符串,再用 Set 等数据结构去重带来的额外开销:

var threeSum = function (nums) {
    const funcSeq = (a, b) => a - b;
    const sortedNums = nums.sort(funcSeq);
    const length = sortedNums.length;
    const result = [];
    for (let i = 0; i < length; i++) {
        let num = sortedNums[i];
        if (num > 0) break;
        if (num === sortedNums[i - 1]) continue;
        let lIndex = i + 1;
        let rIndex = length - 1;
        while (lIndex < rIndex) {
            let lNum = sortedNums[lIndex];
            let rNum = sortedNums[rIndex];
            if (lNum + num + rNum === 0) {
                result.push([lNum, num, rNum]);
                while(lIndex < rIndex && sortedNums[lIndex] === sortedNums[lIndex + 1]) { 
                    lIndex++;
                }
                while(rIndex > lIndex && sortedNums[rIndex] === sortedNums[rIndex - 1]) { 
                    rIndex--;
                }
                rIndex -= 1;
                lIndex += 1;
            } else if (lNum + num + rNum < 0) {
                lIndex += 1;
            } else if (lNum + num + rNum > 0) {
                rIndex -= 1
            }
        }
    }
    return result;
};

而优化之后的结果也是相当理想:

extra

最后,我们照例贴上曾大师的 go 语言代码:

func threeSum(nums []int) [][]int {

	result := [][]int{}

    var keyCountMap map[int]int /*创建集合 */
    keyCountMap = make(map[int]int,len(nums))

    for i := 0; i < len(nums); i++ {
        
        count, ok := keyCountMap [nums[i]] 
        if ok {
            keyCountMap[nums[i]]=count+1;
        }else{
             keyCountMap[nums[i]]=1;
        }
    }

    newNums := make([]int, 0, len(keyCountMap))

    for keyi := range keyCountMap {
        newNums = append(newNums, keyi)
        if keyCountMap[keyi] > 1{
            if keyi==0 {
                if(keyCountMap[keyi] > 2){
                    result = append(result, append([]int{}, 0, 0, 0)) 
                }
                continue
            }
            var remain= 0 - keyi * 2
            _, ok := keyCountMap [remain] 
            if ok {               
                result = append(result, append([]int{}, keyi, keyi, remain))
            }

        }
    }

    for i := 0; i < len(newNums); i++ {
        for j := i+1; j < len(newNums); j++ {
            var remain = 0-(newNums[i]+newNums[j])
            if remain == newNums[i] || remain == newNums[j] {
                continue
            }
            _, ok := keyCountMap [remain] 
            if ok {  

                var b1 bool = true

                for k:=0;k<len(result);k++ {
                    if(newNums[i] == result[k][0]){
                        if(newNums[j] == result[k][1] || remain == result[k][1]){
                            b1 = false
                            break
                        }

                    }else if newNums[j] == result[k][0] {
                        if(newNums[i] == result[k][1] || remain == result[k][1]){
                            b1 = false
                            break
                        }

                    }else if remain == result[k][0] {
                        if(newNums[i] == result[k][1] || newNums[j] == result[k][1]){
                            b1 = false
                            break
                        }
                    }
                    
                }

                if b1 {
                    result = append(result, append([]int{}, newNums[i], newNums[j], remain))
                }
                
            }
       
        }
    }
    
    return result;
}

在这里,他另辟蹊径,采用了类似上周“两数之和”的题目解法,利用空间换时间,将数组转成 map 形式进行查找。同样通过了题目:

在这里,提个小问题:既然在“三数之和”可以参考“两数之和”的转换成 map 解题的方法,那在“两数之和”中,能不能参考上述“先排序,比较大小查找”的方法呢?

结尾

这周的题目难度上升为了“中等”,随着难度的上升,在解题上也无法完全做到完美。如果你有更好的思路,欢迎通过 [email protected] 邮箱联系我们~

下周见!


三人行

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章