1. 程式人生 > 實用技巧 >資料庫索引調優技巧

資料庫索引調優技巧

技術標籤:LeetCode雙指標leetcode演算法三數之和

15. 三數之和

1、參考資料

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

https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-by-leetcode-solution/

2、題目要求

題目要求:

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

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

示例:

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

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

3、程式碼思路

暴力匹配

  1. 暴力匹配可以,三層 for 迴圈帶給我們的是 O(n3) 的複雜度,我們還需要將三個數按照從小到大的順序排列,並使用 HashMap 進行去重操作,得到不包含重複三元組的最終答案
  2. 因為 (a, b, c)(b, a, c)(b, c, a) 屬於重複的三元組,匹配之後我們需要進行排序、去重操作

排序 + 雙指標

  1. 我們首先要理解不重複的本質是什麼?我們先將陣列排序,然後在陣列中找出三個數 abc,滿足其和:a + b + c = 0,其中 a <= b <= c,這樣保證了只有 (a, b, c) 這樣的順序,不會出現 (b, a, c) 或者 (b, c, a)
    這樣的順序,這樣我們就不用對三元組進行去重操作
  2. 但是這樣做我們始終沒有跳出三層 for 迴圈的思想,實際上第二重 + 第三重迴圈我們可以使用一層 while 迴圈 + 雙指標來替代
  3. left = i + 1 為左指標(i 最外層 for 迴圈的計數器),right = nums.length - 1 為右指標。當 left 增加,right 不變時,三數之和增大;當 right 減小,left 不變時,三數之和減小;我們利用這個特點,可以將原來兩層 for 迴圈變為一層 while 迴圈,即我們可以通過雙指標的方式將內層迴圈的複雜度從 O(n2) 降為 O(n)
  4. 為了保證得到的三元組為不重複的三元組,對於數 a
    ,我們每次在 for 迴圈中都判斷數 a 是否與上次的值相同,如果相同,則跳過本次迴圈,對於 leftright,我們每次在外層 for 開始時,將 left 重置為 i + 1,將 right 重置為 nums.length - 1,在匹配到三數之和等於 0 時,將 left 右移至與上次元素值不同的位置,將 right 左移至與上次元素值不同的位置(保證三元組不重複)

總結:

  1. 題目要求不可以包含重複的三元組,我們可以使用模式識別,利用排序避免重複答案
  2. 將陣列排序後,進入第一層 for 迴圈之後其實就是 twoSum 問題,與 twoSum 不同的是,twoSum 只需要找到一組解,而這裡要找到所有解 ,從而將問題變為:在一個排序陣列中找到所有的兩數之和
  3. 在一個排序陣列中找到所有的兩數之和:兩個指標分別指向陣列的首位,如果 sum > target,那麼首指標後移,使得 sum 增大;如果 sum < target,那麼尾指標前移,使得 sum 減小

4、程式碼實現

程式碼

/**
 * @ClassName ThreeSumDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/9/2 17:11
 * @Version 1.0
 */
public class ThreeSumDemo {

    public static void main(String[] args) {
        List<List<Integer>> res = threeSum(new int[]{-1, 0, 1, 2, -1, -4});
        System.out.println(res);
    }

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

        List<List<Integer>> result = new ArrayList<>();

        // Guard Safe
        if (nums == null || nums.length <= 2) {
            return result;
        }

        // 先對陣列進行排序,後面才能方便地進行判斷是否重複
        Arrays.sort(nums);

        // 遍歷陣列,當 i = nums.length - 3時,left = nums.length - 2,right = nums.length - 1,為臨界條件
        for (int i = 0; i < nums.length - 2; i++) {
            // 如果當前元素大於 0,證明已經將陣列中三數之和等於 3 的元素都找出來了,直接跳出迴圈
            if (nums[i] > 0) {
                break;
            }
            // 上一次的元素和當前相同,跳過此次迴圈
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int target = -nums[i]; // 目標值
            int left = i + 1; // left 指標
            int right = nums.length - 1; // right 指標
            // 當 left < right 時,證明還有元素待驗證
            while (left < right) {
                if (nums[left] + nums[right] == target) {
                    // 如果三數之和等於 0,則將這三個數加入集合中
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    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++,嘗試增大三數之和
                    left++;
                } else {
                    // 否則三數之和大於目標值,則 right--,嘗試減小三數之和
                    right--;
                }
            }
        }

        return result;
    }
}