Leetcode之15. 3Sum (medium)
15. 3Sum (medium)
描述
Given an arraynums
of n integers, are there elements a, b, c innums
such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.
Note:
The solution set must not contain duplicate triplets.
Example:
Given array nums = [-1, 0, 1, 2, -1, -4], A solution set is: [ [-1, 0, 1], [-1, -1, 2] ]
分析
首先是最容易想到的暴力破解,通過三重遍歷陣列nums
,依次確定nums[i]
,nums[j]
,nums[k]
並計算三元素之和是否為0。這是最粗暴的解法,但是存在去重的問題,如[-1, 0, 1]和[0, 1, -1]這種情況。
其次,這個題目作為2Sum的進階題目,很容易聯想到將3Sum轉化為求target值為0 - nums[i]
,並在陣列剩餘元素中找出兩個元素之和為target的2Sum問題。但是同樣存在去重問題。
關於去重,由於是一個List
最後,以上兩種思路都存在去重問題,問題需要的是找出陣列中三個元素之和為0的所有組合。去重過程很明顯是和結果無關,但是卻非常麻煩,因此要優化演算法就要著眼於移除去重這個步驟。
在暴力破解的時候就該意識到邊界問題。
在做第一層迴圈時可以這樣寫:for(int i = 0; i < nums.length - 2; i++)
。即第一層迴圈的結束條件是nums.length - 2
,並不需要到nums.length
。
同樣第二層迴圈時:for(int j = i + 1; j < nums.length - 1; j++)
。開始索引不需要從0開始,可以直接從i + 1
開始,而結束為nums.length - 1
。
第三層:for(int k = j + 1; k < nums.length; k++)
。
可以看到在暴力破解的時候,我們已經有意識地通過邊界條件過濾掉一些情況,進行了初步優化。注意到陣列本身是無序的,所以在確定元素的時候難以界定當前遍歷元素是否已經被選中過。如果陣列是有序的,那麼三重遍歷的時候就可以有意識地跳過重複元素。到這裡已經對暴力破解的解題思路進行了優化,但是三重遍歷無疑是導致時間複雜度為O(N^3),這麼高的時間複雜度肯定是要被拋棄的。那麼該如何繼續優化呢?
嘗試優化思路二。首先使用排序解決去重問題。遍歷排序後的陣列,固定第一個元素為nums[i]
,接下來在索引位i + 1
至nums.length
之間找出兩個元素nums[j]
和nums[k]
,二者之和為0 - nums[i]
。固然這可以做遍歷兩次達到目的,相信基本上2Sum都是這樣完成的。但是針對一個有序陣列,夾逼法可以將這個過程的時間複雜度降為O(N)。因此,使用夾逼法找出剩餘兩個元素。ps,別忘了同時對2Sum使用夾逼法進行優化。
程式碼
public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> result = new LinkedList<List<Integer>>(); if (nums == null || nums.length < 3) { return result; } // 對陣列排序 Arrays.sort(nums); //固定第一個元素nums[i] for (int i = 0; i < nums.length - 2; i++) { //預設是從小到大的排序,所以當nums[i]大於0的時候,就可以結束 if (nums[i] > 0) { break; } //nums[i - 1] != nums[i]執行了去重,注意這裡在理解的時候要意識到此時操作的陣列已經是有序陣列 if (i == 0 || nums[i - 1] != nums[i]) { //使用加逼法 int j = i + 1; int k = nums.length - 1; while (j < k) { int sum = nums[i] + nums[j] + nums[k]; if (sum == 0) { result.add(Arrays.asList(nums[i], nums[j], nums[k])); } if (sum <= 0) { while (j < k && nums[j] == nums[++j]); } if(sum >= 0){ while (j < k && nums[k] == nums[--k]); } } } } return result; }
上面的程式碼是優化之後的程式碼,對於理解加逼的過程有點不便,下面是加逼的原始寫法:
while (j < k) { int target = 0 - nums[i]; if(target == (nums[j] + nums[k])){ result.add(Arrays.asList(nums[i], nums[j], nums[k])); j++; while(nums[j] == nums[j - 1] && j < k){ //去重,注意這是一個有序陣列 j++; } k--; while(nums[k] == nums[k + 1] && j < k){ //去重,注意這是一個有序陣列 k--; } }else if(target < (nums[j] + nums[k])){ k--; while(nums[k] == nums[k + 1] && j < k){ k--; } }else if(target > (nums[j] + nums[k])){ j++; while(nums[j] == nums[j - 1] && j < k){ j++; } } }