双指针分为「对撞指针」、「快慢指针」、「分离双指针」。
- 参考来源: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)