兩數之和(two sum)問題通用解法(map法和雙指針)

這類題目都是套路非常明顯的題目,掌握下述的題目後應該對這類問題沒有任何問題。

1. LeetCode01:two sum

題意

  • 給定數組和一個目標數,要求在數組中找到兩個數,其和等於目標數,返回該兩數的下標

思路1:map法

  • 建立從value->index的映射
  • 遍歷數組
    • 依次檢驗當前mapkey中是否包含target - num[i]
      • 如果包含,那麼說明一定存在nums[j] = target - nums[i]已經在map中,且一定j<i,那麼只需要通過targte - nums[i]map中找到下標j,那麼j和i就是結果
      • 如果不包含,則把當前的nums[i]和i添加到map中即可

代碼1:

 public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        if (nums == null || nums.length == 0){
            return null;
        }
        //0. num->index
        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;
    }

思路2:雙指針解法

  • 在有序數組中可以利用兩個相向雙指針,一個從左往右,一個從右往左的遍歷數組,然後求出題目要求(兩數之和等於/大於/小於/最接近於某個數)的答案即可
    • 所以雙指針解法的第一步永遠是排序
  • 如果題目要求返回的值是索引,此時如果直接對數組進行排序,那麼就會丟失掉題目給定數組中數組元素和索引的對應關係,此時通用的解決辦法是利用一個Pair來存儲value和index的對應關係,然後對Pairvalue進行排序
  • 當得到有序數組後,雙指針的邏輯就很簡單了(見代碼中的註釋部分)

代碼2

class Solution {
    class Pair{
        int val;
        int index;
         public Pair(int val, int index){
             this.val = val;
             this.index = index;
         }
    }

    public int[] twoSum(int[] nums, int target) {
        if (nums == null || nums.length == 0){
            return null;
        }
        int[] res = new int[2];

        Pair[] pairs = new Pair[nums.length];
        //0. 將數組元素值和index進行綁定
        for (int i = 0; i < nums.length; i++) {
            pairs[i] = new Pair(nums[i],i);
        }
        //1. 雙指針解法的第一步永遠是排序
        Arrays.sort(pairs,new Comparator<Pair>(){
            @Override
            public int compare(Pair o1, Pair o2) {
                return o1.val - o2.val;
            }
        });
        int left = 0;
        int right = nums.length - 1;

        while (left < right){
            if (pairs[left].val + pairs[right].val == target){
                res[0] = pairs[left].index;
                res[1] = pairs[right].index;
                return res;
            }else if (pairs[left].val + pairs[right].val > target){
                //2. 當前的最大值加上最小值 都還是大於目標值,說明最大值太大了
                right--;
            }else {
                //3.當前的最小值加上最大值 都還是小於目標值,說明最小值太小了
                left++;
            }
        }
        return null;
    }
}

小結

  • 上述兩種思路是解決兩數之和這類問題的通用解決手段

2. LintCode607. Two Sum III - Data structure design

題意

  • 實現一個數據結構,完成如下兩個功能
  • void add(num):可以增加給定的數據
  • boolean find(value):查找當前數據集中是否存在兩數之和等於value

思路

  • 首先,該數據結構需要存儲數據,在設計數據結構的時候,底層的數據存儲要麼用鏈表要麼用動態數組
  • 此題中並沒有說明要存儲數據的具體長度,那麼就只能用動態數組,在Java中最常用的就是list
  • 其次,第二個功能的實現,就是一個兩數之和問題,而且此題不要求返回兩數的索引,只需要判斷是否存在,那麼顯然考慮用map法,因爲完全不需要考慮索引,完全可以把map簡化爲用一個set來做判斷

代碼

class Solution {

    //0. 用list來存數據
    List<Integer> list = new ArrayList<>();

    public void add(int number) {
        // write your code here
        list.add(number);
    }


    public boolean find(int value) {
        // write your code here
        Set<Integer> set = new HashSet<>();

        for (Integer num : list) {

            if (set.contains(value - num)){
                return true;
            }
            set.add(num);
        }
        return false;
    }
}

3. 167. Two Sum II - Input array is sorted

題意

給定有序數組,和一目標數,在數組中找到兩數之和等於目標數,返回兩數在數組中的次序(注意不是基0的索引,所以最後需要+1)

思路

  • 是第1題的簡化版
  • 因爲已經有序了,不需要再排序,自然也不需要處理排序後數組值和索引無法對應的問題
  • 直接用雙指針來處理即可

代碼

  public static int[] twoSum(int[] A, int target) {

        int[] res = new int[2];
        int left = 0;
        int right = A.length - 1;

        while (left < right){

            if (A[left] + A[right] == target){
                res[0] = left + 1;
                res[1] = right + 1;
                return res;
            }else if (A[left] + A[right] > target){
                //1. 和大了 要減小
                right--;
            }else {
                //2. 和小了 要增大
                left++;
            }
        }
        return null;

    }

4. LintCode587.Two Sum - Unique pairs

此題是vip,無法提供鏈接

題意

  • 給定一個數組和目標數,找到數組中有多少組不同的元素對,其和都等於給定的目標數,返回不同的元素對的對數
  • 例如:
    • nums=[1,1,2,45,46,46],target = 47
    • 返回2
    • 因爲:1 + 46 = 47、2 + 45 = 47

思路

  • 仍然是兩數之和問題,且不用返回下標,所以無需考慮數值和下標的問題
  • 關鍵在於去重
  • 雙指針和去重的一個首要操作都是排序
  • 去重的一般手段就是利用while來找到不再重複的元素(具體見代碼和註釋)

代碼

 public int twoSum6(int[] nums, int target) {

        if (nums == null || nums.length == 0){
            return 0;
        }
        //0. 首先排序,因爲此題不需要返回索引,所以無需考慮數值與下標的關係
        //0.1 此處還有一個重要的目的在於爲後續去重做準備,(數組中去重問題一般都需要先排序)
        Arrays.sort(nums);
        int left = 0;
        int right = nums.length - 1;
        int cnt = 0;//計數

        while (left  < right){

            int v = nums[left] + nums[right];
            if (v == target){
                //1. 如果找到一對數,那麼肯定計數+1,指針移動
                cnt++;
                left++;
                right--;
                //2. 去重,爲了避免一對同樣的數的和再組成target
                while (left < right && nums[right] == nums[right+1]){
                    right--;
                }
                while (left < right && nums[left] == nums[left-1]){
                    left++;
                }
            }else if (v > target){
                //3. 大了 要減小
                right--;
            }else {
                //小了 要增大
                left++;
            }
        }
        return cnt;
        
    }

5. LeetCode15. 3Sum

題意

  • 給定數組,要求找到滿足a +b + c = 0的三元組,且無重複

思路

  • a + b + c = 0-> a +b = -c

  • 遍歷數組,將每一個元素當作-c = target

  • 然後在數組剩餘元素中尋找無重複的兩數之和等於target的組合

  • 那麼這時候問題就退化成了上面第4題

  • 同時要注意對target也要去重

    • 去重的一個通用的思路是用set
    • 當然數組中可以有更簡單的方式
    if (i > 0 && nums[i] == nums[i-1]){
                continue;
            }
    

代碼

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {

        List<List<Integer>> results = new ArrayList<>();
        List<Integer> item = new ArrayList<>();
        if (nums == null || nums.length < 3){
            return results;
        }

        //0. 先排序,無需返回index,所以直接排序即可
        Arrays.sort(nums);
        //1.遍歷nums,把每一個元素當作target,然後在數組剩下的部分尋找無重複的兩數之和等於target
        //  問題退化成了two sum
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            //2.1 避免重複的target

            if (i > 0 && nums[i] == nums[i-1]){
                continue;
            }
            //2.2 a + b + c = 0 -> a + b = -c
            int target = -nums[i];
            //2.3 保證數組中剩下的元素至少有兩個
            if (n - i - 1 >= 2) {
                twoSum(i+1,target,nums,results);

            }
        }
        return results;
    }

    private void twoSum(int startIndex,  int target, int[] nums, List<List<Integer>> results) {

        int left = startIndex;
        int right = nums.length - 1;
        while (left < right){
            if (nums[left] + nums[right] == target){
                List<Integer> item = new ArrayList<>();
                item.add(-target);
                item.add(nums[left]);
                item.add(nums[right]);
                results.add(item);
                //指針移動
                left++;
                right--;
                while (left < right && nums[left] == nums[left-1]){
                    left++;
                }
                while (left < right && nums[right] == nums[right +1]){
                    right--;
                }
            }else if (nums[left] + nums[right] < target){
                //小了 要增大
                left++;
            }else {
                //大了 要減小
                right--;
            }
        }
    }
}

6. LintCode-382. Triangle Count

題意

給定數組,要求找出能夠構成三角形的三元組的數量

思路

  • 需要知道三條邊能夠構成一個三角形的充要條件

    • 兩邊之和大於第三邊或兩邊之差小於第三邊(任意一個都是充要的)
  • 那麼這道題就只需要遍歷數組元素,將每一個元素都當作target

  • 然後尋找兩數之和大於target的數量,所以仍然是一個兩數之和的問題

  • 既然是兩數之和問題,所以仍然是先排序

  • 一個比較關鍵的地方就是在於計數的問題

    • 如果我們固定nums[i],然後在[i+1,n-1]這個區間內去找滿足這樣的一個不等式的數量,那麼就會出現求一個CN2C_N^2的組合數問題
    • 所以爲了規避這個問題,我們從前面看,也就是固定nums[i],然後在區間[0,i-1]這個區間內去找滿足這個不等式的兩數數量
    • 具體的實現細節查看代碼中的註釋

代碼

 public int triangleCount(int[] S) {
        // write your code here
        if ( S == null || S.length == 0){
            return 0;
        }
        Arrays.sort(S);
        int count = 0;
        for (int i = 0; i < S.length; i++) {
            //0. 以S[i] = target 在 區間[0,i-1]中尋找兩數之和大於 target
            int left = 0;
            int right = i - 1;
            while (left < right){

                if (S[left] + S[right] > S[i]){
                    //1. 因爲有序,所以 left 到 right區間內的任意兩數之和都大於 S[i]
                    //1.1 例如 3 4 5 6 7
                    //    S[left] = 3  S[right] = 6 S[i] = 7
                    //    這裏本質上是把 S[right] 和S[i]都固定了,然後偶 left 到 right中的任一個數加上S[right]和S[i]
                    //    都滿足條件,所以數量就是從left 到 right-1 直接的數量,也就是right - left
                    count += right - left;
                    //2. 現在改變right,讓if中的左邊的和小一點,看能否找到更多的解
                    right -=1;

                }else {
                    //3. 兩數之和<=target,所以需要增大
                    left++;
                }
            }
        }
        return count;
    }

7. LintCode59. 3Sum Closest

題意

給定數組和目標數,要求在數組中找到一組三元組,其和離給定的目標數最小,返回這個和

思路

  • 三數之和的題目,一定都是先固定一個數,然後降問題退化爲兩數之和
  • 所以仍然是兩數之和的題目,所以先排序
  • 這裏是求全局最接近,凡是求最值型問題,一定需要維護一個或多個全局變量,與臨時變量進行比較,然後更新
  • 所以,這道題目只需要依次遍歷數組,固定nums[i],然後從區間[i+1,n-1]中尋找可能的離目標數最小的三個數
  • 相關細節在代碼中

代碼

  public int threeSumClosest(int[] numbers, int target) {

        if (numbers == null || numbers.length == 0){
            return 0;
        }
        Arrays.sort(numbers);
        //0. 定義全局變量(凡是求最值型問題,都一定需要定義這樣的全局變量來和一些臨時變量進行比較更新)
        int closestSum = Integer.MAX_VALUE;
        int closestDiff = Integer.MAX_VALUE;
        //1. 遍歷數組,先固定每個元素, -2是因爲從 i開始到最後至少需要三個元素 n -3 n-2 n-1
        for (int i = 0; i < numbers.length - 2; i++) {
            int e1 = numbers[i];
            int left = i+1;
            int right = numbers.length - 1;
            while (left < right){
                //2. 先求出當前的和以及和target的距離
                int tempSum = e1 + numbers[left] + numbers[right];
                int tempDiff = Math.abs(tempSum - target);
                //3. 如果當前距離更短則更新
                //3.1 這裏有一個細節是,進入了if之後沒有移動指針
                //     這樣在下次的while循環中,一定會進入else分支,然後移動指針
                if (tempDiff < closestDiff){
                    closestDiff = tempDiff;
                    closestSum = tempSum;
                }else {
                    //4. 如果當前距離沒有更短,則需要縮短tempSum和target的距離
                    //4.1 具體怎麼縮短就需要看當前的具體情況了
                    if (tempSum < target){
                        left++;
                    }else {
                        right--;
                    }
                }
            }
        }
        return closestSum;
    }

8. LeetCode18-4Sum

題意

給定數組和一個目標數,找到4個數之和等於該目標數的組合,返回這些組合

思路

  • 遍歷數組,依次固定兩個數,然後把問題轉換爲兩數之和
  • 注意對每一個數的去重

代碼

 public List<List<Integer>> fourSum(int[] nums, int target) {


        List<List<Integer>> results = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return results;
        }
        Arrays.sort(nums);
        //1. i後面至少還需要3個數 n-4 n-3 n-2 n-1
        for (int i = 0; i < nums.length - 3; i++) {
            //去重
            if (i > 0 && nums[i] == nums[i-1]){
                continue;
            }
            int e1 = nums[i];
            //1.1 j後面至少還要2個數 n-3 n-2 n-1
            for (int j = i+1; j < nums.length - 2; j++) {
                //去重 e1 和e2 是可以相等的
                if (j != i + 1 && nums[j] == nums[j-1]){
                    continue;
                }
                int e2 = nums[j];

                int left = j + 1;
                int right = nums.length - 1;
                while (left < right){

                    int sum = e1 + e2 + nums[left] + nums[right];
                    if (sum == target){
                        List<Integer> item = new ArrayList<>();
                        item.add(e1);
                        item.add(e2);
                        item.add(nums[left]);
                        item.add(nums[right]);
                        results.add(item);
                        left++;
                        right--;
                        //2. 去重
                        while (left < right && nums[left] == nums[left-1]){
                            left++;
                        }
                        while (left < right && nums[right] == nums[right+1]){
                            right--;
                        }
                    }else if (sum > target){
                        right--;
                    }else {
                        left++;
                    }
                }
            }
        }
        return results;

    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章