LeetCode 課前預習 - 掌握 Two Pointer 和 Sliding Window 捕捉使用時機
前言
今天想要深度的去了解Two pointer與sliding window 的使用,這也是面試常見的技巧。主要整理出『如何辨識要使用這些技巧』。
Two Pointer
那我們先從two pointer開始,因為sliding window其實是two pointer的一種特例,或是進階應用。
那麼什麼是two pinter呢?pinter 俗稱指標,指標用來儲存另一個物件的記憶體位置,通常在leetcode中,指標只是一個整數,用來指向陣列的某個物件索引。
1 | pointer = 0 |
Two pointer就是指向陣列的兩個指標,通常是指向陣列的頭尾,然後向中間移動,或是指向陣列的兩個不同位置,然後向中間移動。雙指標允許我們同時查看兩個不同的值,然後我們根據這兩個值的關係來決定下一步的操作。
Example: Valid Palidorme
Palidorme 就是一個經典的 two pointer 例子,Palidorme 又稱作『回文』,向前讀和向後讀是一樣的,例如:"racecar"
(倒過來也長一樣)。為了開始檢查,我們在兩端初始化一個指標,left和right,分別指向字串的開頭和結尾。然後我們每次檢查兩個指標指向的字元是否相同,如果不同,就返回False,如果相同,就繼續檢查下一個字元,直到兩個指標相遇。
1 | string = 'racecar' |
以下是python代碼
1 | def is_palindrome(string): |
Other Scenarios
two pinter通常不像是上面的例子這麼簡單,有時候需要發揮想像力。以下是常見使用two pointer的情境:
- Binary Search 二元搜尋
- Slow and Fast Pointer to detect a cycle
- One pointer as a boundary and one another to search
- Pointer at the beginning of two sorted arrays
- Sliding Window (Advanced)
Leetcode 題目:
難度較低
- Valid Palindrome
- Two Sum Sorted
- Remove Duplicates from Sorted Array
- Merged Two Sorted Arrays
- Intersection of Two Arrays II
- Binary Search
- Reverse a String
難度較高
How to Recognize it
我們可以通過幾種方式來辨識two pointer的使用時機:
- O(N^2)降低成O(N):把雙層for迴圈的循環減少到單層迴圈
- Input是有序的:如果input已經排序或是有順序
- 兩個索引的比較:如果我們要將一個索引值跟另一個索引值進行比較
- 兩個索引的交換:我們需要在索引之間交換
- 陣列的分區:如果我們需要將陣列分區,然後在分區中進行操作
Sliding Window
Sliding window 是雙指標的擴展,透過left和right兩個指標來建立window(窗口),通常問題是要我們反回滿足特定條件的最大或最小子範圍。因此我們透過滑動(收縮和擴展)『window』來找到最佳範圍。在這裡我們會使用Max Consecutive Ones III來舉例。
Key Steps
但是在開始之前,我們要先知道Sliding Window的三個關鍵步驟:
- Expand out Window 擴展窗口
- Meet the condition and process the window 滿足條件並處理窗口
- Shrink the window 收縮窗口
以下是代碼中3個關鍵步驟的呈現方式
1 | def sliding_window(nums): |
Other Scenarios
Leetcode 常見的Sliding Window題目:
- Max Consecutive Ones I
- Max Consecutive Ones II
- Max Consecutive Ones III
- Longest Substring with At Most K Distinct Characters
- Longest Substring Without Repeating Characters
- Minimum Window Substring
Example: Max Consecutive Ones III
問題如下,我們可以替換1次0,找到最大的連續1的數量。但其實題目可以換句話說:我們要找到最大的window,裡面最多只有1個0。
1 | MAX CONSECUTIVE ONES II |
那麼,促使我們停止擴大window的條件是什麼呢?
就是當我們的window中0的數量超過1個時,我們就要停止擴大window。因為 0 太多拉!再擴大下去,會遠遠的超過偏差。換句話說我們需要擴展window,直到我們的window中0的數量超過1個,要停下腳步思考一下。
我們可以使用一個變數count
來記錄0的數量:
1 | # CONDITION TO STOP EXPANSION |
那我們重新把這個停止擴大的條件放入我們的sliding window結構
1 | def sliding_window(nums): |
因此,我們透過向右擴展window,直到我們的window中0的數量超過1個,我們就要停止擴展window。但是等等!我們怎麼知道count什麼時候會被增加呢?我們需要在擴展window的時候,檢查我們的window中是否有0,如果有,我們就要增加count。
1 | # 向右擴展window時要檢查是否有0 |
那我們再把它加進去代碼中
1 | def sliding_window(nums): |
很好!現在我們已經擴展了視窗,直到滿足條件為止。那我們接下來要做什麼呢?就是開始尋找我們的最佳解,在這個題目就是找到最大長度的連續1,我們可以透過global_max
來記錄最大的連續1的長度。一但發現更好的解,我們就要更新global_max
。在最後作為結果返回。當我們已經檢查過當前的window是否最長後,我們透過增加left來縮小window,直到我們的window中0的數量小於等於1個。
但是要注意的是,當我們增加left之前,也要檢查left是否會讓我們0的數量遞減,因為已經不再window中了,我們要減少count。
1 | def sliding_window(nums): |
作為最後步驟,我們需要處理邊緣情況,就是最大陣列其實是陣列的尾端。
1 | [1, 0, 0, 1, 1, 1, 1, 1] |
那我們把邊緣狀況加入我們的代碼中:
1 | def sliding_window(nums): |
How to Recognize it
總結來說 Sliding Window 就是需要定義出以下狀況:
- 何時停止擴展?
- 處理當前window的時機?
- 何時收縮window?
因此我們需要3個變數
- window bounds: 通常是 left, right 來表示window的邊界
- Track condition: 通常是一個變數來追蹤滿足條件,這個例子裡面我們使用count
- Return value: 通常是一個變數來記錄最終結果,這個例子裡面我們使用global_max
我們的思考邏輯是這樣的:
- 定義條件以停止擴展
- 展開視窗直到滿足條件,但是在展開視窗前,要在right處檢查或處理元素
- 如果滿足停止擴展的條件,則處理當前視窗
- 縮小視窗,直到滿足條件,但是在縮小視窗前,要在left處檢查或處理元素
- 最後,處理邊緣情況