力扣.1 兩數之和 N 種解法 two-sum

老马啸西风發表於2024-11-11

陣列系列

力扣資料結構之陣列-00-概覽

力扣.53 最大子陣列和 maximum-subarray

力扣.128 最長連續序列 longest-consecutive-sequence

力扣.1 兩數之和 N 種解法 two-sum

力扣.167 兩數之和 II two-sum-ii

力扣.170 兩數之和 III two-sum-iii

力扣.653 兩數之和 IV two-sum-IV

力扣.015 三數之和 three-sum

力扣.016 最接近的三數之和 three-sum-closest

力扣.259 較小的三數之和 three-sum-smaller

題目

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

你可以假設每種輸入只會對應一個答案,並且你不能使用兩次相同的元素。

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

示例 1:

輸入:nums = [2,7,11,15], target = 9
輸出:[0,1]
解釋:因為 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

輸入:nums = [3,2,4], target = 6
輸出:[1,2]
示例 3:

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

提示:

2 <= nums.length <= 10^4
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9

只會存在一個有效答案

進階:你可以想出一個時間複雜度小於 O(n^2) 的演算法嗎?

前言

這道題作為 leetcode 的第一道題,看起來非常簡單。

不過今天回頭看,解法還是很多的。

大概可以有下面幾種:

  1. 暴力解法

  2. 陣列排序+二分

  3. HashSet/HashMap 最佳化

v1-暴力解法

思路

直接兩次迴圈,找到符合結果的資料返回。

這種最容易想到,一般工作中也是我們用到最多的。

實現

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];

        final int n = nums.length;
        for(int i = 0; i < n; i++) {
            for(int j = i+1; j < n; j++) {
                if(nums[i] + nums[j] == target) {
                    res[0] = i;
                    res[1] = j;
                }
            }
        }

        return res;
    }
}

效果

49ms 33.92%

效果一般

小結

暴力演算法雖然容易想到,不過如果遇到特別長的場景用例,會直接超時。

當然這一題明顯看到了 leetcode 的憐憫,怕我們上來就放棄。

我們如何改進一下呢?

排序是這個場景另一種很有用的方式。

v2-排序+二分

思路

我們希望排序,然後透過二分法來提升效能。

程式碼

public int[] twoSum(int[] nums, int target) {
    int[] res = new int[2];
    Arrays.sort(nums);

    // 遍歷+二分
    int n = nums.length;
    for(int i = 0; i < n; i++) {
        // 找另一部分
        int t = target - nums[i];
        // 找到了自己怎麼辦?
        int j = Arrays.binarySearch(nums, t);
        if(j > 0) {
            res[0] = i;
            res[1] = j;
            return res;
        }
    }

    return res;
}

效果

當然,你會發現這段程式碼無法透過測試。

因為排序導致順序錯亂了。

我們要如何保證順序的同時,有進行排序呢?

順序修正

整體思路解釋:

  1. 我們用一個二維陣列,記錄原始值+原始的下標

  2. 排序後,target-nums[i]就是剩下要找的數,我們在陣列中用二分法尋找。

這裡為了限制 j > i,二分法我們直接自己實現了,順便練習一下。

class Solution {

    public int[] twoSum(int[] nums, int target) {
        int n = nums.length;

        // 儲存值+下標 避免排序後找不到原始的索引
        List<int[]> indexList = new ArrayList<>();
        for(int i = 0; i < n; i++) {
            indexList.add(new int[]{nums[i], i});
        }
        Collections.sort(indexList, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0] - o2[0];
            }
        });

        // 遍歷+二分  這裡直接手寫二分比較簡單,因為直接查詢數字可能會重複
        for(int i = 0; i < n-1; i++) {
            int t = target - indexList.get(i)[0];
            //從當前 i 的後面開始尋找
            int j = binarySearch(indexList, t, i+1);
            if(j >= 0) {
                // 原始下標
                return new int[]{indexList.get(i)[1], j};
            }
        }

        //NOT FOUND
        return new int[]{-1, -1};
    }

    private int binarySearch(List<int[]> indexList,
                             final int target,
                             final int startIx) {
        int left = startIx;
        int right = indexList.size()-1;
        while (left <= right) {
            int mid = left + (right-left) / 2;
            int val = indexList.get(mid)[0];
            if(val == target) {
                // 原始下標
                return indexList.get(mid)[1];
            }

            // update
            if(val < target) {
                left = mid+1;
            } else {
                right = mid-1;
            }
        }

        return -1;
    }

}

效果

9ms 38.89%

只能說,比暴力要好不少。

不過依然後進步的空間。

v3-排序+雙指標

在做完了第 T167 之後,收到了雙指標的啟發。

思路

我們定義兩個指標

left=0
right=n-1
sum=num[left]+num[right-1]

因為陣列有有序的,所以只有 3 種情況:

  1. sum == target 直接滿足

  2. sum < target,left++

  3. sum > target, right--

實現

class Solution {

    
    public int[] twoSum(int[] nums, int target) {
        int n = nums.length;

        // 儲存值+下標 避免排序後找不到原始的索引
        List<int[]> indexList = new ArrayList<>();
        for(int i = 0; i < n; i++) {
            indexList.add(new int[]{nums[i], i});
        }
        Collections.sort(indexList, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0] - o2[0];
            }
        });

        // 雙指標
        int left = 0;
        int right = n-1;
        while (left < right) {
            int sum = indexList.get(left)[0] + indexList.get(right)[0];
            if(sum == target) {
                return new int[]{indexList.get(left)[1], indexList.get(right)[1]};
            }
            if(sum < target) {
                left++;
            }
            if(sum > target) {
                right--;
            }
        }

        //NOT FOUND
        return new int[]{-1, -1};
    }

}

效果

8ms 39.15%

v4-HashMap

思路

在我們寫完上面的寫法之後,有沒有一種感覺?

既然是要找另一部分的值,那麼直接 Hash,複雜度 O(1) 不是更快?

是的,你真是個小機靈鬼。

雜湊在這種等於的場景是最快的,不過上面的二分適用性更廣一些,比如查詢大於或者小於的時候。

當然,這是其他型別的題目。

我們先來看一下雜湊的解法。

程式碼

public int[] twoSum(int[] nums, int target) {
    int n = nums.length;
    HashMap<Integer, Integer> hashMap = new HashMap<>();
    for(int i = 0; i < n; i++) {
        int other = target - nums[i];
        if(hashMap.containsKey(other)) {
            int j = hashMap.get(other);
            return new int[]{i, j};
        }
        // 儲存
        hashMap.put(nums[i], i);
    }
    return new int[]{-1, -1};
}

效果

2ms 99.68%

只能說效果拔群,Hash 確實是這類方法中最快的。

小結

這類題目的思路基本都是類似的。

我們後續將看一下 n 數之和的系列,感興趣的小夥伴點點贊,關注不迷路。

相關文章