在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:左右指针和快慢指针。
在数组中并没有真正意义上的指针,但我们可以把索引当做数组中的指针,这样也可以在数组中施展双指针技巧,本文主要讲数组相关的双指针算法。
本文参考labuladong算法小抄:双指针技巧秒杀七道数组题目 | labuladong 的算法笔记
一、快慢指针技巧
数组问题中比较常见的快慢指针技巧,是让你原地修改数组。
26. 删除有序数组中的重复项 | 力扣 | LeetCode |
给你一个 非严格递增排列 的数组 nums
,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums
中唯一元素的个数。
考虑 nums
的唯一元素的数量为 k
,你需要做以下事情确保你的题解可以被通过:
- 更改数组
nums
,使nums
的前k
个元素包含唯一元素,并按照它们最初在nums
中出现的顺序排列。nums
的其余元素与nums
的大小不重要。 - 返回
k
。
示例 1:
输入:nums = [1,1,2] 输出:2, nums = [1,2,_] 解释:函数应该返回新的长度 2 ,不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4] 输出:5, nums = [0,1,2,3,4] 解释:函数应该返回新的长度 5,并且原数组 nums 的前五个元素被修改为,不需要考虑数组中超出新长度后面的元素。
【python 解法】
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
slow = fast = 0
length = len(nums)
while fast < length:
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
fast += 1
# 此时的slow为新数组的右边界索引,题目要求返回数组长度,故需slow + 1
return slow + 1
【算法思路】
我们让慢指针 slow
走在后面,快指针 fast
走在前面探路,找到一个不重复的元素就赋值给 slow
并让 slow
前进一步。
这样,就保证了 nums[0..slow]
都是无重复的元素,当 fast
指针遍历完整个数组 nums
后,nums[0..slow]
就是整个数组去重之后的结果。
算法执行的过程如下 GIF 图:
83. 删除排序链表中的重复元素删除排序链表中的重复元素
给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例 1:
输入:head = [1,1,2] 输出:[1,2]
示例 2:
输入:head = [1,1,2,3,3] 输出:[1,2,3]
提示:
- 链表中节点数目在范围
[0, 300]
内 -100 <= Node.val <= 100
- 题目数据保证链表已经按升序 排列
【python 解法】
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:
return head
# 浅拷贝
slow = head
fast = head
while fast:
if fast.val != slow.val:
slow.next = fast
slow = slow.next
fast = fast.next
# 此时slow的next还指向下个节点,需要将其截断才能得到去重后的结果
slow.next = None
return head
算法执行的过程请看下面这个 GIF:
27. 移除元素
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。
示例 1:
输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2,_,_] 解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,4,0,3,_,_,_] 解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。 注意这五个元素可以任意顺序返回。 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
【python 解法】
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
fast = slow = 0
length = len(nums)
while fast < length:
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
注意这里和有序数组去重的解法有一个细节差异,我们这里是先给 nums[slow]
赋值然后再给 slow++
,这样可以保证 nums[0..slow-1]
是不包含值为 val
的元素的,最后的结果数组长度就是 slow
。
283. 移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums =[0,1,0,3,12]
输出:[1,3,12,0,0]
示例 2:
输入: nums =[0]
输出:[0]
【python 解法】
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
fast = slow = 0
length = len(nums)
while fast < length:
if nums[fast] != 0:
nums[slow] = nums[fast]
slow += 1
fast += 1
# 将后续值直接赋为0
for s in range(slow, length):
nums[s] = 0
题目让我们将所有 0 移到最后,其实就相当于移除 nums
中的所有 0,然后再把后面的元素都赋值为 0 即可。
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"] 输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"] 输出:["h","a","n","n","a","H"]
【python 解法】
class Solution:
def reverseString(self, s: List[str]) -> None:
s = s.reverse()
二、左右指针的常用算法
1、二分查找
我在另一篇文章 二分查找框架详解 中有详细探讨二分搜索代码的细节问题,这里只写最简单的二分算法,旨在突出它的双指针特性:
def binarySearch(nums: List[int], target: int) -> int:
# 一左一右两个指针相向而行
left, right = 0, len(nums) - 1
while left <= right:
mid = (right + left) // 2
if nums[mid] == target:
return mid # 如果nums[mid]等于target,返回mid
elif nums[mid] < target:
left = mid + 1 # 如果nums[mid]小于target,left移动到mid+1
elif nums[mid] > target:
right = mid - 1 # 如果nums[mid]大于target,right移动到mid-1
return -1 # 如果target不在nums中,返回-1
167. 两数之和 II - 输入有序数组
给你一个下标从 1 开始的整数数组 numbers
,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target
的两个数。如果设这两个数分别是 numbers[index1]
和 numbers[index2]
,则 1 <= index1 < index2 <= numbers.length
。
以长度为 2 的整数数组 [index1, index2]
的形式返回这两个整数的下标 index1
和 index2
。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
示例 1:
输入:numbers = [2,7,11,15], target = 9 输出:[1,2] 解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
示例 2:
输入:numbers = [2,3,4], target = 6 输出:[1,3] 解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。
示例 3:
输入:numbers = [-1,0], target = -1 输出:[1,2] 解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
提示:
2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers
按 非递减顺序 排列-1000 <= target <= 1000
【python 解法1:借助哈希表(字典)】
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
# 已经遍历过的数用字典存储其索引
dic = {}
for index, num in enumerate(numbers):
# 如果所需的数已存在于字典中,直接返回二者索引即可
tmp = target - num
if tmp in dic:
return [dic[tmp], index + 1]
dic[num] = index + 1
return []
【python 解法2:双指针】
def twoSum(nums, target):
# 一左一右两个指针相向而行
left, right = 0, len(nums) - 1
while left < right:
sum = nums[left] + nums[right]
if sum == target:
# 题目要求的索引是从 1 开始的
return [left + 1, right + 1]
elif sum < target:
left += 1 # 让 sum 大一点
elif sum > target:
right -= 1 # 让 sum 小一点
return [-1, -1]
另一篇文章 一个函数秒杀所有 nSum 问题 中也运用类似的左右指针技巧给出了 nSum
问题的一种通用思路,这里就不做赘述了。
5. 最长回文子串
给你一个字符串 s
,找到 s
中最长的 回文子串。
示例 1:
输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd" 输出:"bb"
【python 双指针解法】
class Solution:
def longestPalindrome(self, s: str) -> str:
# 中心开花向两端遍历
def palindrome(l: int, r: int):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1
r += 1
return s[l+1: r]
res = ''
for i in range(len(s)):
# 分别考虑回文串长度为奇数和偶数的情况
s1 = palindrome(i, i)
s2 = palindrome(i, i+1)
res = s1 if len(s1) > len(res) else res
res = s2 if len(s2) > len(res) else res
return res
【算法思路】
首先明确一下,回文串就是正着读和反着读都一样的字符串。
比如说字符串 aba
和 abba
都是回文串,因为它们对称,反过来还是和本身一样;反之,字符串 abac
就不是回文串。
现在你应该能感觉到回文串问题和左右指针肯定有密切的联系,比如让你判断一个字符串是不是回文串,你可以写出下面这段代码:
def isPalindrome(s: str) -> bool:
# 一左一右两个指针相向而行
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧。
如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。所以我们可以先实现这样一个函数:
# 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
def palindrome(s: str, l: int, r: int) -> str:
# 防止索引越界
while l >= 0 and r < len(s) and s[l] == s[r]:
# 双指针,向两边展开
l -= 1
r += 1
# 返回以 s[l] 和 s[r] 为中心的最长回文串
return s[l + 1: r]
这样,如果输入相同的 l
和 r
,就相当于寻找长度为奇数的回文串,如果输入相邻的 l
和 r
,则相当于寻找长度为偶数的回文串。
那么回到最长回文串的问题,解法的大致思路就是:
for 0 <= i < len(s):
找到以 s[i] 为中心的回文串
找到以 s[i] 和 s[i+1] 为中心的回文串
更新答案
你应该能发现最长回文子串使用的左右指针和之前题目的左右指针有一些不同:之前的左右指针都是从两端向中间相向而行,而回文子串问题则是让左右指针从中心向两端扩展。不过这种情况也就回文串这类问题会遇到,所以我也把它归为左右指针了。
到这里,数组相关的双指针技巧就全部讲完了,这些技巧的更多扩展延伸见 更多数组双指针经典高频题。