雙指標法
雙指標
雙指標的過程:在遍歷陣列的過程中,定義兩個相同方向或者相反方向的指標進行遍歷,根據兩個指標所在的資料進行處理,從而達到相應的目的。
首尾指標(對撞指標)
對撞指標是指在陣列中,將最左側的索引(0)定義為左指標(left),最右側的索引(陣列長度-1)定義為右指標(right),然後兩個指標都向中間移動遍歷陣列。
適用場景:連續陣列、字串。
125. 驗證迴文串
https://leetcode-cn.com/problems/valid-palindrome/
給定一個字串,驗證它是否是迴文串,只考慮字母和數字字元,可以忽略字母的大小寫。
說明:本題中,我們將空字串定義為有效的迴文串。
示例 1:
輸入: "A man, a plan, a canal: Panama"
輸出: true
解釋:"amanaplanacanalpanama" 是迴文串
示例 2:
輸入: "race a car"
輸出: false
解釋:"raceacar" 不是迴文串
提示:
1 <= s.length <= 2 * 105
字串 s 由 ASCII 字元組成
""" 使用雙指標。初始時,左右指標分別指向 sgood 的兩側,隨後我們不斷地將這兩個指標相向移動,每次移動一步,並判斷這兩個指標指向的字元是否相同。當這兩個指標相遇時,就說明 sgood 是迴文串。 isalnum() # 判斷是否由數字或字母組成 isdigit() # 判斷全部是由整陣列成 isalpha() # 判斷是否全部由字母組成""" class Solution: def isPalindrome(self, s: str) -> bool: # 去除標點空格等,判斷是否由數字或字母組成 sgood = "".join(ch.lower() for ch in s if ch.isalnum()) left, right = 0, len(sgood) - 1 while left < right: if sgood[left] != sgood[right]:return False left, right = left + 1, right - 1 return True
167. 兩數之和 II - 輸入有序陣列
https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/
給定一個已按照 非遞減順序排列 的整數陣列numbers ,請你從陣列中找出兩個數滿足相加之和等於目標數target 。
函式應該以長度為 2 的整數陣列的形式返回這兩個數的下標值。numbers 的下標 從 1 開始計數 ,所以答案陣列應當滿足 1 <= answer[0] < answer[1] <= numbers.length 。
你可以假設每個輸入 只對應唯一的答案 ,而且你 不可以 重複使用相同的元素。
示例 1:
輸入:numbers = [2,7,11,15], target = 9
輸出:[1,2]
解釋:2 與 7 之和等於目標數 9 。因此 index1 = 1, index2 = 2 。
示例 2:
輸入:numbers = [2,3,4], target = 6
輸出:[1,3]
示例 3:
輸入:numbers = [-1,0], target = -1
輸出:[1,2]
提示:
2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers 按 非遞減順序 排列
-1000 <= target <= 1000
僅存在一個有效答案
""" 初始時兩個指標分別指向第一個元素位置和最後一個元素的位置。每次計算兩個指標指向的兩個元素之和,並和目標值比較。如果兩個元素之和等於目標值,則發現了唯一解。如果兩個元素之和小於目標值,則將左側指標右移一位。如果兩個元素之和大於目標值,則將右側指標左移一位。移動指標之後,重複上述操作,直到找到答案 """ class Solution: def twoSum(self, numbers: List[int], target: int) -> List[int]: left, right = 0, len(numbers) - 1 while left < right: if numbers[left] + numbers[right] > target: right -= 1 elif numbers[left] + numbers[right] < target: left += 1 else: return [left + 1, right + 1] return [-1, -1]
11. 盛最多水的容器
https://leetcode-cn.com/problems/container-with-most-water/
給你 n 個非負整數 a1,a2,...,an,每個數代表座標中的一個點(i,ai) 。在座標內畫 n 條垂直線,垂直線 i的兩個端點分別為(i,ai) 和 (i, 0) 。找出其中的兩條線,使得它們與x軸共同構成的容器可以容納最多的水。
說明:你不能傾斜容器。
示例 1:
輸入:[1,8,6,2,5,4,8,3,7]
輸出:49
解釋:圖中垂直線代表輸入陣列 [1,8,6,2,5,4,8,3,7]。在此情況下,容器能夠容納水(表示為藍色部分)的最大值為49。
示例 2:
輸入:height = [1,1]
輸出:1
示例 3:
輸入:height = [4,3,2,1,4]
輸出:16
示例 4:
輸入:height = [1,2,1]
輸出:2
提示:
n == height.length
2 <= n <= 105
0 <= height[i] <= 104
""" 本題是一道經典的面試題,最優的做法是使用「雙指標」。如果讀者第一次看到這題,不一定能想出雙指標的做法。 我們先從題目中的示例開始,一步一步地解釋雙指標演算法的過程。稍後再給出演算法正確性的證明。 題目中的示例為: [1, 8, 6, 2, 5, 4, 8, 3, 7] 在初始時,左右指標分別指向陣列的左右兩端,它們可以容納的水量為 min(1, 7) * 8 = 8 # 8是下標的長度,即寬。 此時我們需要移動一個指標。移動哪一個呢?直覺告訴我們,應該移動對應數字較小的那個指標(即此時的左指標)。 這是因為,由於容納的水量是由: 兩個指標指向的數字中較小值 * 指標之間的距離 決定的。 如果我們移動數字較大的那個指標,那麼前者「兩個指標指向的數字中較小值」不會增加,後者「指標之間的距離」會減小, 那麼這個乘積會減小。因此,我們移動數字較大的那個指標是不合理的。因此,我們移動 數字較小的那個指標。 所以,我們將左指標向右移動: [1, 8, 6, 2, 5, 4, 8, 3, 7] ^ ^ 此時可以容納的水量為 min(8,7)*7=49。由於右指標對應的數字較小,我們移動右指標: [1, 8, 6, 2, 5, 4, 8, 3, 7] ^ ^ 此時可以容納的水量為 min(8,3)*6=18。由於右指標對應的數字較小,我們移動右指標: [1, 8, 6, 2, 5, 4, 8, 3, 7] ^ ^ 此時可以容納的水量為 min(8,8)*5=40。兩指標對應的數字相同,我們可以任意移動一個,例如左指標: [1, 8, 6, 2, 5, 4, 8, 3, 7] ^ ^ 此時可以容納的水量為 min(6,8)*4=24。由於左指標對應的數字較小,我們移動左指標 按這個規律一直移動指標,直到兩個指標重合。 在我們移動指標的過程中,計算到的最多可以容納的數量為 49,即為最終的答案。 """ class Solution: def maxArea(self, height: List[int]) -> int: max_area = 0 left, right = 0, len(height) - 1 while left < right: if height[left] <= height[right]: area = height[left] * (right - left) max_area = max(max_area, area) left += 1 else: area = height[right] * (right - left) max_area = max(max_area, area) right -= 1 return max_area
快慢指標
快慢指標:兩個指標從同一側開始遍歷陣列,將這兩個指標分別定義為快指標(fast)和慢指標(slow),
兩個指標以不同的策略移動,直到兩個指標的值相等(或其他特殊條件)為止,如 fast 每次增長兩個,slow 每次增長一個。
141. 環形連結串列
https://leetcode-cn.com/problems/linked-list-cycle/
給定一個連結串列,判斷連結串列中是否有環。
如果連結串列中有某個節點,可以通過連續跟蹤 next 指標再次到達,則連結串列中存在環。 為了表示給定連結串列中的環,我們使用整數 pos 來表示連結串列尾連線到連結串列中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該連結串列中沒有環。注意:pos 不作為引數進行傳遞,僅僅是為了標識連結串列的實際情況。
如果連結串列中存在環,則返回 true 。 否則,返回 false 。
進階:
你能用 O(1)(即,常量)記憶體解決此問題嗎?
示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:true
解釋:連結串列中有一個環,其尾部連線到第二個節點。
示例2:
輸入:head = [1,2], pos = 0
輸出:true
解釋:連結串列中有一個環,其尾部連線到第一個節點。
示例 3:
輸入:head = [1], pos = -1
輸出:false
解釋:連結串列中沒有環。
提示:
連結串列中節點的數目範圍是 [0, 104]
-105 <= Node.val <= 105
pos 為 -1 或者連結串列中的一個 有效索引 。
方法1:遍歷
""" 最容易想到的方法是遍歷所有節點,每次遍歷到一個節點時,判斷該節點此前是否被訪問過。 具體地,我們可以使用雜湊表來儲存所有已經訪問過的節點。每次我們到達一個節點,如果該節點已經存在於雜湊表中,則說明該連結串列是環形連結串列,否則就將該節點加入雜湊表中。重複這一過程,直到我們遍歷完整個連結串列即可。 """ # Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None class Solution: def hasCycle(self, head: ListNode) -> bool: seen = set() while head: if head in seen: return True seen.add(head) head = head.next return False
方法2:快慢指標
""" 本方法需要讀者對「Floyd 判圈演算法」(又稱龜兔賽跑演算法)有所瞭解。 假想「烏龜」和「兔子」在連結串列上移動,「兔子」跑得快,「烏龜」跑得慢。當「烏龜」和「兔子」從連結串列上的同一個節點開始移動時,如果該連結串列中沒有環,那麼「兔子」將一直處於「烏龜」的前方;如果該連結串列中有環,那麼「兔子」會先於「烏龜」進入環,並且一直在環內移動。等到「烏龜」進入環時,由於「兔子」的速度快,它一定會在某個時刻與烏龜相遇,即套了「烏龜」若干圈。 我們可以根據上述思路來解決本題。具體地,我們定義兩個指標,一快一滿。慢指標每次只移動一步,而快指標每次移動兩步。初始時,慢指標在位置 head,而快指標在位置 head.next。這樣一來,如果在移動的過程中,快指標反過來追上慢指標,就說明該連結串列為環形連結串列。否則快指標將到達連結串列尾部,該連結串列不為環形連結串列。 為什麼我們要規定初始時慢指標在位置 head,快指標在位置 head.next,而不是兩個指標都在位置 head(即與「烏龜」和「兔子」中的敘述相同)? 觀察下面的程式碼,我們使用的是 while 迴圈,迴圈條件先於迴圈體。由於迴圈條件一定是判斷快慢指標是否重合,如果我們將兩個指標初始都置於 head,那麼 while 迴圈就不會執行。因此,我們可以假想一個在 head 之前的虛擬節點,慢指標從虛擬節點移動一步到達 head,快指標從虛擬節點移動兩步到達 head.next,這樣我們就可以使用 while 迴圈了。 當然,我們也可以使用 do-while 迴圈。此時,我們就可以把快慢指標的初始值都置為 head。 """ # Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None class Solution: def hasCycle(self, head: ListNode) -> bool: if not head or not head.next: return False slow = head fast = head.next while slow != fast: if not fast or not fast.next: # 無環的時候,快指標能夠到達連表末尾 return False slow = slow.next fast = fast.next.next return True
26. 刪除有序陣列中的重複項
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/
給你一個有序陣列 nums ,請你 原地 刪除重複出現的元素,使每個元素 只出現一次 ,返回刪除後陣列的新長度。
不要使用額外的陣列空間,你必須在 原地 修改輸入陣列 並在使用 O(1) 額外空間的條件下完成。
說明:
為什麼返回數值是整數,但輸出的答案是陣列呢?
請注意,輸入陣列是以「引用」方式傳遞的,這意味著在函式裡修改輸入陣列對於呼叫者是可見的。
你可以想象內部操作如下:
// nums 是以“引用”方式傳遞的。也就是說,不對實參做任何拷貝
int len = removeDuplicates(nums);
// 在函式裡修改輸入陣列對於呼叫者是可見的。
// 根據你的函式返回的長度, 它會打印出陣列中 該長度範圍內 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
輸入:nums = [1,1,2]
輸出:2, nums = [1,2]
解釋:函式應該返回新的長度 2 ,並且原陣列 nums 的前兩個元素被修改為 1, 2 。不需要考慮陣列中超出新長度後面的元素。
示例 2:
輸入:nums = [0,0,1,1,1,2,2,3,3,4]
輸出:5, nums = [0,1,2,3,4]
解釋:函式應該返回新的長度 5 , 並且原陣列 nums 的前五個元素被修改為 0, 1, 2, 3, 4 。不需要考慮陣列中超出新長度後面的元素。
提示:
0 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
nums 已按升序排列
""" 這道題目的要求是:對給定的有序陣列 nums 刪除重複元素,在刪除重複元素之後,每個元素只出現一次,並返回新的長度,上述操作必須通過原地修改陣列的方法,使用 O(1) 的空間複雜度完成。 由於給定的陣列 nums 是有序的,因此對於任意 i<j,如果 nums[i]=nums[j],則對任意 i≤k≤j,必有 nums[i]=nums[k]=nums[j],即相等的元素在陣列中的下標一定是連續的。利用陣列有序的特點,可以通過雙指標的方法刪除重複元素。 如果陣列 nums 的長度為 0,則陣列不包含任何元素,因此返回 0。 當陣列 nums 的長度大於 0 時,陣列中至少包含一個元素,在刪除重複元素之後也至少剩下一個元素,因此 nums[0] 保持原狀即可,從下標 1 開始刪除重複元素。 定義兩個指標fast 和 slow 分別為快指標和慢指標,快指標表示遍歷陣列到達的下標位置,慢指標表示下一個不同元素要填入的下標位置,初始時兩個指標都指向下標 1。 假設陣列 nums 的長度為 n。將快指標 fast 依次遍歷從 1 到 n−1 的每個位置,對於每個位置,如果 nums[fast] != nums[fast-1] 說明 nums[fast] 和之前的元素都不同,因此將 nums[fast] 的值複製到 nums[slow],然後將 slow 的值加 1,即指向下一個位置。 遍歷結束之後,從nums[0] 到 nums[slow−1] 的每個元素都不相同且包含原陣列中的每個不同的元素,因此新的長度即為 slow,返回 slow 即可。 """ class Solution: def removeDuplicates(self, nums: List[int]) -> int: if not nums: return 0 n = len(nums) fast = slow = 1 while fast < n: if nums[fast] != nums[fast - 1]: nums[slow] = nums[fast] slow += 1 fast += 1 return slow
80. 刪除有序陣列中的重複項 II
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/
給你一個有序陣列 nums ,請你 原地 刪除重複出現的元素,使每個元素 最多出現兩次 ,返回刪除後陣列的新長度。
不要使用額外的陣列空間,你必須在 原地 修改輸入陣列 並在使用 O(1) 額外空間的條件下完成。
說明:
為什麼返回數值是整數,但輸出的答案是陣列呢?
請注意,輸入陣列是以「引用」方式傳遞的,這意味著在函式裡修改輸入陣列對於呼叫者是可見的。
你可以想象內部操作如下:
// nums 是以“引用”方式傳遞的。也就是說,不對實參做任何拷貝
int len = removeDuplicates(nums);
// 在函式裡修改輸入陣列對於呼叫者是可見的。
// 根據你的函式返回的長度, 它會打印出陣列中 該長度範圍內 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
輸入:nums = [1,1,1,2,2,3]
輸出:5, nums = [1,1,2,2,3]
解釋:函式應返回新長度 length = 5, 並且原陣列的前五個元素被修改為 1, 1, 2, 2, 3 。 不需要考慮陣列中超出新長度後面的元素。
示例 2:
輸入:nums = [0,0,1,1,1,1,2,3,3]
輸出:7, nums = [0,0,1,1,2,3,3]
解釋:函式應返回新長度 length = 7, 並且原陣列的前五個元素被修改為0, 0, 1, 1, 2, 3, 3 。 不需要考慮陣列中超出新長度後面的元素。
提示:
1 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
nums 已按升序排列
""" 因為給定陣列是有序的,所以相同元素必然連續。我們可以使用雙指標解決本題,遍歷陣列檢查每一個元素是否應該被保留,如果應該被保留,就將其移動到指定位置。 具體地,我們定義兩個指標 slow 和 fast 分別為慢指標和快指標,其中慢指標表示處理出的陣列的長度,快指標表示已經檢查過的陣列的長度,即 nums[fast] 表示待檢查的第一個元素,nums[slow−1] 為上一個應該被保留的元素所移動到的指定位置。 因為本題要求相同元素最多出現兩次而非一次,所以我們需要檢查上上個應該被保留的元素 nums[slow−2] 是否和當前待檢查元素 nums[fast] 相同。 當且僅當 nums[slow−2]=nums[fast] 時,當前待檢查元素 nums[fast] 不應該被保留(因為此時必然有 nums[slow−2]=nums[slow−1]=nums[fast])。最後 slow 即為處理好的陣列的長度。 特別地,陣列的前兩個數必然可以被保留,因此對於長度不超過 2 的陣列,我們無需進行任何處理,對於長度超過 2 的陣列,我們直接將雙指標的初始值設為 2 即可。 """ class Solution: def removeDuplicates(self, nums: List[int]) -> int: n = len(nums) if n < 3: return n fast = slow = 2 while fast < n: if nums[fast] != nums[slow - 2]: nums[slow] = nums[fast] slow += 1 fast += 1 return slow