TwoSum 相關問題思路總結

  • 2019 年 10 月 4 日
  • 筆記

概述

TwoSum 作為 LeetCode 的第一題存在,想必大家應該對其並不陌生。如果僅僅是看這道題目本身,並不難,思想也特別的簡單。

但是關鍵問題在於,由這個問題演變出來的題目和思路比較多,而且存在著不少的細節問題,今天我們就借著具體的題目和思路來看看 TwoSum 還可以怎麼玩?

兩種思路

對於 TwoSum 類問題,總的來說有兩種大的方向,一種方向是藉助 Hash 表,另外一種是藉助排序,然後利用相向雙指針來解決問題,我們分別來看看:

1、Hash 表的做法

public int[] twoSum(int[] nums, int target) {      Map<Integer, Integer> remainValues = new HashMap<>();        for (int i = 0; i < nums.length; ++i) {          if (remainValues.containsKey(target - nums[i])) {              int anotherIndex = remainValues.get(target - nums[i]);                return new int[] {i, anotherIndex};          }            remainValues.put(nums[i], i);      }        return new int[] {-1, -1};  }  

思路很簡單,遍曆數組,每訪問一個元素,先判斷其配對的元素是否在 Hash 表中,如果在的話就說明我們找到了答案,將其輸出即可,如果沒有找到,就將當前的元素放入 Hash 表中,以方便後面的元素來配對。

這裡因為題目要求輸出元素在數組中的位置,所以用 HashMap 來存儲訪問過的元素和其對應的 index。

我們再來分析一下其時空複雜度。

  • 由於使用了 Hash 表,空間複雜度是 O(n) 的,另外就是最差的情況,數組中的每個元素都要遍歷到,因此時間複雜度也是 O(n)
  • 從時間上面來看,這個演算法肯定是最優的,這很好理解,你要在數組中尋找配對的答案,數組當中的數肯定都需要看一遍。

2、排序加雙指針的思路

public int[] twoSum(int[] n, int t) {      if (n == null || n.length == 0) {          return new int[0];      }        Arrays.sort(n);        int[] result = new int[2];        int l = 0, r = n.length - 1;      while (l < r) {          if (n[l] + n[r] == t) {              result[0] = n[l];              result[1] = n[r];              return result;          } else if (n[l] + n[r] < t) {              l++;          } else {              r--;          }      }        return new int[0];  }  

這種思路的前提是題目沒有要求我們必須輸出元素在數組中的位置,因為排序會改變元素在數組中的位置,這裡,我們輸出元素本身即可。

這裡的思路就是一頭一尾兩個指針。

  • 每次判斷的時候,我們將左右兩個指針指向的元素加起來的和與我們要找的 target 對比,如果比 target 小,也就是說明如果在左指針不變的情況下,左指針加上左右指針中間的任意一個元素都會比 target 小,這也說明左指針不可能是我們要找的答案,因此向右移動左指針。
  • 如果是加起來和我們要找的 target 對比,比 target 大,分析類似,這時需要將右指針向左移動。

我們來看看這裡的時間複雜度,因為做了排序這麼一個操作,其時間複雜度就會是 O(nlgn) ,對於空間複雜度來說,這裡並沒有使用額外的空間,因此空間複雜度是常數級的 O(1)

兩種方法都講完了,如果你有一些編程經驗的話,相信這些東西不難理解。

你有沒有想過這兩種方法分別比較適合什麼樣的情況呢,基於 TwoSum 問題,思考下面的變化:

  • 如果題目要求輸出所有可能的答案,該怎麼處理?
  • 如果題目要求輸出所有可能的答案,並且數組中有重複元素該怎麼處理?
  • 如果題目要求找到比 target 小/大 的配對該怎麼處理?
  • 如果要找出兩數之差等於 target 的配對,該如何進行?
  • TwoSum 的解題思路是否可以拓展到 TreeSum 或者更多的配對?

上面的問題可能有些你曾想過,有些沒有,那麼就讓我們帶著上述的問題來看看具體的例題,熟練地將上述兩種方法應用到實際的題目中去。

變形題目分析

題目描述

題目還是 TwoSum,但是這時需要你返回所有可能的情況(不重複),並且數組中允許重複元素的出現。

題目解析

首先我們需要思考的是,使用之前提到的兩種方法中的哪一種會比較好。

是不是兩種方法都可以,你分析一下會發現其實兩種方法都是可行的,和之前的 TwoSum 不一樣的是,這時當找到答案後,需要繼續尋找,而不是直接返回

但是由於數組中存在重複元素,因此兩種方法裡面都需要考慮去重的機制。

  • 對於 Hash 表的方法來說,其實你並不清楚之前是否添加過相同的答案,因此我們考慮使用一個集合去存儲答案,保證答案的不重複性;
  • 對於排序的方法來說,去重的方式有所不同,排完序後,相同的元素會挨在一起,對於之前考慮過的元素,我們只需要略過就行,指針的移動很好地保證了這一點。

這裡就展示排序實現的方法:

程式碼實現

public List<int[]> twoSum6(int[] nums, int target) {      if (nums == null || nums.length < 2)          return 0;        Arrays.sort(nums);        List<int[]> results = new ArrayList<>();        int left = 0, right = nums.length - 1;        while (left < right) {          int v = nums[left] + nums[right];          if (v == target) {              int[] result = {nums[left], nums[right]};              results.add(result);              left++; right--;                while (left < right && nums[right] == nums[right + 1]) {                  right--;              }                while (left < right && nums[left] == nums[left - 1]){                  left++;              }          } else if (v > target) {              right--;          } else {              left++;          }      }        return results;  }  

題目描述

題目來源於 LeetCode 上第 1099 號問題: 小於 K 的兩數之和。

給你一個整數數組 A 和一個整數 K,請在該數組中找出兩個元素,使它們的和小於 K 但儘可能地接近 K返回這兩個元素的和

如不存在這樣的兩個元素,請返回 -1

示例 1:

輸入:A = [34,23,1,24,75,33,54,8], K = 60  輸出:58  解釋:  34 和 24 相加得到 58,58 小於 60,滿足題意。  

示例 2:

輸入:A = [10,20,30], K = 15  輸出:-1  解釋:  我們無法找到和小於 15 的兩個元素。  

提示:

  1. 1 &lt;= A.length &lt;= 100
  2. 1 &lt;= A[i] &lt;= 1000
  3. 1 &lt;= K &lt;= 2000

題目解析

傳統的 TwoSum 都是要你找到等於 target 的配對,那麼如果說要找到 大於/小於 target 的配對呢?

這個時候 Hash 表的方法就很難 work 了,因為 Hash 表比較適合處理 等於 的情況 !

那麼就需要考慮如何使用排序加雙指針的方法來解決這個問題,這裡,題目是要求小於 target 的數量,我們還是按照之前的分析思路來分析。

如果說當前左右指針指向的元素的和大於或者等於 target,那麼勢必我們需要向左移動右指針,讓兩個元素的和儘可能地小,當前頭尾指針指向的元素和小於 target 的時候,這時我們需要記錄答案,雖然這道題目裡面沒提,如果說要記錄配對數量的話,這時並不是記錄一個答案,如果說當前左指針固定,除了當前的右指針指向的元素,在左指針和右指針之間的數都是滿足要求的,我們只需要加上這個區間的數量即可,當然如果數組中存在重複元素,那麼我們就需要按照之前的套路遍歷去重了,當然對於這道題來說,我們選擇滿足條件的最大值即可。

程式碼實現

public int twoSumLessThanK(int[] A, int K) {      if (A == null || A.length == 0) {          return -1;      }        Arrays.sort(A);        int l = 0, r = A.length - 1;      int result = Integer.MIN_VALUE;        while (l < r) {          if (A[l] + A[r] >= K) {              r--;          } else {              result = Math.max(result, A[l] + A[r]);              l++;          }      }        return result == Integer.MIN_VALUE ? -1 : result;  }  

題目描述

題目來源於 LeetCode 上第 170 號問題:兩數之和 III – 數據結構設計。

設計並實現一個 TwoSum 的類,使該類需要支援 addfind 的操作。

add 操作 – 對內部數據結構增加一個數。 find 操作 – 尋找內部數據結構中是否存在一對整數,使得兩數之和與給定的數相等。

示例 1:

add(1); add(3); add(5);  find(4) -> true  find(7) -> false  

示例 2:

add(3); add(1); add(2);  find(3) -> true  find(6) -> false  

題目解析

題目要求讓你實現一個數據結構,這個結構支援 「添加元素」 和 「TwoSum」 兩個功能。

這時你需要綜合兩種方法的優劣性來選擇。

首先是 Hash 表的方法,如果使用這個方法,我們不需要考慮太多的東西,元素來了直接扔進數組就行,也就是說 添加元素 操作只需要 O(1) 的時間複雜度就可以完成,但是 TwoSum 的完成需要額外 O(n) 的空間;

再來看看排序的方法,因為這裡插入元素我們需要保證元素有序,因此 添加元素 需要 O(n) 的時間,但是這裡 TwoSum 操作並不需要額外空間,綜合來考慮,因為 添加元素TwoSum 操作都會比較頻繁,因此 Hash 表的方法在時間上面更優。

程式碼實現

private Map<Integer, Integer> elements;  private int MAX_VALUE = Integer.MIN_VALUE;  private int MIN_VALUE = Integer.MAX_VALUE;  /** Initialize your data structure here. */  public TwoSum() {      elements = new HashMap<>();  }    /** Add the number to an internal data structure.. */  public void add(int number) {      elements.put(number, elements.getOrDefault(number, 0) + 1);      MAX_VALUE = Math.max(MAX_VALUE, number);      MIN_VALUE = Math.min(MIN_VALUE, number);  }    /** Find if there exists any pair of numbers which sum is equal to the value. */  public boolean find(int value) {      if (value < 2 * MIN_VALUE || value > 2 * MAX_VALUE) {          return false;      }        for (int i : elements.keySet()) {          if (i * 2 == value && elements.get(i) >= 2) {              return true;          } else if (i * 2 != value && elements.containsKey(value - i)) {              return true;          }      }        return false;  }  

題目描述

題目來源於 LeetCode 上第 15 號問題: 三數之和。

給定一個包含 n 個整數的數組 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?找出所有滿足條件且不重複的三元組。

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

例如, 給定數組 nums = [-1, 0, 1, 2, -1, -4],  滿足要求的三元組集合為:  [    [-1, 0, 1],    [-1, -1, 2]  ]  

題目解析

三數之和,Hash 表以及排序的思路都是可行的。

但是這裡涉及到去重,這裡比較推薦的做法是排序加雙指針。

思路其實比較直觀,確定一個元素,然後去找另外兩個元素,這麼一來就把 3Sum 轉變成了 2Sum 的問題,這裡我兩個方法都實現了一下,你可以進行參考。

程式碼實現

排序加雙指針

// sort + two pointers  public List<List<Integer>> threeSum(int[] nums) {      if (nums == null || nums.length < 3) {          return new ArrayList<>();      }        Arrays.sort(nums);        List<List<Integer>> results = new ArrayList<>();      for (int i = 0; i < nums.length - 2; ++i) {          if ((i != 0) && (nums[i] == nums[i - 1])) {              continue;          }            List<Integer> result = new ArrayList<>();            twoSum(results, nums, i + 1, -nums[i]);            result = null;      }        return results;  }    private void twoSum(List<List<Integer>> results,                      int[] nums,                      int startIndex,                      int target) {      int left = startIndex, right = nums.length - 1;      while (left < right) {          if (nums[left] + nums[right] > target) {              right--;          } else if (nums[left] + nums[right] < target) {              left++;          } else {              List<Integer> result = new ArrayList<>();                result.add(-target);              result.add(nums[left]);              result.add(nums[right]);                results.add(result);                left++; right--;                while ((left < right) && (nums[left - 1] == nums[left])) {                  left++;              }                while ((right > left) && (nums[right + 1] == nums[right])) {                  right--;              }          }      }  }  

哈希表

// HashSet  public List<List<Integer>> threeSum(int[] nums) {      if (nums == null || nums.length < 3) {          return new ArrayList<>();      }        Arrays.sort(nums);        Set<List<Integer>> resultsSet = new HashSet<>();      List<List<Integer>> results = new ArrayList<>();        for (int i = 0; i < nums.length - 2; ++i) {          if ((i != 0) && (nums[i - 1] == nums[i])) {              continue;          }            Set<Integer> existedValue = new HashSet<>();            for (int j = i + 1; j < nums.length; ++j) {              if (!existedValue.contains(nums[j])) {                  existedValue.add(-nums[j] - nums[i]);              } else {                  List<Integer> result = new ArrayList<>();                    Collections.addAll(result, nums[i], -nums[j] - nums[i], nums[j]);                    resultsSet.add(result);              }          }            existedValue = null;      }        results.addAll(resultsSet);        return results;  }  

題目描述

題目來源於 LeetCode 上第 611 號問題:有效三角形的個數。

給定一個包含非負整數的數組,你的任務是統計其中可以組成三角形三條邊的三元組個數。

示例 1:

輸入: [2,2,3,4]  輸出: 3  解釋:  有效的組合是:  2,3,4 (使用第一個 2)  2,3,4 (使用第二個 2)  2,2,3  

注意:

  • 數組長度不超過1000。
  • 數組裡整數的範圍為 [0, 1000]。

題目解析

題目要求選出三條邊,使得這三條邊能夠構成三角形,咋眼看上去這道題貌似和 TwoSum 沒啥關係。

但我們回顧一下中學時期學的東西,三邊構成三角形的條件是 任意兩邊之和大於第三邊,那是不是說我們需要把三條邊都組合配對考慮一下?其實不用,我們可以得出下面的結論

a < b < c && a + b > c => 三角形  

如果已知三條邊的大小順序,那麼其實我們只需要比較一次即可。

你再看看這是不是我們熟悉的 TwoSum 變種問題 – 如果題目要求找到比 target 小/大 的配對該怎麼處理?

這個時候我們從右往左選定 c ,然後使用 TwoSum 來找出 a 、b 即可,由於題目只要求輸出個數,那麼就按照之前講的思路,直接相加即可。

程式碼實現

public int triangleCount(int[] S) {      if (S == null || S.length == 0) {          return 0;      }        Arrays.sort(S);        int result = 0;        for (int i = S.length - 1; i >= 2; --i) {          int l = 0, r = i - 1;          while (l < r) {              if (S[i] < S[l] + S[r]) {  // S[i] < S[l] + S[r] && S[i] > S[r] > S[l]                  result += r - l;  //  直接加上可能的個數                  r--;              } else {                  l++;              }          }      }        return result;  }  

題目描述

題目來源於 LeetCode 上第 18 號問題: 四數之和。

給定一個包含 n 個整數的數組 nums 和一個目標值 target,判斷 nums 中是否存在四個元素 abcd ,使得 a + b + c + d 的值與 target 相等?找出所有滿足條件且不重複的四元組。

注意:

答案中不可以包含重複的四元組。

示例:

給定數組 nums = [1, 0, -1, 0, -2, 2],和 target = 0。    滿足要求的四元組集合為:  [    [-1,  0, 0, 1],    [-2, -1, 1, 2],    [-2,  0, 0, 2]  ]  

題目分析

4Sum 的思路和 3Sum 的思路是一樣的,只不過這時我們需要先將其轉換成 3Sum 來處理,其實就是比 3Sum 多了一次選擇。

程式碼實現

public List<List<Integer>> fourSum(int[] nums, int target) {      if (nums.length < 4) {          return new ArrayList<>();      }        Arrays.sort(nums);        List<List<Integer>> results = new ArrayList<>();        for (int i = 0; i < nums.length - 3; ++i) {          if (i != 0 && nums[i] == nums[i - 1]) {              continue;          }            List<List<Integer>> subResults = threeSum(nums, i + 1, target - nums[i]);            for (List<Integer> res : subResults) {              res.add(nums[i]);              results.add(res);          }      }        return results;  }    private List<List<Integer>> threeSum(int[] nums, int start, int target) {      List<List<Integer>> results = new ArrayList<>();        for (int i = start; i < nums.length - 2; ++i) {          if (i != start && nums[i] == nums[i - 1]) {              continue;          }            List<List<Integer>> res = twoSum(nums, i + 1, target - nums[i]);            for (List<Integer> r : res) {              r.add(nums[i]);              results.add(r);          }      }        return results;  }    private List<List<Integer>> twoSum(int[] nums, int start, int target) {      List<List<Integer>> results = new ArrayList<>();        int l = start, r = nums.length - 1;      while (l < r) {          if (nums[l] + nums[r] < target) {              l++;          } else if (nums[l] + nums[r] > target) {              r--;          } else {              List<Integer> result = new ArrayList<>();              result.add(nums[l]); result.add(nums[r]);              results.add(result);                r--; l++;              while (l < r && nums[r] == nums[r - 1]) {                  r--;              }                while (l < r && nums[l] == nums[l + 1]) {                  l++;              }          }      }        return results;  }  

題目描述

給定一個 target,求出兩數之差等於 target 的情況。

題目解析

如果使用 Hash 表的做法,對於傳統的 TwoSum,我們找到的答案是滿足等式:

num1 + num2 = target  

因此這個時候判斷的時候,我們只需要判斷 target - nums 在不在 Hash 表中即可,對於兩數之差的話,我們找到的答案是滿足等式:

num1 - num2 = target or num2 - num1 = target  

在這種情況下,我們需要判斷 target + num 以及 num - target 即可,也就是相比之前,判斷條件不同且多了一個。

如果是使用排序加雙指針的方法呢?這其實是個打破思路局限的很好例子,這個時候我們需要用到同向雙指針了,兩個指針均從左向右移動,一前一後,用右邊的減去左邊的差值來和 target 做比較,如果小了,移動右指針,大了,移動左指針,等於,輸出答案。

程式碼實現

public int[] twoDiff(int[] n, int t) {      if (n == null || n.length == 0) {          return new int[0];      }        Arrays.sort(n);        int[] result = new int[2];        int l = 0, r = 1;      while (r < n.length) {          if (l == r) {              r++;          }            if (n[r] - n[l] == t) {              result[0] = n[l];              result[1] = n[r];              return result;          } else if (n[r] - n[l] < t) {              r++;          } else {              l++;          }      }        return new int[0];  }  

總結

TwoSum 相關的問題就分析到這裡,非常有趣的一點是,TwoSum 不僅可以擴展成 3Sum,4Sum,它也可以擴展為 KSum, 但是這裡就需要用到動態規劃的知識了,暫不在這裡討論。希望這次分享的內容對你有所幫助