1. 程式人生 > 其它 >雙指標法

雙指標法

雙指標

雙指標的過程:在遍歷陣列的過程中,定義兩個相同方向或者相反方向的指標進行遍歷,根據兩個指標所在的資料進行處理,從而達到相應的目的。

首尾指標(對撞指標)

對撞指標是指在陣列中,將最左側的索引(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