先說一個消息,爲了方便互相交流學習,青銅三人行建了個微信羣,感興趣的夥伴可以掃碼加下面的小助手抱你入羣哦!
哈嘍~每週一題,代碼無敵。歡迎各位繼續觀看「青銅三人行」的刷題現場。
三數之和
青銅三人行——每週一題@三數之和
力扣題目
給你一個包含 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] 郵箱聯繫我們~
下週見!
三人行