001-ksum 求符合條件的 k 個數 1. Two Sum/15. 3Sum/18. 4Sum/

老馬嘯西風發表於2023-03-27

推薦閱讀

000-從零開始的資料結構與演算法

001-01-ksum 求符合條件的 k 個數 1. Two Sum/15. 3Sum/18. 4Sum/

002-兩數相加 add two numbers

003-無重複字元的最長子串 Longest Substring Without Repeating Characters

004-尋找兩個正序陣列的中位數

005-最長迴文子串 Longest Palindromic Substring

006-N 字形變換 zigzag conversion

007-整數反轉 reverse integer 整數的位運算彙總

008-Regular Expression Matching 正規表示式匹配 + 42.Wildcard Matching 萬用字元匹配

009-盛最多水的容器 Container With Most Water 雙指標法 + 42. 接雨水 Trapping Rain Water + 407. Trapping Rain Water II

010-刪除連結串列的倒數第 N 個結點 Remove Nth Node From End of List 雙指標

011-21.合併多個有序的連結串列 merge k sorted lists

012-括號生成 generate-parentheses + 20. 有效的括號 valid parentheses + 32. 最長有效括號 Longest Valid Parentheses

013-K 個一組翻轉連結串列 Reverse Nodes in k-Group + 24. 兩兩交換連結串列中的節點 swap nodes in pairs

014-兩數相除 divide two integers

015-串聯所有單詞的子串 Substring with Concatenation of All Words

016-31.下一個排列 next permutation + 46. 全排列 permutations + 47. 全排列 II permutations-ii + 60. 排列序列 permutation sequence

017-33. 搜尋旋轉排序陣列 Search in Rotated Sorted Array + 81. Search in Rotated Sorted Array II + 153. Find Minimum in Rotated Sorted Array 尋找旋轉排序陣列中的最小值 + 154.Find Minimum in Rotated Sorted Array II

018-34. 在排序陣列中查詢元素的第一個和最後一個位置 Find First and Last Position of Element in Sorted Array

019-36. 有效的數獨 Valid Sudoku + 37. 解數獨 sudoku solver

020-39. 組合總和 Combination Sum + 40. 組合總和 II Combination Sum II + 77. 組合 combinations + 216. Combination Sum III + 377. 組合總和 Ⅳ

PIC

1. Two Sum 兩數之和

題目

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

你可以假設每種輸入只會對應一個答案。但是,陣列中同一個元素不能使用兩遍。

示例:

給定 nums = [2, 7, 11, 15], target = 9

因為 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

思路

最簡單粗暴的一個雙層迴圈:

(1)遍歷第一層陣列 nums[i]

(2)遍歷第二層陣列 nums[j],如果 nums[i] + nums[j] == t,符合。

基礎解法

java 實現:

public int[] twoSumBasic(int[] nums, int target) {
    for(int i = 0; i < nums.length; i++) {
        for(int j = 0; j < nums.length; j++) {
            // 每個元素只使用一次
            if(i == j) {
                continue;
            }
            if(nums[i] + nums[j] == target) {
                return new int[]{i, j};
            }
        }
    }
    // 實際每個都有答案,不應該都到這裡。
    return null;
}

效能分析:

可謂慘不忍睹,為什麼呢?

是否有比較好的最佳化思路?

Runtime: 167 ms, faster than 5.01% of Java online submissions for Two Sum.
Memory Usage: 41.3 MB, less than 10.92% of Java online submissions for Two Sum.

最佳化解法

最佳化思路:

藉助 HashMap 資料結構,將原本 O(n) 的遍歷,降低為 O(1) 的查詢。

java 實現如下:

實現時注意 HashMap 的擴容問題,此處直接指定為和陣列一樣大。

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

效果:

Runtime: 1 ms, faster than 99.93% of Java online submissions for Two Sum.
Memory Usage: 39.5 MB, less than 69.35% of Java online submissions for Two Sum.

15. 3Sum 三數之和

結束了第一道開胃菜之後,我們來看看第二道菜。

題目

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

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

示例:

給定陣列 nums = [-1, 0, 1, 2, -1, -4],

滿足要求的三元組集合為:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

粗暴解法

最簡單的思路,我們直接來一個三層迴圈:

public List<List<Integer>> threeSum(int[] nums) {
    if(nums.length < 3) {
        return Collections.emptyList();
    }
    List<List<Integer>> results = new ArrayList<>();
    Set<String> stringSet = new HashSet<>();
    for(int i = 0; i < nums.length-2; i+=3) {
        for(int j = i+1; j < nums.length-1; j++) {
            for(int k = j+1; k < nums.length; k++) {
                if((nums[i] + nums[j]+nums[k]) == 0) {
                    List<Integer> list = Arrays.asList(nums[i], nums[j], nums[k]);
                    // 排序保證結果不重複
                    Collections.sort(list);
                    String string = list.toString();
                    if(stringSet.contains(string)) {
                        continue;
                    }
                    stringSet.add(string);
                    results.add(list);
                }
            }
        }
    }
    return results;
}
  • 執行結果

很不幸,這個是不透過的。會執行超時,因為執行的比較慢。

最佳化解法

最佳化思路:

(1)對原始陣列進行排序,保證可以使用雙指標法

(2)固定 1 個元素。剩餘的兩個元素採用雙指標的方式。初始化時,一個在最左邊,一個在最右邊。然後不斷調整位置,直到符合條件為止。

(3)不可以包含重複的三元組,要同時跳過重複的資訊。

java 實現:

public List<List<Integer>> threeSum(int[] nums) {
    //1. 排序
    Arrays.sort(nums);
    List<List<Integer>> results = new ArrayList<>(nums.length);
    //2. 雙指標
    for(int i = 0; i < nums.length; i++) {
        int num = nums[i];
        if(num > 0) {
            return results;
        }
        if(i > 0 && nums[i] == nums[i-1]) {
            continue;
        }
        int l = i+1;
        int r = nums.length-1;
        while (l < r) {
            int sum = num + nums[l] + nums[r];
            if(sum < 0) {
                l++;
            } else if(sum > 0) {
                r--;
            } else {
                List<Integer> integers = new ArrayList<>(3);
                integers.add(num);
                integers.add(nums[l]);
                integers.add(nums[r]);
                results.add(integers);
                // 跳過重複的元素
                while(l < r && nums[l+1] == nums[l]) {
                    l++;
                }
                while (l < r && nums[r-1] == nums[r]) {
                    r--;
                }
                l++;
                r--;
            }
        }
    }
    return results;
}

效能:

速度超過 99.87 的使用者提交,還不錯。

16. 最接近的三數之和

給你一個長度為 n 的整數陣列 nums 和 一個目標值 target。

請你從 nums 中選出三個整數,使它們的和與 target 最接近。

返回這三個數的和。

假定每組輸入只存在恰好一個解。

示例 1:

輸入:nums = [-1,2,1,-4], target = 1
輸出:2
解釋:與 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

示例 2:

輸入:nums = [0,0,0], target = 1
輸出:0

提示:

3 <= nums.length <= 1000

-1000 <= nums[i] <= 1000

-10^4 <= target <= 10^4

V1-雙指標法

思路

針對多個數之和,對無序的陣列進行一次排序,可以大大降低後續的時間複雜度。

我們透過兩個指標,l r 分別計算每一次的差值,找到最小的差異。

當然,等於肯定最小,直接返回即可。

java 實現

    /**
     * 思路:
     *
     * 能否繼續借助排序+雙指標?
     *
     * 1. 如果相等,則直接返回
     * 2. 否則需要儲存最接近的一個值。
     *
     * 3. 如果差異越來越大,則直接停止。
     *
     * 使用 abs
     *
     * @param nums 數字
     * @param target 目標值
     * @return 結果
     * @since v1
     */
    public int threeSumClosest(int[] nums, int target) {
        // 最小
        if(nums.length == 3) {
            return nums[0]+nums[1]+nums[2];
        }

        //1. 排序
        Arrays.sort(nums);

        //2. 雙指標
        int diff = Integer.MAX_VALUE;
        for(int i = 0; i < nums.length; i++) {
            int l = i+1;
            int r = nums.length-1;

            // 去重 i,l,r
            while (l < r) {
                // 此處可以直接返回
                int sum = nums[i] + nums[l] + nums[r];
                int loopDiff = sum-target;

                if(sum == target) {
                    return target;
                } else if(loopDiff < 0) {
                    // 偏小
                    l++;

                    if(Math.abs(loopDiff) < Math.abs(diff)) {
                        diff = loopDiff;
                    }
                } else {
                    // 偏大
                    r--;

                    if(Math.abs(loopDiff)  < Math.abs(diff)) {
                        diff = loopDiff;
                    }
                }
            }
        }

        return target+diff;
    }

效果

Runtime: 5 ms, faster than 99.27% of Java online submissions for Container With Most Water.
Memory Usage: 39 MB, less than 100% of Java online submissions for Container With Most Water.

效果還是非常不錯的。

V2-最佳化

思路

針對上面的演算法進行最佳化。

能否繼續借助排序+雙指標?

  1. 最大值如果依然小於原有差異,跳過
  2. 最小值如果依然大於原有差異,跳過。

直接先把可能有結果的大概範圍找到,然後再進一步細化,快速定位結果。

java 實現

    public int threeSumClosest(int[] nums, int target) {
        // 最小
        int result = nums[0] + nums[1] + nums[2];

        //1. 排序
        Arrays.sort(nums);

        //2. 雙指標
        for(int i = 0; i < nums.length-2; i++) {
            int l = i+1;
            int r = nums.length-1;

            if (nums[i] + nums[i+1] + nums[i+2] - target >= Math.abs(target - result)) {
                break;  //Too big, can't get better result!
            }
            if (i < nums.length-3 && nums[i+1] + nums[nums.length-2] + nums[nums.length-1] < target) {
                continue; //Too small, skip
            }

            while (l < r) {
                // 此處可以直接返回
                int sum = nums[i] + nums[l] + nums[r];

                // 如果差異較小
                if(Math.abs(sum-target) < Math.abs(result-target)) {
                    result = sum;
                } else if(sum < target) {
                    // 偏小
                    l++;
                } else if(sum > target) {
                    r--;
                } else {
                    return sum;
                }
            }
        }

        return result;
    }

效果

Runtime: 1 ms, faster than 100.00% of Java online submissions for 3Sum Closest.
Memory Usage: 38.9 MB, less than 85.21% of Java online submissions for 3Sum Closest.

18. 4Sum 四數之和

常言道,有二有三必須有四。

這道題當然還有四個數之和的版本。

題目

給定一個包含 n 個整數的陣列 nums 和一個目標值 target,判斷 nums 中是否存在四個元素 a,b,c 和 d ,使得 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]
]

解法

解題思路類似上述的 3 個數之和,區別是這次我們固定左邊 2 個元素。

java 實現如下:

public List<List<Integer>> fourSum(int[] nums, int target) {
    // 大小可以最佳化
    List<List<Integer>> resultList = new ArrayList<>(nums.length);
    //1. 排序
    Arrays.sort(nums);
    //2. 類似雙指標,固定左邊2個元素。
    for(int i = 0; i < nums.length -3; i++) {
        // 跳過 i 的重複元素
        if(i > 0 && nums[i] == nums[i-1]) {
            continue;
        }
        for(int j = i+1; j < nums.length-2; j++) {
            // 確保跳過 j 的重複元素
            if(j > i+1 && nums[j] == nums[j-1])  {
                continue;
            }
            // 雙指標法則
            int l = j+1;
            int r = nums.length-1;
            while (l < r) {
                int sum = nums[i]+nums[j]+nums[l]+nums[r];
                // 遍歷完所有符合的資訊
                if(sum < target) {
                    l++;
                } else if(sum > target) {
                    r--;
                } else {
                    List<Integer> result = Arrays.asList(nums[i], nums[j], nums[l], nums[r]);
                    resultList.add(result);
                    // 跳過重複的元素
                    while (l < r && nums[l] == nums[l+1]) {
                        l++;
                    }
                    while(l < r && nums[r] == nums[r-1]) {
                        r--;
                    }
                    l++;
                    r--;
                }
            }
        }
    }
    return resultList;
}

經過 3sum 之後,你對自己的實現信心十足。

效果對比打敗了 49.10% 的使用者。WTF!

其實答案也很簡單,因為大家和你一樣已經學會了這種解法。

那麼,我們還能最佳化嗎?

最佳化方法

思路:

主要是可以快速返回的一些最佳化

(1)對於長度小於 4 的陣列,直接返回

(2)如果在當前迴圈中 max 小於 target,或者 min 大於 target 其實也可以快速跳過。

java 實現:

public List<List<Integer>> fourSum(int[] nums, int target) {
    //1.1 快速返回
    if(nums.length < 4) {
        return Collections.emptyList();
    }
    // 大小可以最佳化
    List<List<Integer>> resultList = new ArrayList<>(nums.length);
    //2. 排序
    Arrays.sort(nums);
    //1.2 範圍判斷
    final int length = nums.length;
    int min = nums[0] + nums[1] + nums[2] + nums[3];
    int max = nums[length-1] + nums[length-2] + nums[length-3] + nums[length-4];
    if(min > target || max < target) {
        return resultList;
    }
    //3. 類似雙指標,固定左邊2個元素。
    for(int i = 0; i < length -3; i++) {
        // 跳過 i 的重複元素
        if(i > 0 && nums[i] == nums[i-1]) {
            continue;
        }
        for(int j = i+1; j < length-2; j++) {
            // 確保跳過 j 的重複元素
            if(j > i+1 && nums[j] == nums[j-1])  {
                continue;
            }
            // 雙指標法則
            int l = j+1;
            int r = length-1;
            // 快速跳過
            int minInner = nums[i] + nums[j] + nums[j+1] + nums[j+2];
            int maxInner = nums[i] + nums[j] + nums[r-1] + nums[r];
            if(minInner > target || maxInner < target) {
                continue;
            }
            while (l < r) {
                int sum = nums[i]+nums[j]+nums[l]+nums[r];
                // 遍歷完所有符合的資訊
                if(sum < target) {
                    l++;
                } else if(sum > target) {
                    r--;
                } else {
                    List<Integer> result = Arrays.asList(nums[i], nums[j], nums[l], nums[r]);
                    resultList.add(result);
                    // 跳過重複的元素
                    while (l < r && nums[l] == nums[l+1]) {
                        l++;
                    }
                    while(l < r && nums[r] == nums[r-1]) {
                        r--;
                    }
                    l++;
                    r--;
                }
            }
        }
    }
    return resultList;
}

效果:

超過了 92.32% 的提交,勉強過關。

Runtime: 5 ms, faster than 92.21% of Java online submissions for 4Sum.
Memory Usage: 39.9 MB, less than 56.17% of Java online submissions for 4Sum.

ksum

題目

經過了上面的 2sum/3sum/4sum 的洗禮,我們現在將這道題做下推廣。

如何求 ksum?

思路

其實所有的這種解法都可以轉換為如下的兩個問題:

(1)sum 問題

(2)將 k sum 問題轉換為 k-1 sum 問題

示例程式碼

/**
 * 對 k 個數進行求和
 * @param nums 陣列
 * @param target 目標值
 * @param k k
 * @param index 下標
 * @return 結果類表
 * @since v1
 */
public List<List<Integer>> kSum(int[] nums, int target, int k, int index) {
    int len = nums.length;
    List<List<Integer>> resultList = new ArrayList<>();
    if (index >= len) {
        return resultList;
    }
    if (k == 2) {
        int i = index, j = len - 1;
        while (i < j) {
            //find a pair
            if (target - nums[i] == nums[j]) {
                List<Integer> temp = new ArrayList<>();
                temp.add(nums[i]);
                temp.add(target - nums[i]);
                resultList.add(temp);
                //skip duplication
                while (i < j && nums[i] == nums[i + 1]) {
                    i++;
                }
                while (i < j && nums[j - 1] == nums[j]) {
                    j--;
                }
                i++;
                j--;
                //move left bound
            } else if (target - nums[i] > nums[j]) {
                i++;
                //move right bound
            } else {
                j--;
            }
        }
    } else {
        for (int i = index; i < len - k + 1; i++) {
            //use current number to reduce ksum into k-1 sum
            List<List<Integer>> temp = kSum(nums, target - nums[i], k - 1, i + 1);
            if (temp != null) {
                //add previous results
                for (List<Integer> t : temp) {
                    t.add(0, nums[i]);
                }
                resultList.addAll(temp);
            }
            while (i < len - 1 && nums[i] == nums[i + 1]) {
                //skip duplicated numbers
                i++;
            }
        }
    }
    return resultList;
}

開源地址

為了便於大家學習,所有實現均已開源。歡迎 fork + star~

https://github.com/houbb/leetcode

參考資料

https://leetcode-cn.com/problems/two-sum

https://leetcode.com/problems/two-sum/discuss/3/Accepted-Java-O(n)-Solution

https://leetcode-cn.com/problems/3sum

https://leetcode-cn.com/problems/4sum

https://leetcode.com/problems/4sum/discuss/8609/My-solution-g...

相關文章