这三道题都用到双指针,但指针承担的职责不同:80 题确定保留区的结束位置,88 题确定新元素的写入位置,977 题则在两端之间取舍。三者放在一起对照,能看清每个指针当下代表什么。
80. 删除有序数组中的重复项 II
这题是 26. 删除有序数组中的重复项 的进阶版。原题要求每个元素最多保留一个:slow 指向已保留部分的末尾,fast 向前扫描,遇到新值时先把 slow 后移一位,再写入该值,扫描结束后 [0, slow] 即答案区间。
本题把限制放宽为每个元素最多保留两个,最直接的改法是增加一个计数器,记录当前元素已经出现几次,超过两次就不再写入。
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
slow = 0
fast = 0
count = 0
while fast < len(nums):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
elif slow < fast and count < 2:
slow += 1
nums[slow] = nums[fast]
fast += 1
count += 1
if fast < len(nums) and nums[fast] != nums[fast - 1]:
count = 0
return slow + 1这段代码可以正确运行,但理解成本偏高:count 记录的是元素出现的次数,并非被写入的次数,即使某个重复元素被跳过,count 依然递增。因此 count < 2 的真实含义是“该元素尚未出现第三次”,而不是“已经保留了两个”。
另一处容易被忽略的是 slow < fast:第一轮循环中两个指针都在下标 0,nums[fast] 与 nums[slow] 指向同一元素,必然相等。缺少这一条件时,这个元素会被当成重复项再写一次,nums[1] 因此被覆盖。
因此我更倾向另一种写法:让 slow 表示下一个可写入的位置,有效区间从 [0, slow] 改为 [0, slow),判断重复时不再需要计数器。
由于数组有序,nums[slow - 2] 就是保留区倒数第二个元素,若当前元素与它相等,说明该值已经保留了两份,应当跳过;否则可以写入。fast < 2 时直接写入即可,既满足“最多两次”的要求,也避开了 slow - 2 越界的边界情况。
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
slow = 0
fast = 0
while fast < len(nums):
if fast < 2 or nums[fast] != nums[slow - 2]:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow循环结束后 slow 即新数组长度,[0, slow) 为答案区间。
88. 合并两个有序数组
如果不限制空间,合并两个有序数组很直接:p1、p2 分别遍历 nums1 和 nums2,每次取较小值写入结果,再把未取完的部分接上。合并两个有序链表也是同样的思路。
本题要求原地写回 nums1:前 m 个位置存放数据,后 n 个位置是预留的空位。若从前往后写入,尚未读取的元素会被覆盖,因为目标位置仍被数据占用。
既然后半部分本就为空,可以从后往前填:每次比较两个数组的末尾元素,将较大者写入 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.
"""
tail = m + n - 1
p1 = m - 1
p2 = n - 1
while p1 >= 0 and p2 >= 0:
if nums1[p1] > nums2[p2]:
nums1[tail] = nums1[p1]
p1 -= 1
else:
nums1[tail] = nums2[p2]
p2 -= 1
tail -= 1
while p2 >= 0:
nums1[tail] = nums2[p2]
p2 -= 1
tail -= 1tail 是下一个待写入的位置,p1 指向 nums1 原有数据的末尾,p2 指向 nums2 的末尾。第一个循环结束时,nums2 可能还有剩余,需要再补一段;nums1 中剩余的元素已经在正确位置,无须再移动。
977. 有序数组的平方
最容易想到的写法是先平方再排序,时间复杂度 O(n log n);要做到 O(n),则需要利用原数组有序这一条件。
平方之后,最大值只可能出现在两端:负数区间的平方从左到右递减,正数区间的平方从左到右递增,中间元素的平方不会超过两端。
因此用左右指针从两端向中间收缩,每次比较绝对值,将较大者的平方从结果数组末尾向前填充。填充方向与上一题一致,区别只在于结果写入一个新数组。
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
left = 0
right = len(nums) - 1
res = [0] * (right + 1)
idx = right
while left <= right:
if abs(nums[left]) > abs(nums[right]):
res[idx] = nums[left] * nums[left]
left += 1
idx -= 1
else:
res[idx] = nums[right] * nums[right]
right -= 1
idx -= 1
return residx 指向结果数组下一个待填位置,初值为结果数组的最后一个下标。循环条件必须写成 left <= right:两个指针相遇时还有一个元素未处理,写成 left < right 会漏掉它;当 left 越过 right,说明所有元素都已处理完毕。