Python算法-双指针(Two Pointers)

双指针分为「对撞指针」、「快慢指针」、「分离双指针」。

  • 参考来源:https://algo.itcharge.cn/

对撞指针:两个指针方向相反。适合解决查找有序数组中满足某些约束条件的一组元素问题、字符串反转问题。
快慢指针:两个指针方向相同。适合解决数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。
分离双指针:两个指针分别属于不同的数组 / 链表。适合解决有序数组合并,求交集、并集问题。

125. 验证回文串

输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串

  • 对撞指针
class Solution:
    def isPalindrome(self, s: str) -> bool:
        # 特殊情况:空字符串
        if s == " ":
            return True
        left = 0
        right = len(s) - 1
        while left < right:
            # 使用 isalnum() 函数检测字符串 str 是否由字母和数字组成
            if not s[left].isalnum():
                left += 1
                continue
            if not s[right].isalnum():
                right -= 1
                continue
            # 使用 lower() 函数将字符统一转换为小写             
            if s[left].lower() == s[right].lower():
                left += 1
                right -= 1
            else:
                return False

        return True 
11. 盛最多水的容器

输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

  • 对撞指针
class Solution:
    def maxArea(self, height: List[int]) -> int:
        result = 0
        left = 0
        right = len(height) - 1
        while left < right:
            # 求解矩形的面积
            l = right - left
            h = min(height[left], height[right])
            area = l*h
            # 需要不断维持更新最大值
            result = max(result, area)
            # 应该使得 较低直线的高度尽可能的高
            # 当left指向的直线高度较低,向右移动
            if height[left] < height[right]:
                left += 1
            # 当right指向的直线高度较低,向左移动
            else:
                right -= 1
        return result
26. 删除有序数组中的重复项

输入:nums = [1,1,2]
输出:2, nums = [1,2]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

  • 快慢指针
class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        # 定义两个指针
        slow = 0
        fast = 1
        # 可以视作把非重复元素放在数组左边
        while fast < len(nums):
            if nums[slow] != nums[fast]:
                slow += 1
                nums[slow] = nums[fast]
            fast += 1
        return slow + 1
349. 两个数组的交集

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

  • 分离指针
class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        # 分离双指针一般用于处理有序数组合并,求交集、并集问题
        # 1 先将两个数组排序
        nums1.sort()
        nums2.sort()
        # 使用双指针求交集
        point1 = 0
        point2 = 0
        result = []
        while point1 < len(nums1) and point2 < len(nums2):
            # 元素同时出现在两个数组
            if nums1[point1] == nums2[point2]:
                # 保证数组没有重复元素
                if nums1[point1] not in result:
                    result.append(nums1[point1])
                # 齐头并进
                point1 += 1
                point2 += 1
            # point1落后于point2,需要追赶
            elif nums1[point1] < nums2[point2]:
                point1 += 1
            # point2落后于point1,需要追赶   
            else:
                point2 += 1
        return result
344. 反转字符串

输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

  • 对撞指针,从两侧向中间进行遍历
class Solution:
    def reverseString(self, s: List[str]) -> None:
        """
        Do not return anything, modify s in-place instead.
        """
        left = 0
        right = len(s) - 1
        while left < right:
            s[left], s[right] = s[right], s[left]
            left += 1
            right -= 1
        return s
345. 反转字符串中的元音字母

给你一个字符串 s ,仅反转字符串中的所有元音字母,并返回结果字符串。
元音字母包括 'a'、'e'、'i'、'o'、'u',且可能以大小写两种形式出现。

输入:s = "hello"
输出:"holle"

  • 非同步的双指针,具体情况具体分析
class Solution:
    def reverseVowels(self, s: str) -> str:
        alphabet = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U']
        left = 0
        right = len(s) - 1
        s_list = list(s)
        while left < right:
            # 左指针向右走
            if s_list[left] not in alphabet:
                left += 1
                continue
            # 右指针向左走
            elif s_list[right] not in alphabet:
                right -= 1
                continue
            elif s_list[left] in alphabet and s_list[right] in alphabet:
                s_list[left], s_list[right] = s_list[right], s_list[left]
                left += 1
                right -= 1
            
        return "".join(s_list)
125. 验证回文串

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。

输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串

class Solution:
    def isPalindrome(self, s: str) -> bool:
        # 特殊情况:空字符串
        if s == " ":
            return True
        left = 0
        right = len(s) - 1
        while left < right:
            # 使用 isalnum() 函数检测字符串 str 是否由字母和数字组成
            if not s[left].isalnum():
                left += 1
                continue
            if not s[right].isalnum():
                right -= 1
                continue
            # 使用 lower() 函数将字符统一转换为小写             
            if s[left].lower() == s[right].lower():
                left += 1
                right -= 1
            else:
                return False

        return True
611. 有效三角形的个数

给定一个包含非负整数的数组,你的任务是统计其中可以组成三角形三条边的三元组个数。

输入: [2,2,3,4]
输出: 3
解释:
有效的组合是:
2,3,4 (使用第一个 2)
2,3,4 (使用第二个 2)
2,2,3

  • 双层循环
class Solution:
    def triangleNumber(self, nums: List[int]) -> int:
        # 给定数组得到三元组
        # 三角形定理:任意两条边之和小于第三条边
        # 对于排序好的三元组[a, b, c],只需要满足 a+b > c
        result = 0
        nums.sort()
        length = len(nums)
        # 保证全部遍历[left, right+1]
        for i in range(2, length):
            left = 0
            right = i - 1
            # 当left < right 时,跳出小循环
            while left < right:
                # 找到满足条件的左边界
                if nums[left] + nums[right] <= nums[i]:
                    left += 1
                else:
                    result += (right - left)
                    right -= 1
        return result
16. 最接近的三数之和

给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。
返回这三个数的和。
假定每组输入只存在恰好一个解。

输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        n = len(nums)
        result = float('inf')
        for i in range(2, n):
            left = 0
            right = i-1
            while left < right:
                total = nums[left] + nums[right] + nums[i] 
                if abs(target-total) < abs(target-result):
                    result = total
                if total < target:
                    left += 1
                else:
                    right -= 1
        return result
15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        result = []                  # 结果输出
        n = len(nums)
        # 先将数组递增排列
        nums.sort()
        # 定义双指针 a, left, right
        for i in range(n):
            if i > 0 and nums[i] == nums[i-1]:
                continue
            left = i+1
            right = n-1
            # 双指针寻找区间
            while left < right:
                # 答案中不可以包含重复的三元组,对于重复的元素直接跳过
                while left < right and left > i+1 and nums[left] == nums[left-1]:
                    left += 1
                while left < right and right < n-1 and nums[right] == nums[right+1]:
                    right -= 1
                # 满足条件的三元组
                if left < right and nums[i] + nums[left] + nums[right] == 0:
                    result.append([nums[i], nums[left], nums[right]])
                    left += 1
                    right -= 1
                elif nums[i] + nums[left] + nums[right] > 0:
                    right -= 1
                else:
                    left += 1
        return result
18. 四数之和

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
(1)0 <= a, b, c, d < n
(2)a、b、c 和 d 互不相同
(3)nums[a] + nums[b] + nums[c] + nums[d] == target
(4)你可以按 任意顺序 返回答案 。

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        result = []
        n = len(nums)
        nums.sort()
        for i in range(n):
            # 直接跳过相同元素
            if i > 0 and nums[i] == nums[i-1]:
                continue
            for j in range(i+1,n):
                if j > i+1 and nums[j] == nums[j-1]:
                    continue
                left = j+1
                right = n-1
                while left < right:
                    while left < right and left > j+1 and nums[left] == nums[left-1]:
                        left += 1
                    while left < right and right < n-1 and nums[right] == nums[right+1]:
                        right -= 1
                    if left < right and nums[i] + nums[j] + nums[left] + nums[right] == target:
                        result.append([nums[i], nums[j], nums[left], nums[right]])
                        left += 1
                        right -= 1
                    elif left < right and nums[i] + nums[j] + nums[left] + nums[right] < target:
                        left += 1
                    else:
                        right -= 1
        return result
283. 移动零

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

# 思路:双指针
class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        left = right = 0
        while right < len(nums):
            if nums[right] != 0:
                nums[left], nums[right] = nums[right], nums[left]
                left += 1
            right += 1
27. 移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        left = 0
        right = 0
        # 将非 val 值的元素进行前移,left 指针左边均为处理好的非 val 值元素,
        # 而从 left 指针指向的位置开始, right 指针左边都为 val 值
        # 1 [3,2,2,3]
        # 2 [3,3,2,2]  left += 1 --> 2
        while right < len(nums):
            if nums[right] != val:
                nums[left], nums[right] = nums[right], nums[left]
                left += 1
            right += 1
        return left
80. 删除有序数组中的重复项 II

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 最多出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow = 2
        fast = 2
        # 因为有序,所以当 nums[slow-2] = nums[slow]时,
        # 必有nums[slow] = nums[slow-1] = nums[slow-2]
        # 不相等时进行添加
        while fast < len(nums):
            if nums[slow-2] != nums[fast]:
                nums[slow] = nums[fast]
                slow += 1
            fast += 1
        return slow
845. 数组中的最长山脉

给出一个整数数组 arr,返回最长山脉子数组的长度。如果不存在山脉子数组,返回 0 。

输入:arr = [2,1,4,7,3,2,5]
输出:5
解释:最长的山脉子数组是 [1,4,7,3,2],长度为 5。

class Solution:
    def longestMountain(self, arr: List[int]) -> int:
        # 使用变量 ans 保存最长山脉长度。
        # 遍历数组,假定当前节点为山峰。
        # 使用双指针 left、right 分别向左、向右查找山脉的长度。
        # 如果当前山脉的长度比最长山脉长度更长,则更新最长山脉长度。
        # 最后输出 ans。
        length = len(arr)
        result = 0
        for i in range(1, length-1):
            # 指定当前节点为山峰
            if arr[i] > arr[i-1] and arr[i] > arr[i+1]:
                left = i-1
                right = i+1
                # 山峰长度左右扩展
                while left > 0 and arr[left-1] < arr[left]:
                    left -= 1
                while right < length-1 and arr[right+1] < arr[right]:
                    right += 1
                # 更新山峰长度
                if right - left + 1 > result:
                    result = right - left + 1
        return result
88. 合并两个有序数组

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

        # 定义两个指针,分别指向数组的尾部
        p1 = m-1
        p2 = n-1
        p = m + n - 1
        while p1 >= 0 and p2 >= 0:
            if nums1[p1] <= nums2[p2]:
                nums1[p] = nums2[p2]
                p2 -= 1
            else:
                nums1[p] = nums1[p1]
                p1 -= 1
            p -= 1
        # 最后把nums2中的剩余元素赋值到nums1中
        nums1[:p2+1] = nums2[:p2+1]
334. 递增的三元子序列

输入:nums = [1,2,3,4,5]
输出:true
解释:任何 i < j < k 的三元组都满足题意
输入:nums = [2,1,5,0,4,6]
输出:true
解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] == 0 < nums[4] == 4 < nums[5] == 6

class Solution:
    def increasingTriplet(self, nums: List[int]) -> bool:
        # 先定义两个极大值
        a = float('inf')
        b = float('inf')
        # 然后寻找连续三个递增的数组
        for num in nums:
            # 使a尽可能小
            if num <= a:
                a = num
            # 使b尽可能小
            elif num <= b:
                b = num
            # 在同时满足上面两个条件的情况下
            else:
                return True
        return False
978. 最长湍流子数组

输入:[9,4,2,10,7,8,8,1,9]
输出:5
解释:(A[1] > A[2] < A[3] > A[4] < A[5])
输入:[4,8,12,16]
输出:2
输入:[100]
输出:1

class Solution:
    def maxTurbulenceSize(self, arr: List[int]) -> int:
        # 大 小 大 小。。。
        left = 0
        right = 1
        result = 1                        # 当数组长度为1时,本身即为湍流数组
        while right < len(arr):
            if arr[right-1] == arr[right]:
                left = right
            elif right != 1 and arr[right-2] < arr[right-1] < arr[right]:
                left = right - 1
            elif right != 1 and arr[right-2] > arr[right-1] > arr[right]:
                left = right - 1
            # 更新子数组的最大长度
            result = max(result, right-left+1)
            right += 1
        return result
719. 找出第 k 小的距离对

输入:
nums = [1,3,1]
k = 1
输出:0
解释:
所有数对如下:
(1,3) -> 2
(1,1) -> 0
(3,1) -> 2
因此第 1 个最小距离的数对是 (1,1),它们之间的距离为 0。

class Solution:
    def smallestDistancePair(self, nums: List[int], k: int) -> int:
        # 计算两个数之间的距离满足指定长度dist的个数count
        # 计算 nums[right] 和 nums[left] 之间的距离,如果大于 mid,则 left 向右移动,
        # 直到 距离小于等于 mid 时,统计当前距离对数为 right - left。
        # 最终将这些符合要求的距离对数累加,就得到了所有小于等于 mid 的距离对数目。
        def get_count(dist):
            left = 0
            count = 0
            for right in range(1, len(nums)):
                while nums[right] - nums[left] > dist:
                    left += 1
                count += (right - left)
            return count

        # 二分查找法寻找k的位置
        # 我们可以在这个区间上进行二分,
        # 对于二分的位置 mid,统计距离小于等于 mid 的距离对数,并根据它和 k 的关系调整区间上下界。
        # 距离的区间范围[0, nums[-1] - nums[0]]
        nums.sort()
        left = 0
        right = nums[-1] - nums[0]
        while left < right:
            mid = left + (right - left)//2
            if get_count(mid) >= k:
                right = mid
            else:
                left = mid + 1
        return left
350. 两个数组的交集 II

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]

# 方法1
class Solution:
    def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
        mapping1 = {}
        mapping2 = {}
        res = []
        # 分别统计两个数组中元素出现的频率
        for num in nums1:
            if num in mapping1:
                mapping1[num] += 1
            else:
                mapping1[num] = 1
        for num in nums2:
            if num in mapping2:
                mapping2[num] += 1
            else:
                mapping2[num] = 1
        # 对同时出现的元素进行筛选
        for item in mapping1:
            if item in mapping2 and mapping1[item] <= mapping2[item]:
                for i in range(mapping1[item]):
                    res.append(item)
            if item in mapping2 and mapping1[item] > mapping2[item]:
                for i in range(mapping2[item]):
                    res.append(item)

        # print(mapping1)
        # print(mapping2)
        return res

# 方法2
class Solution:
    def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
        mapping = {}
        result = []
        for num1 in nums1:
            if num1 in mapping:
                mapping[num1] += 1
            else:
                mapping[num1] = 1
        for num2 in nums2:
            if num2 in mapping and mapping[num2] != 0:
                mapping[num2] -= 1
                result.append(num2) 
        return result
925. 长按键入

输入:name = "alex", typed = "aaleex"
输出:true
解释:'alex' 中的 'a' 和 'e' 被长按。

class Solution:
    def isLongPressedName(self, name: str, typed: str) -> bool:
        left_1 = 0
        left_2 = 0
        n1 = len(name)
        n2 = len(typed)
        while left_1 < n1 and left_2 < n2:
            if name[left_1] == typed[left_2]:
                left_1 += 1
                left_2 += 1
            # 跳过重复元素
            elif left_2 > 0 and typed[left_2 - 1] == typed[left_2]:
                left_2 += 1
            # 当跳过之后两个元素不相等,则输出False
            else:
                return False 
        # 跳过typed中多余的重复元素
        while 0 < left_2 < n2 and typed[left_2-1] == typed[left_2]:
            left_2 += 1
        
        # 最后判断,如果 left_1 == len(name) 并且 left_2 == len(typed),
        # 则说明匹配,返回 True,否则返回 False。
        if left_1 == n1 and left_2 == n2:
            return True
        else:
            return False
844. 比较含退格的字符串

输入:s = "ab#c", t = "ad#c"
输出:true
解释:S 和 T 都会变成 “ac”。

# 模拟算法
class Solution:
    def backspaceCompare(self, s: str, t: str) -> bool:
        list1 = []
        list2 = []
        for i in s:
            if i != '#':
                list1.append(i)
            elif list1 and i == '#':
                list1.pop()
        for j in t:
            if j != '#':
                list2.append(j)
            elif list2 and j == '#':
                list2.pop()
        return list1 == list2

# 双指针
class Solution:
    def backspaceCompare(self, s: str, t: str) -> bool:
        i = len(s) - 1
        j = len(t) - 1
        skips = 0
        skipt = 0
        while i >= 0 or j >= 0:
            # 处理数组尾部的 #
            while i >= 0:
                if s[i] == '#':
                    skips += 1
                    i -= 1
                elif skips > 0:
                    skips -= 1
                    i -= 1
                else:
                    break
            while j >= 0:
                if t[j] == '#':
                    skipt += 1
                    j -= 1
                elif skipt > 0:
                    skipt -= 1
                    j -= 1
                else:
                    break

            if i >= 0 and j >= 0:
                if s[i] != t[j]:
                    return False
            elif i >=0 or j >= 0:
                return False
            i -= 1
            j -= 1
        return True
917. 仅仅反转字母

给你一个字符串 s ,根据下述规则反转字符串:

  • 所有非英文字母保留在原有位置。
  • 所有英文字母(小写或大写)位置反转。
    返回反转后的 s 。

输入:s = "Test1ng-Leet=code-Q!"
输出:"Qedo1ct-eeLg=ntse-T!"

# 常规思路
class Solution:
    def reverseOnlyLetters(self, s: str) -> str:
        alpha = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
        'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
        s_item = []
        for item in s:
            if item in alpha:
                s_item.append(item)
        s_reverse = s_item[::-1]
        # print(s_reverse)
        for i in range(len(s)):
            if s[i] not in alpha:
                s_reverse.insert(i, s[i])
        return ''.join(s_reverse)

# 双指针
class Solution:
    def reverseOnlyLetters(self, s: str) -> str:
        s_list = list(s)
        left = 0
        right = len(s) - 1
        while left < right:
            while left < right and not ('a' <= s[left] <= 'z' or 'A' <= s[left] <= 'Z'):
                left += 1
            while left < right and not ('a' <= s[right] <= 'z' or 'A' <= s[right] <= 'Z'):
                right -= 1
            if left < right:
                s_list[left], s_list[right] = s_list[right], s_list[left]
                left += 1
                right -= 1
        return ''.join(s_list)
392. 判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

# 双指针
class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        p1 = 0
        p2 = 0
        while p1 < len(s) and p2 < len(t):
            if s[p1] == t[p2]:
                p1 += 1
                p2 += 1
            else:
                p2 += 1
        return p1 == len(s)

你可能感兴趣的:(Python算法-双指针(Two Pointers))