經典算法題之 n 數之和問題


1、 兩數之和

給定一個整數數組 nums 和一個整數目標值 target,請你在該數組中找出 和為目標值 target  的那 兩個 整數,並返回它們的數組下標。

你可以假設每種輸入只會對應一個答案。但是,數組中同一個元素在答案里不能重復出現。

你可以按任意順序返回答案。

方法一:暴力枚舉

思路及算法

最容易想到的方法是枚舉數組中的每一個數 x,尋找數組中是否存在 target - x。

當我們使用遍歷整個數組的方式尋找 target - x 時,需要注意到每一個位於 x 之前的元素都已經和 x 匹配過,因此不需要再進行匹配。而每一個元素不能被使用兩次,所以我們只需要在 x 后面的元素中尋找 target - x。

 

 1 class Solution {
 2     public int[] twoSum(int[] nums, int target) {
 3         int n = nums.length;
 4         for (int i = 0; i < n; ++i) {
 5             for (int j = i + 1; j < n; ++j) {
 6                 if (nums[i] + nums[j] == target) {
 7                     return new int[]{i, j};
 8                 }
 9             }
10         }
11         return new int[0];
12     }
13 }

復雜度分析

時間復雜度:O(N2),其中N 是數組中的元素數量。最壞情況下數組中任意兩個數都要被匹配一次。

空間復雜度:O(1)。

 

方法二:哈希表

思路及算法

注意到方法一的時間復雜度較高的原因是尋找 target - x 的時間復雜度過高。因此,我們需要一種更優秀的方法,能夠快速尋找數組中是否存在目標元素。如果存在,我們需要找出它的索引。

使用哈希表,可以將尋找 target - x 的時間復雜度降低到O(1)。

這樣我們創建一個哈希表,對於每一個 x,我們首先查詢哈希表中是否存在 target - x,然后將 x 插入到哈希表中,即可保證不會讓 x 和自己匹配。

 

 1 class Solution {
 2     public int[] twoSum(int[] nums, int target) {
 3         Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
 4         for (int i = 0; i < nums.length; ++i) {
 5             if (hashtable.containsKey(target - nums[i])) {
 6                 return new int[]{hashtable.get(target - nums[i]), i};
 7             }
 8             hashtable.put(nums[i], i);
 9         }
10         return new int[0];
11     }
12 }

復雜度分析

時間復雜度:O(N),其中N 是數組中的元素數量。對於每一個元素 x,我們可以O(1) 地尋找 target - x。

空間復雜度:O(N),其中N 是數組中的元素數量。主要為哈希表的開銷。

 

說明,這道題本身並不難,但是是這一類題目思想的基礎,暴力解法是通用的方法,但是當數據量很大的時候,就會存在時間開銷巨大的問題,因此,使用空間換時間,使用hash表將target - x的結果存儲。后續的題目都是這個思想

 

2、找出數組中兩數之和為指定值的所有整數對

問題描述

  給定一個整型數組(數組中的元素可重復),以及一個指定的值。打印出數組中兩數之和為指定值的 所有整數對

方法一:排序加雙指針

先將整型數組排序,排序之后定義兩個指針left和right。left指向已排序數組中的第一個元素,right指向已排序數組中的最后一個元素

將 arr[left] + arr[right] 與 給定的元素比較,若前者大,right--;若前者小,left++;若相等,則找到了一對整數之和為指定值的元素。

此方法采用了排序,排序的時間復雜度為O(NlogN),排序之后掃描整個數組求和比較的時間復雜度為O(N)。故總的時間復雜度為O(NlogN)。空間復雜度為O(1)

 1 public class ExpectSumOfTwoNumber {
 2 
 3     public static void expectSum_bySort(int[] arr, int expectSum) {
 4         List<List<Integer>> res = new ArrayList<>();
 5         if (arr == null || arr.length == 0) {
 6             return;
 7         }
 8         Arrays.sort(arr);
 9         int left = 0, right = arr.length - 1;
10 
11         while (left < right) {
12             if (arr[left] + arr[right] > expectSum) {
13                 right--;
14             } else if (arr[left] + arr[right] < expectSum) {
15                 left++;
16             } else//equal
17             {
18                 res.add(Arrays.asList(left, right));
19                 left++;
20                 right--;
21             }
22         }
23         for (List<Integer> l : res) {
24             System.out.println(l.get(0) + ":" + arr[l.get((0))] + ", " + l.get(1) + ":" + arr[l.get((1))]);
25         }
26     }
27 }

 

方法二、哈希表(注意是有問題的)

依次遍歷整型數組,對整型數組中的每一個元素,求解它的suplement(expectedSum-arr[i]).suplement就是指定的值減去該數組元素。

如果該元素的 suplement不在HashSet中,則將該元素添加到HashSet。

如果該元素的suplement在HashSet中,說明已經找到了一對整數之和為指定值的元素。

該方法使用了HashSet,故空間復雜度為O(N),由於只需要掃描一遍整型數組,故時間復雜度為O(N)

 1 public class ExpectSumOfTwoNumber {
 2 
 3     public static void expectSum_bySet(int[] arr, int expectSum) {
 4         List<List<Integer>> res = new ArrayList<>();
 5         if (arr == null || arr.length == 0) {
 6             return;
 7         }
 8         HashSet<Integer> intSets = new HashSet<Integer>(arr.length);
 9 
10         Integer suplement;
11         for( int i=0; i<arr.length; i++ ){
12             suplement = expectSum - arr[i];
13             if (!intSets.contains(suplement)) {
14                 intSets.add(arr[i);
15             }
16             else{
17                 res.add(Arrays.asList(suplement, arr[i]));
18                 intSets.remove(suplement);
19             }
20         }
21         for (List<Integer> l: res) {
22             System.out.println(l.get(0) +", "+ l.get(1));
23         }
24     }
25 
26     public static void main(String[] args) {
27         int[] arr = {2, 7, 4, 9, 3};
28         int expectSum = 11;
29         expectSum_bySet(arr, expectSum);
30         System.out.println("----------------");
31         int[] arr2 = {3, 7, 9, 1, 2, 8, 5, 6, 10, 5};
32         int expectSum2 = 10;
33         expectSum_bySet(arr2, expectSum2);
34         System.out.println("----------------");
35         int[] arr3 = {2, 5, 1, 5, 5, 6, 3, 3};
36         int expectSum3 = 8;
37         expectSum_bySet(arr3, expectSum3);
38     }
39 }
40 
41 ------------------------------------------------------------------------
42 >>>
43 2, 9
44 ----------------
45 1, 9
46 5, 5
47 4, 6
48 0, 10
49 ----------------
50 3, 5
51 2, 6

 

可以看到,對於第三個測試用例,當數據出現重復時,結果會出現問題,原因是set會將相同數據去重,解決方法是用list代替set,注意remove函數的重載

 1 public class ExpectSumOfTwoNumber {
 2 
 3     public static void expectSum_bySet(int[] arr, int expectSum) {
 4         List<List<Integer>> res = new ArrayList<>();
 5         if (arr == null || arr.length == 0) {
 6             return;
 7         }
 8         List<Integer> intList = new ArrayList<>();
 9 
10         Integer suplement;
11         for( int i=0; i<arr.length; i++ ){
12             suplement = expectSum - arr[i];
13             if (!intList.contains(suplement)) {
14                 intList.add(arr[i]);
15             }
16             else{
17                 res.add(Arrays.asList(suplement, arr[i]));
18                 // 注意,此處remove的是值,由於remove函數重載的問題,如果使用int suplement, 
19                 //  remove函數會刪除指定位置的index,可能造成數組越界
20                 intList.remove(suplement);
21             }
22         }
23         for (List<Integer> l: res) {
24             System.out.println(l.get(0) +", "+ l.get(1));
25         }
26     }
27 
28     public static void main(String[] args) {
29         int[] arr = {2, 7, 4, 9, 3};
30         int expectSum = 11;
31         expectSum_bySet(arr, expectSum);
32         System.out.println("----------------");
33         int[] arr2 = {3, 7, 9, 1, 2, 8, 5, 6, 10, 5};
34         int expectSum2 = 10;
35         expectSum_bySet(arr2, expectSum2);
36         System.out.println("----------------");
37         int[] arr3 = {2, 5, 1, 5, 5, 6, 3, 3};
38         int expectSum3 = 8;
39         expectSum_bySet(arr3, expectSum3);
40     }
41 }
42 
43 ------------------------------------------------------------------------
44 >>>
45 7, 4
46 2, 9
47 ----------------
48 3, 7
49 9, 1
50 2, 8
51 5, 5
52 ----------------
53 2, 6
54 5, 3
55 5, 3

 

 

3、三數之和

給你一個包含 n 個整數的數組 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?請你找出所有和為 0 且不重復的三元組。

注意:答案中不可以包含重復的三元組。

 

示例 1:

輸入:nums = [-1,0,1,2,-1,-4]
輸出:[[-1,-1,2],[-1,0,1]]
示例 2:

輸入:nums = []
輸出:[]

排序加雙指針

 1 class Solution {
 2     public List<List<Integer>> threeSum(int[] nums) {
 3         int n = nums.length;
 4         Arrays.sort(nums);
 5         List<List<Integer>> ans = new ArrayList<List<Integer>>();
 6         // 枚舉 a
 7         for (int first = 0; first < n; ++first) {
 8             // 需要和上一次枚舉的數不相同
 9             if (first > 0 && nums[first] == nums[first - 1]) {
10                 continue;
11             }
12             // c 對應的指針初始指向數組的最右端
13             int third = n - 1;
14             int target = -nums[first];
15             // 枚舉 b
16             for (int second = first + 1; second < n; ++second) {
17                 // 需要和上一次枚舉的數不相同
18                 if (second > first + 1 && nums[second] == nums[second - 1]) {
19                     continue;
20                 }
21                 // 需要保證 b 的指針在 c 的指針的左側
22                 while (second < third && nums[second] + nums[third] > target) {
23                     --third;
24                 }
25                 // 如果指針重合,隨着 b 后續的增加
26                 // 就不會有滿足 a+b+c=0 並且 b<c 的 c 了,可以退出循環
27                 if (second == third) {
28                     break;
29                 }
30                 if (nums[second] + nums[third] == target) {
31                     List<Integer> list = new ArrayList<Integer>();
32                     list.add(nums[first]);
33                     list.add(nums[second]);
34                     list.add(nums[third]);
35                     ans.add(list);
36                 }
37             }
38         }
39         return ans;
40     }
41 }

 

4、四數之和

給你一個由 n 個整數組成的數組 nums ,和一個目標值 target 。請你找出並返回滿足下述全部條件且不重復的四元組 [nums[a], nums[b], nums[c], nums[d]] (若兩個四元組元素一一對應,則認為兩個四元組重復):

0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意順序 返回答案 。

 

示例 1:

輸入:nums = [1,0,-1,0,-2,2], target = 0
輸出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:

輸入:nums = [2,2,2,2,2], target = 8
輸出:[[2,2,2,2]]

排序加雙指針

思路與算法

最朴素的方法是使用四重循環枚舉所有的四元組,然后使用哈希表進行去重操作,得到不包含重復四元組的最終答案。假設數組的長度是n,則該方法中,枚舉的時間復雜度為O(n4),去重操作的時間復雜度和空間復雜度也很高,因此需要換一種思路。

為了避免枚舉到重復四元組,則需要保證每一重循環枚舉到的元素不小於其上一重循環枚舉到的元素,且在同一重循環中不能多次枚舉到相同的元素。

為了實現上述要求,可以對數組進行排序,並且在循環過程中遵循以下兩點:

  • 每一種循環枚舉到的下標必須大於上一重循環枚舉到的下標;
  • 同一重循環中,如果當前元素與上一個元素相同,則跳過當前元素。

使用上述方法,可以避免枚舉到重復四元組,但是由於仍使用四重循環,時間復雜度仍是O(n4)。注意到數組已經被排序,因此可以使用雙指針的方法去掉一重循環。

使用兩重循環分別枚舉前兩個數,然后在兩重循環枚舉到的數之后使用雙指針枚舉剩下的兩個數。假設兩重循環枚舉到的前兩個數分別位於下標i和j,其中i<j。初始時,左右指針分別指向下標j+1 和下標n−1。

每次計算四個數的和,並進行如下操作:

  • 如果和等於target,則將枚舉到的四個數加到答案中,然后將左指針右移直到遇到不同的數,將右指針左移直到遇到不同的數;
  • 如果和小於target,則將左指針右移一位;
  • 如果和大於target,則將右指針左移一位。
  • 使用雙指針枚舉剩下的兩個數的時間復雜度是O(n),因此總時間復雜度是O(n3),低於O(n4)

 

具體實現時,還可以進行一些剪枝操作:

  • 在確定第一個數之后,如果nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target,說明此時剩下的三個數無論取什么值,四數之和一定大於target,因此退出第一重循環;
  • 在確定第一個數之后,如果nums[i]+nums[n−3]+nums[n−2]+nums[n−1]<target,說明此時剩下的三個數無論取什么值,四數之和一定小於target,因此第一重循環直接進入下一輪,枚舉ums[i+1];
  • 在確定前兩個數之后,如果nums[i]+nums[j]+nums[j+1]+nums[j+2]>target,說明此時剩下的兩個數無論取什么值,四數之和一定大於target,因此退出第二重循環;
  • 在確定前兩個數之后,如果nums[i]+nums[j]+nums[n−2]+nums[n−1]<target,說明此時剩下的兩個數無論取什么值,四數之和一定小於target,因此第二重循環直接進入下一輪,枚舉nums[j+1]。

 

 

 1 class Solution {
 2     public List<List<Integer>> fourSum(int[] nums, int target) {
 3         List<List<Integer>> quadruplets = new ArrayList<List<Integer>>();
 4         if (nums == null || nums.length < 4) {
 5             return quadruplets;
 6         }
 7         Arrays.sort(nums);
 8         int length = nums.length;
 9         for (int i = 0; i < length - 3; i++) {
10             if (i > 0 && nums[i] == nums[i - 1]) {
11                 continue;
12             }
13             if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
14                 break;
15             }
16             if ((long) nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) {
17                 continue;
18             }
19             for (int j = i + 1; j < length - 2; j++) {
20                 if (j > i + 1 && nums[j] == nums[j - 1]) {
21                     continue;
22                 }
23                 if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
24                     break;
25                 }
26                 if ((long) nums[i] + nums[j] + nums[length - 2] + nums[length - 1] < target) {
27                     continue;
28                 }
29                 int left = j + 1, right = length - 1;
30                 while (left < right) {
31                     int sum = nums[i] + nums[j] + nums[left] + nums[right];
32                     if (sum == target) {
33                         quadruplets.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
34                         while (left < right && nums[left] == nums[left + 1]) {
35                             left++;
36                         }
37                         left++;
38                         while (left < right && nums[right] == nums[right - 1]) {
39                             right--;
40                         }
41                         right--;
42                     } else if (sum < target) {
43                         left++;
44                     } else {
45                         right--;
46                     }
47                 }
48             }
49         }
50         return quadruplets;
51     }
52 }

 

 

 

5、統計特殊四元組

 

給你一個 下標從 0 開始 的整數數組 nums ,返回滿足下述條件的 不同 四元組 (a, b, c, d) 的 數目 :

nums[a] + nums[b] + nums[c] == nums[d] ,且
a < b < c < d

示例 1:

輸入:nums = [1,2,3,6]
輸出:1
解釋:滿足要求的唯一一個四元組是 (0, 1, 2, 3) 因為 1 + 2 + 3 == 6 。
示例 2:

輸入:nums = [3,3,6,4,5]
輸出:0
解釋:[3,3,6,4,5] 中不存在滿足要求的四元組。
示例 3:

輸入:nums = [1,1,1,3,5]
輸出:4
解釋:滿足要求的 4 個四元組如下:
- (0, 1, 2, 3): 1 + 1 + 1 == 3
- (0, 1, 3, 4): 1 + 1 + 3 == 5
- (0, 2, 3, 4): 1 + 1 + 3 == 5
- (1, 2, 3, 4): 1 + 1 + 3 == 5

方法一:直接枚舉

思路與算法

最簡單的方法是直接枚舉四個下標 a,b,c,da,b,c,d 並進行判斷。

 1 class Solution {
 2     public int countQuadruplets(int[] nums) {
 3         int n = nums.length;
 4         int ans = 0;
 5         for (int a = 0; a < n; ++a) {
 6             for (int b = a + 1; b < n; ++b) {
 7                 for (int c = b + 1; c < n; ++c) {
 8                     for (int d = c + 1; d < n; ++d) {
 9                         if (nums[a] + nums[b] + nums[c] == nums[d]) {
10                             ++ans;
11                         }
12                     }
13                 }
14             }
15         }
16         return ans;
17     }
18 }

復雜度分析

時間復雜度:O(n4)

空間復雜度:O(1)。


方法二:使用哈希表存儲nums[d]

思路與算法

如果我們已經枚舉了前三個下標a,b,c,那么就已經知道了等式左側
nums[a]+nums[b]+nums[c] 的值,即為nums[d] 的值。對於下標 d 而言,它的取值范圍是 c<d<n,那么我們可以使用哈希表統計數組 nums[c+1] 到 nums[n−1] 中每個元素出現的次數。

這樣一來,我們就可以直接從哈希表中獲得滿足等式的 d 的個數,而不需要在[c+1,n−1] 的范圍內進行枚舉了。

細節

在枚舉前三個下標 a,b,c 時,我們可以先逆序枚舉 c。在 c 減小的過程中,d 的取值范圍是逐漸增大的:即從 c+1 減小到 c 時,d 的取值范圍中多了 c+1 這一項,而其余的項不變。因此我們只需要將 nums[c+1] 加入哈希表即可。

在這之后,我們就可以枚舉 a,b 並使用哈希表計算答案了。

class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length;
        int ans = 0;
        Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();
        for (int c = n - 2; c >= 2; --c) {
            cnt.put(nums[c + 1], cnt.getOrDefault(nums[c + 1], 0) + 1);
            for (int a = 0; a < c; ++a) {
                for (int b = a + 1; b < c; ++b) {
                    ans += cnt.getOrDefault(nums[a] + nums[b] + nums[c], 0);
                }
            }
        }
        return ans;
    }
}

 

方法三:使用哈希表存儲 nums[d]−nums[c]

思路與算法

我們將等式左側的 nums[c] 移動到右側,變為: nums[a]+nums[b]=nums[d]−nums[c]

如果我們已經枚舉了前兩個下標 a, b,那么就已經知道了等式左側 nums[a]+nums[b] 的值,即為 nums[d]−nums[c] 的值。對於下標 c, d 而言,它的取值范圍是 b<c<d<n,那么我們可以使用哈希表統計滿足上述要求的每一種 nums[d]−nums[c] 出現的次數。這樣一來,我們就可以直接從哈希表中獲得滿足等式的 c, d 的個數,而不需要在 [b+1,n−1] 的范圍內進行枚舉了。

細節

在枚舉前兩個下標 a, b 時,我們可以先逆序枚舉 b。在 b 減小的過程中, c 的取值范圍是逐漸增大的:即從 b+1 減小到 b 時,c 的取值范圍中多了 b+1 這一項,而其余的項不變。

因此我們只需要將所有滿足 c=b+1 且 d>c 的 c, d 對應的 nums[d]−nums[c] 加入哈希表即可。在這之后,我們就可以枚舉 a 並使用哈希表計算答案了。

 

 

 1     class Solution {
 2     public int countQuadruplets(int[] nums) {
 3         int n = nums.length;
 4         int ans = 0;
 5         Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();
 6         for (int b = n - 3; b >= 1; --b) {
 7             for (int d = b + 2; d < n; ++d) {
 8                 cnt.put(nums[d] - nums[b + 1], cnt.getOrDefault(nums[d] - nums[b + 1], 0) + 1);
 9             }
10             for (int a = 0; a < b; ++a) {
11                 ans += cnt.getOrDefault(nums[a] + nums[b], 0);
12             }
13         }
14         return ans;
15     }
16 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM