秒殺 2Sum 3Sum 4Sum 算法題

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

做爲一道標着 Easy 難度的題,它真的這麼簡單嗎?面試

我在以前的刷題視頻裏說過,你們刷題必定要吃透一類題,爲何有的人題目作着愈來愈少,有的人總以爲刷不完的題,就是由於沒有分類吃透。算法

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

2 Sum

這道題題意就是,給一個數組和一個目標值,讓你在這個數組裏找到兩個數,使得它倆之和等於這個目標值的。數據結構

好比題目中給的例子,目標值是 9,而後數組裏 2 + 7 = 9,因而返回 2 和 7 的下標。編輯器

方法一

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

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

因此,面試時必定要先問面試官,是但願優化時間仍是優化空間url

通常來講咱們追求優化時間,但你不能默認面試官也是這麼想的,有時候他就是想考你有沒有這個意識呢。spa

若是一個方法可以兼具優化時間和空間那就更好了,好比斐波那契數列這個問題中從遞歸到 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 相關的全部問題啦,若是有收穫的話,記得關注我哦~

相關文章
相關標籤/搜索