天天看點

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

面向大象程式設計

本期例題:LeetCode 167 - Two Sum II - Input array is sorted[1](Easy)

給定一個已按照升序排列的有序數組,找到兩個數使得它們相加之和等于目标數(不可以重複使用同一個數),傳回兩個數的下标值 i 和 j,要求 i < j。

假設每個輸入有且僅有一個答案。

示例:

  • 輸入: numbers = [2, 7, 11, 15], target = 9
  • 輸出: [0, 1]
(注意:這裡和原題略有不同,下标改為從 0 開始,更自然。)

Two Sum 是 LeetCode 的第一道題,你們應該都見過。乍一看來,Two Sum II 這道題和 Two Sum 問題一樣平平無奇。然而,這道題實際上内藏玄機,加上了數組有序的變化之後,它就換了一套解法。

如果你直接翻答案的話,會發現這就是一道普通的雙指針解法。兩個指針,

 的時間。但是,如果你隻看答案,沒有了解背後的道理,就會陷入一看就會,一做就跪的困境。

實際上,在這個雙指針解法背後蘊含的是縮減搜尋空間的通用思想。那麼,這篇文章将會為你細細講述這個解法背後的道理,讓你能真正地了解這道經典題目。同時,要做到下次遇到同類題目時,可以快速想到這種解法。

這篇文章将會包含:

  • 雙指針解法的正确性解釋
  • 雙指針解法的背後原理:縮減搜尋空間
  • 相同原理的另一道例題講解
  • 相關題目

雙指針的解法

在做 Two Sum II 之前,你可能已經知道 Two Sum 的解法。使用一個散清單,做一次線性周遊,可以得到 

 時間複雜度、 

 空間複雜度的解法。那麼在 Two Sum II 中,數組有序這個變化,能幫助降低複雜度嗎?

看到有序這個條件,你可能首先想到的是二分查找。但是仔細一想,需要固定一個數,對另一個數進行二分查找,這樣時間複雜度是 

,顯然不行。在不排序的情況下都做得到 

 時間、 

 空間。那麼我們的目标隻可能是:

 的時間、

那麼怎麼能做到呢?這就是本題的雙指針解法了。讓我們先看看答案是怎麼寫的:

public int[] twoSum(int[] nums, int target) {
    int i = 0;
    int j = nums.length - 1;
    while (i < j) {
        int sum = nums[i] + nums[j];
        if (sum < target) {
            i++;
        } else if (sum > target) {
            j--;
        } else {
            return new int[]{i, j};
        }
    }
    return new int[]{-1, -1};
}      

代碼的邏輯很簡單,就輕輕巧巧地使用了兩個指針,一個隻向左移動,一個隻向右移動。走完一趟,就得到了答案。

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

兩個指針從兩頭向中間移動

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

兩個指針在中間相遇

很多題解都隻給出了這樣一個答案,仿佛道理是不證自明的。但事實上,這個解法并不是能讓人一眼就看懂。我第一次看到這道題的答案時,把這個解法叫做 Amazing O(n) 解法。這個解法的神奇之處在于,它把原本 

 的複雜度壓縮到了 

,同時保證了算法的正确性。我們需要解釋它為什麼一定能得到解,而不會漏掉某些情況。

雙指針解法的正确性解釋

我們考慮兩個指針指向的數字,​

​A[i]​

​​ 和 ​

​A[j]​

​​。由于數組是有序的,在一開始,​

​A[i]​

​​ 是數組中最小的數字,​

​A[j]​

​​ 是最大的數字。我們将 ​

​A[i] + A[j]​

​​ 與目标和 ​

​target​

​ 進行比較,則可能有兩種情況:

  • ​A[i] + A[j]​

    ​ 大了。這時候,我們應該去找更小的兩個數。由于 ​

    ​A[i]​

    ​ 已經是最小的元素了,将任何 ​

    ​A[i]​

    ​ 以外的數跟 ​

    ​A[j]​

    ​ 相加的話,和隻會更大。是以 ​

    ​A[j]​

    ​ 一定不能構成正确的解,于是将 ​

    ​j​

    ​ 向左移動一格,排除 ​

    ​A[j]​

    ​。
  • ​A[i] + A[j]​

    ​ 小了。這時候,我們應該去找更大的兩個數。由于 ​

    ​A[j]​

    ​ 已經是最大的元素了,将任何 ​

    ​A[j]​

    ​ 以外的數跟 ​

    ​A[i]​

    ​ 相加的話,和隻會更小。是以 ​

    ​A[i]​

    ​ 一定不能構成正确的解,于是将 ​

    ​i​

    ​ 向右移動一格,排除 ​

    ​A[i]​

    ​。

而第一步排除掉最左或最後的一個數後,我們再看子數組 ​

​A[i..j]​

​​ ,其中 ​

​A[i]​

​​ 又是最小的數字,​

​A[j]​

​ 又是最大的數字。我們可以繼續進行這樣的排除。以此類推,進行 

可以看到,無論 ​

​A[i] + A[j]​

​ 的結果是大了還是小了,我們都可以排除掉其中一個數。這樣的排除法和二分搜尋很相似。二分搜尋通過每次排除一半的元素來減少比較的次數;而這道題的方法通過每次排除一個元素來減少比較的次數。兩者又恰好都是利用了數組有序這個性質。

說到這裡,這個解法的原理已經揭開一半了。接下來,我們再用更直覺的方式,從搜尋空間的角度真正地了解這道題。

搜尋空間的縮減過程

在這道題中,我們要尋找的是符合條件的一對下标 

,它們需要滿足的限制條件是:

  •  都是合法的下标,即

  • (題目要求)

而我們希望從中找到滿足 ​

​A[i] + A[j] == target​

​ 的下标 

。以 

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

下标 i, j 的搜尋空間

由于 

 的限制條件的限制,搜尋空間是白色的倒三角部分。可以看到,搜尋空間的大小是 

 數量級的。如果用暴力解法求解,一次隻檢查一個單元格,那麼時間複雜度一定是 

。而更優的算法,則可以在一次操作内排除掉多個不合格的單元格,進而快速削減搜尋空間,定位問題的解。

那麼我們來看看,本題的雙指針解法是如何削減搜尋空間的。一開始,我們檢查右上方單元格 

,即計算 ​

​A[0] + A[7]​

​ :

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

檢查單元格 0, 7

假設 ​

​A[0] + A[7]​

​​ 小于目标和,由于 ​

​A[7]​

​​ 已經是最大的數,我們可以推出 ​

​A[0] + A[6]​

​​ 、​

​A[0] + A[5]​

​​、……、​

​A[0] + A[1]​

​​ 也都小于目标和,這些都是不合要求的解,可以一次排除,如下圖所示。這相當于排除 

 的全部解,削減了一行的搜尋空間,對應雙指針解法中的 ​​

​i++​

​。

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

排除 i = 0 的全部解

在削減一行搜尋空間之後,剩餘的搜尋空間仍然是倒三角形狀。我們檢查右上方的單元格 

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

檢查單元格 1, 7

假設 ​

​A[1] + A[7]​

​​ 大于目标和,此時需要排除 

 的全部解,削減一列搜尋空間,對應雙指針解法中的 ​​

​j--​

​:

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

排除 j = 7 的全部解

以此類推,每次會削減一行或一列的搜尋空間,經過 

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

搜尋空間的減小過程(動圖)

舉一反三:二維矩陣搜尋

Two Sum II 或許太過簡單,并不需要用到搜尋空間的知識。那麼讓我們來看看這個思想在另一個例題中的應用。

例題:LeetCode 240 - Search a 2D Matrix II[2](Medium)

一個 
  • 每行的元素從左到右升序排列
  • 每列的元素從上到下升序排列
寫一個高效的算法在矩陣中搜尋目标值 target。

這道題目的特點是,二維矩陣本身就是搜尋空間,是以我們不需要思考搜尋空間的形狀,直接進行搜尋即可。看到有序的這個條件,我們很容易想到使用二分搜尋。二分搜尋的中間點,顯然應該是矩陣的中心點:

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

二分搜尋的中間點

不過仔細思考就會發現,根據矩陣的性質,這個中心點把矩陣分成的四個部分中,隻有左上部分全部小于中心點,隻有右下部分全部大于中心點。也就是說做一次比較最多隻能削減 1/4 的搜尋空間。更大的問題是,删除掉左上部分或者右下部分之後,剩餘的形狀不再是一個規則的矩形,就無法繼續進行二分搜尋了。

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

一次比較最多删除 1/4 的搜尋空間

那麼我們不妨換一種思路,不追求一次削減一半的搜尋空間,而是像剛才的 Two Sum II 問題那樣,一次削減一行或者一列。類比剛才的雙指針解法,我們可以檢查矩陣右上角的元素,即 ​

​matrix[0][7]​

​。

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

檢查矩陣右上角的元素

如果 ​

​matrix[0][7]​

​ 小于目标值,由于矩陣每行是有序的,同一行所有的元素肯定都小于目标值,就可以排除一行的元素。

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

削減 i = 0 的一行搜尋空間

而如果 ​

​matrix[0][7]​

​ 大于目标值,由于矩陣每列是有序的,同一列所有的元素一定都大于目标值,這又可以排除一列的元素。

LeetCode 例題精講 | 04 用雙指針解 Two Sum:縮減搜尋空間

削減 j = 7 的一列搜尋空間

那麼,我們每次都必定可以排除一行或一列的元素。經過 

總結

從本文的例題可以看出,LeetCode 很多題目的答案很簡單,但若想真正記住并舉一反三,還是要了解題目背後的思想。Two Sum II 表面上是一個簡單的雙指針解法,但為了能想出這個解法,需要了解背後的搜尋空間思想。

搜尋空間的技巧需要在實際題目中靈活運用,本文中二維矩陣搜尋例題的解法就是靈活運用了搜尋空間思想。這裡再列出一道和本題非常相關的題目,請仔細體會其中削減搜尋空間的思想:

  • 11. Container With Most Water[3]

參考資料

[1]

LeetCode 167 - Two Sum II - Input array is sorted: ​​https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/​​

[2]

LeetCode 240 - Search a 2D Matrix II: ​​https://leetcode.com/problems/search-a-2d-matrix-ii/​​

[3]

11. Container With Most Water: ​​https://leetcode.com/problems/container-with-most-water/​​

歡迎關注我的公衆号“五分鐘學算法”,如果喜歡,麻煩點一下“

繼續閱讀