秒殺 2Sum 3Sum 4Sum 演算法題

碼農田小齊發表於2020-08-20

2 Sum 這題是 Leetcode 的第一題,相信大部分小夥伴都聽過的吧。

作為一道標著 Easy 難度的題,它真的這麼簡單嗎?

我在之前的刷題視訊裡說過,大家刷題一定要吃透一類題,為什麼有的人題目做著越來越少,有的人總覺得刷不完的題,就是因為沒有分類吃透。

單純的追求做題數量是沒有意義的,Leetcode 的題目只會越來越多,就像高三時的模考試卷一樣做不完,但分類總結,學會解決問題的方式方法,才能遇到新題也不手足無措。

2 Sum

這道題題意就是,給一個陣列和一個目標值,讓你在這個陣列裡找到兩個數,使得它倆之和等於這個目標值的。

比如題目中給的例子,目標值是 9,然後陣列裡 2 + 7 = 9,於是返回 2 和 7 的下標。

方法一

在我多年前還不知道時空複雜度的時候,我想這還不簡單嘛,就每個組合挨個試一遍唄,也就是兩層迴圈。

後來我才知道,這樣時間複雜度是很高的,是 O(n^2);但另一方面,這種方法的空間複雜度最低,是 O(1)

所以,面試時一定要先問面試官,是希望優化時間還是優化空間

一般來說我們追求優化時間,但你不能預設面試官也是這麼想的,有時候他就是想考你有沒有這個意識呢。

如果一個方法能夠兼具優化時間和空間那就更好了,比如斐波那契數列這個問題中從遞迴到 DP 的優化,就是時間和空間的雙重優化,不清楚的同學後臺回覆「遞迴」快去補課~

我們來看下這個程式碼:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] + nums[j] == target) {
                    return new int[]{i, j};
                }
            }
        }
        return new int[]{-1, -1};
    }
}
  • 時間複雜度:O(n^2)
  • 空間複雜度:O(1)

喏,這速度不太行誒。

方法二

那在我學了 HashMap 這個資料結構之後呢,我又有了新的想法。

HashMap 或者 HashSet 的最大優勢就是能夠用 O(1) 的時間獲取到目標值,那麼是不是可以優化方法一的第二個迴圈呢?

有了這個思路,假設當前在看 x,那就是需要把 x 之前或者之後的數放在 HashSet 裡,然後看下 target - x 在不在這個 hashSet 裡,如果在的話,那就匹配成功~

誒這裡有個問題,這題要求返回這倆數的下標,可是 HashSet 裡的數是無序的...

那就用升級版——HashMap 嘛~~還不瞭解 HashMap 的原理的同學快去公眾號後臺回覆「HashMap」看文章啦。

HashMap 裡記錄下數值和它的 index 這樣匹配成功之後就可以順便得到 index 了。

這裡我們不需要提前記錄所有的值,只需要邊過陣列邊記錄就好了,為了防止重複,我們只在這個當前的數出現之前的陣列部分裡找另一個數。

總結一下,

  • HashMap 裡記錄的是下標 i 之前的所有出現過的數;
  • 對於每個 nums[i] ,我們先檢查 target - nums[i] 是否在這個 map 裡;
  • 如果在就直接返回了,如果不在就把當前 i 的資訊加進 map 裡。
class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(target - nums[i])) {
                res[0] = map.get(target - nums[i]);
                res[1] = i;
                return res;
            }
            map.put(nums[i], i);
        }
        return res;
    }
}
  • 時間複雜度:O(n)
  • 空間複雜度:O(n)

喏,速度提升至 beat 99.96%

擴充

這是最基本的 2 Sum 問題,這個題可以有太多的變種了:

  • 如果這個陣列裡有不止一組結果,要求返回所有組合,該怎麼做?

  • 如果這個陣列裡有重複元素,又該怎麼做?

  • 如果這個陣列是一個排好序了的陣列,那如何利用這個條件呢?- Leetcode 167

  • 如果不是陣列而是給一個 BST ,該怎麼在一棵樹上找這倆數呢?- Leetcode 653

...

這裡講一下排序陣列這道題,之後會在 BST 的文章裡會講 653 這題。

排序陣列

我們知道排序演算法中最快的也需要 O(nlogn),所以如果是一個 2 Sum 問題,那沒必要專門排序,因為排序會成為運算的瓶頸。

但如果題目給的就是個排好序了的陣列,那肯定要好好收著了呀!

因為當陣列是排好序的時候,我們可以進一步優化空間,達到 O(n) 的時間和 O(1) 的空間。

該怎麼利用排好序這個性質呢?

那就是說,在 x 右邊的數,都比 x 要大;在 x 左邊的數,都比 x 要小。

  • 如果 x + y > target,那麼就要 y 往左走,往小的方向走;

  • 如果 x + y < target,那麼就要 x 往右走,往大的方向走。

這也就是典型的 Two pointer 演算法,兩個指標相向而行的情況,我之後也會出文章詳細來講噠。

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int left = 0;
        int right = numbers.length - 1;
        while (left < right) {
            int sum = numbers[left] + numbers[right];
            if (sum == target) {
                return new int[]{left + 1, right + 1}; //Your returned answers are not zero-based.
            } else if (sum < target) {
                left ++;
            } else {
                right --;
            }
        }
        return new int[]{-1, -1};
    }
}

3 Sum

3 Sum 的問題其實就是一個 2 Sum 的升級版,因為 1 + 2 = 3 嘛。。

那就是外面一層迴圈,固定一個值,在剩下的陣列裡做 2 Sum 問題。

反正 3 Sum 怎麼著都得 O(n^2) ,就可以先排序,反正不在乎排序的這點時間了,這樣就可以用 Two pointer 來做了。

還需要注意的是,這道題返回的是數值,而非 index,所以它不需要重複的數值——The solution set must not contain duplicate triplets.

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i + 2 < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) {
                 // skip same result
                continue;
            }
            int j = i + 1;
            int k = nums.length - 1;  
            int target = -nums[i];
            while (j < k) {
                if (nums[j] + nums[k] == target) {
                    res.add(Arrays.asList(nums[i], nums[j], nums[k]));
                    j++;
                    k--;
                    while (j < k && nums[j] == nums[j - 1]) {
                        j++;  // skip same result
                    }
                    while (j < k && nums[k] == nums[k + 1]) {
                        k--;  // skip same result
                    }
                } else if (nums[j] + nums[k] > target) {
                    k--;
                } else {
                    j++;
                }
            }
        }
        return res;
    }
}

4 Sum

最後就是 4 Sum 問題啦。

這一題如果只是 O(n^3) 的解法沒什麼難的,因為就是在 3 Sum 的基礎上再加一層迴圈嘛。

但是如果在面試中只做出 O(n^3) 恐怕就過不了了哦?

這 4 個數,可以想成兩兩的 2 Sum,先把第一個 2 Sum 的結果存下來,然後在後續的陣列中做第二個 2 Sum,這樣就可以把時間降低到 O(n^2) 了。

這裡要注意的是,為了避免重複,也就是下圖的 nums[x] + nums[y] + nums[z] + nums[k] ,其實和 nums[z] + nums[k] + nums[x] + nums[y] 並沒有區別,所以我們要限制第二組的兩個數要在第一組的兩個數之後哦。

看下程式碼吧:

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
            Set<List<Integer>> set = new HashSet<>();
    Map<Integer, List<List<Integer>>> map = new HashMap<>();
    Arrays.sort(nums);
    // 先處理第一對,把它們的sum存下來
    for(int i = 0; i < nums.length - 3; i++) {
      for(int j = i + 1; j < nums.length - 2; j++) {
        int currSum = nums[i] + nums[j];
        List<List<Integer>> pairs = map.getOrDefault(currSum, new ArrayList<>());
        pairs.add(Arrays.asList(i, j));
        map.put(currSum, pairs);
      }
    }
    
    // 在其後做two sum
    for(int i = 2; i < nums.length - 1; i++) {
      for(int j = i + 1; j < nums.length; j++) {
        int currSum = nums[i] + nums[j];
        List<List<Integer>> prevPairs = map.get(target - currSum);
        if(prevPairs == null) {
            continue;
        }
        for(List<Integer> pair : prevPairs) {
          if(pair.get(1) < i) {
            set.add(Arrays.asList(nums[pair.get(0)], nums[pair.get(1)], nums[i], nums[j]));
          }
        }
       }
     }
     return new ArrayList<>(set);
    }
}

好啦,以上就是 2 Sum 相關的所有問題啦,如果有收穫的話,記得關注我哦~

相關文章