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
相關的所有問題啦,如果有收穫的話,記得關注我哦~