青銅三人行之三數之和

先說一個消息,爲了方便互相交流學習,青銅三人行建了個微信羣,感興趣的夥伴可以掃碼加下面的小助手抱你入羣哦!
青銅三人行小助手

哈嘍~每週一題,代碼無敵。歡迎各位繼續觀看「青銅三人行」的刷題現場。

三數之和

青銅三人行——每週一題@三數之和

力扣題目

給你一個包含 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] 郵箱聯繫我們~

下週見!


三人行

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