LeetCode之双指针

328 阅读9分钟

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。

1.1 有序数组中的两数之和

给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。 函数应该返回这两个下标值 index1index2,其中 index1 必须小于 index2

返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。

  • 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
  • 如果 sum > target,移动较大的元素,使 sum 变小一些;
  • 如果 sum < target,移动较小的元素,使 sum 变大一些。

数组中的元素最多遍历一次,时间复杂度为 O(N)。只使用了两个额外变量,空间复杂度为 O(1)

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left = 0
        right = len(numbers) - 1
        while left <= right:
            sum_ = numbers[left] + numbers[right]
            if sum_ == target:
                return [left + 1, right + 1]  # 题目要求索引是从 1 开始的
            elif sum_ < target:
                left += 1  # 让 sum_ 小一点
            elif sum_ > target:
                right -= 1  # 让 sum_ 大一点
        return [-1, -1]

1.2 无序数组中的两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

1. 暴力法:

def twoSum(nums, target):
    for i in range(len(nums) - 1):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j]:
                return [i, j]
    return [-1, -1]  # 不存在这么两数

这个解法非常直接,时间复杂度 O(N^2),空间复杂度 O(1)

2. 哈希表:

通过一个哈希表减少时间复杂度:

def twoSum(nums, target):
    hashmap={}
    for index, num in enumerate(nums):  # 构造一个哈希表,元素映射到相应的索引
        hashmap[num] = index
        
    for i, num in enumerate(nums):
        j = hashmap.get(target - num)
        if j is not None and i != j:  # 如果 j 存在且不是 i 本身
            return [i, j]

由于哈希表的查询时间为 O(1),算法的时间复杂度降低到 O(N),但是需要 O(N) 的空间复杂度来存储哈希表。不过综合来看,是要比暴力解法高效的。

2. 两数平方和

给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c

输入: 5
输出: True
解释: 1 * 1 + 2 * 2 = 5

输入: 3
输出: False

可以看成是在元素为 0 — c 的有序数组中查找两个数,使得这两个数的平方和为 c,如果能找到,则返回 true,表示 c 是两个整数的平方和。

事实上,右指针并不需要初始化到 c,初始化到 int(sqrt(c)) 即可。

因为最多只需要遍历一次 0 — int(sqrt(c)),所以时间复杂度为 O(int(sqrt(c)))。又因为只使用了两个额外的变量,因此空间复杂度为 O(1)

class Solution:
    def judgeSquareSum(self, c: int) -> bool:
        if c < 0: return False
        left, right = 0, int(sqrt(c))
        while left <= right:
            sum_ = left ** 2 + right ** 2
            if sum_ == c:
                return True
            elif sum_ < c:
                left += 1
            else:
                right -= 1
        return False

3. 反转字符串中的元音字符

编写一个函数,以字符串作为输入,反转该字符串中的元音字母。

输入: "hello"
输出: "holle"

输入: "leetcode"
输出: "leotcede"

使用双指针:

一个指针从头向尾遍历,一个指针从尾到头遍历,当两个指针都遍历到元音字符时,交换这两个元音字符。

为了快速判断一个字符是不是元音字符,我们将全部元音字符添加到集合 vowel 中,从而以 O(1) 的时间复杂度进行该操作。

  • 时间复杂度为 O(N):只需要遍历所有元素一次
  • 空间复杂度 O(1):只需要使用两个额外变量
class Solution:
    def reverseVowels(self, s: str) -> str:
        vowel = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'}
        if not s: return ""
        left, right = 0, len(s) - 1
        res = [None] * len(s)

        while left <= right:
            if s[left] not in vowel:
                res[left] = s[left]
                left += 1
            elif s[right] not in vowel:
                res[right] = s[right]
                right -= 1
            else:
                res[left] = s[right]
                res[right] = s[left]
                left += 1
                right -= 1
        return ''.join(res)

使用栈:

利用栈先进后出的特点,将字符串 s 中的元音字符存入栈中。循环遍历字符串元素,当遇到元音字符时,从栈中弹出的元素即为此元音字符所要反转的字符。

class Solution:
    def reverseVowels(self, s: str) -> str:
        vowel = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'}
        ls = []
        vs = [i for i in s if i in vowel]  # 将 s 中的元音字符存入栈中
        
        for i in s:
            if i not in vowel:
                ls.append(i)
            else:
                ls.append(vs.pop())
        return ''.join(ls)

4. 回文字符串

给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。

输入: "aba"
输出: True

输入: "abca"
输出: True
解释: 你可以删除c字符。

注意:字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。

  • 本题的关键是处理删除一个字符。在使用双指针遍历字符串时,如果出现两个指针指向的字符不相等的情况,我们就试着删除一个字符,再判断删除完之后的字符串是否是回文字符串。
  • 在判断是否为回文字符串时,我们不需要判断整个字符串,因为左指针左边和右指针右边的字符之前已经判断过具有对称性质,所以只需要判断中间的子字符串即可。
  • 在试着删除字符时,我们既可以删除左指针指向的字符,也可以删除右指针指向的字符。
class Solution:
    def validPalindrome(self, s: str) -> bool:
        def isPalindrome(s, i, j):
            while i < j:
                if s[i] != s[j]:
                    return False
                i += 1
                j -= 1
            return True

        left, right = 0, len(s) - 1
        while left < right:
            if s[left] != s[right]:
                return isPalindrome(s, left, right - 1) or isPalindrome(s, left + 1, right)
            left += 1
            right -= 1
        return True

5. 归并两个有序数组

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6],       n = 3
输出: [1,2,2,3,5,6]

需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值。

class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        """
        Do not return anything, modify nums1 in-place instead.
        """
        index1 = m - 1
        index2 = n - 1
        index_merge = m + n - 1

        while index1 >= 0 or index2 >= 0:
            if index1 < 0:
                nums1[index_merge] = nums2[index2]
                index2 -= 1
            elif index2 < 0:
                nums1[index_merge] = nums1[index1]
                index1 -= 1
            elif nums1[index1] > nums2[index2]:
                nums1[index_merge] = nums1[index1]
                index1 -= 1
            else:
                nums1[index_merge] = nums2[index2]
                index2 -= 1
            index_merge -= 1

6.1 判断链表是否有环

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

使用快慢指针,一个跑得快,一个跑得慢。如果不含有环,跑得快的那个指针最终会遇到 None,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。

class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        fast = slow = head
        while fast and fast.next:
            fast = fast.next.next
            slow = slow.next
            if fast == slow:
                return True
        return False

6.2 链表中环的起点

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。

第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。

设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。

巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。

所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。

class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        fast = slow = head
        while True:
            if fast == None or fast.next == None:
                return 
            fast = fast.next.next
            slow = slow.next
            if fast == slow:
                break
            
        slow = head
        while slow != fast:
            fast = fast.next
            slow = slow.next
        return slow

6.3 寻找链表的中点

类似上面的思路,我们还可以让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置

while fast or fast.next:
    fast = fast.next.next
    slow = slow.next
return slow  # slow 就在中间位置

当链表的长度是奇数时,slow 恰巧停在中点位置;如果长度是偶数,slow 最终的位置是中间偏右。

6.4 寻找链表的倒数第 k 个元素

思路还是使用快慢指针,让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点(为了简化,假设 k 不会超过链表长度):

slow = fast = head
while k > 0:
    fast = fast.next
    k -= 1
while fast != None:
    slow = slow.next
    fast = fast.next
return slow