1、对于数组上的双指针问题,主要是指两个变量在数组上移动或遍历的问题,之前整理的“二分查找”系列的算法题目,就是双指针解决数组上的索引相向移动。
2、双指针算法的关键在于:初始化双指针值和双指针值分别更新的条件,保持对以上两点的清晰认知,就能准确掌握这类题目。
3、本次博客总结的“基于元素移除的类型”题目,该类题目必然首先需要一个指针遍历数组元素,然后结合要求O(1)的空间复杂度,必然需要额外的指针在原数组上维护新的数组,因此需要采用双指针的解法。
27. 移除元素
元素移除的基础题,给定序列和目标元素,将目标元素全部移动到数组尾部,新数组的元素顺序可以改变,也不需要考虑数组中超出新长度后面的元素。
from typing import List
'''
27. 移除元素
题目描述:给你一个数组nums和一个值val,你需要 原地 移除所有数值等于val的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。
例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
题眼:原地修改数组+元素的顺序可以改变+不考虑超出新数组长度后的元素。
思路1:双指针之快慢指针,慢指针维护新数组,快指针遍历原数组,不等于val值时进行元素覆盖(赋值操作多),更新慢指针——可以保留原元素在新数组中的相对顺序
思路2:双指针之左右指针:左指针遍历原数组,等于val值时,由右指针值赋值(赋值操作少),更新右指针,直到右指针小于左指针——不以保留原元素在新数组中的相对顺序
'''
class Solution:
# 双指针之快慢指针:需要对留下来的元素不断进行赋值操作,可以保留原元素在新数组中的相对顺序
def removeElements(self, nums: List[int], val: int) -> int:
slow, fast = 0, 0 # 快慢指针都指向起始位置,慢指针维护新数组,快指针遍历原数组
while fast < len(nums):
if nums[fast] != val:
nums[slow] = nums[fast] # 这里也可用 元素交换替换元素覆盖,实现val值全部放到 超出新数组长度后的位置
slow += 1 # 更新慢指针
fast += 1 # 更新快指针
return slow
# 双指针之左右指针:避免了留下来的数组元素的拷贝操作,赋值操作少,不可以保留原元素在新数组中的相对顺序
def removeElements2(self, nums: List[int], val: int) -> int:
left, right = 0, len(nums) - 1 # 左指针遍历原数组+维护新数组,右指针 赋值
while left <= right: # 注意迭代停止的状态
if nums[left] == val:
nums[left] = nums[right] # 这里也可用 元素交换替换元素覆盖,实现val值全部放到 超出新数组长度后的位置
right -= 1 # 更新右指针
left += 1 # 更新左指针
return left
if __name__ == '__main__':
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
nums = [int(n) for n in in_line[1].split(']')[0].split('[')[1].split(',')]
val = int(in_line[2].strip())
result = obj.removeElements2(nums, val)
print(result, ',nums =', nums[:result])
except EOFError:
break
283. 移动零
给定目标元素为0,将目标元素全部移动到数组尾部,需要保持新数组元素的相对顺序,需要考虑数组中超出新长度后面的元素:因此只能采用数组元素交换,不可以用数组元素覆盖。
from typing import List
'''
283. 移动零
题目描述:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
题眼:“27. 移除元素"的特例”——元素值val为0移动到末尾+保持非零元素的相对顺序
思路:双指针之快慢指针,慢指针维护新数组,快指针遍历原数组,不等于val值时进行元素交换,更新慢指针——可以保留原元素在新数组中的相对顺序
'''
class Solution:
def moveZeros(self, nums: List[int]):
slow, fast = 0, 0 # 慢指针标记处理好的序列末尾,当遇到一次0以后,标记着0序列的开头;快指针遍历原数组
while fast < len(nums):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow] # 元素交换 把 val值全部放到 超出新数组长度后的位置
slow += 1
fast += 1
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
nums = [int(n) for n in in_line[1].split(']')[0].split('[')[1].split(',')]
obj.moveZeros(nums)
print(nums)
except EOFError:
break
26. 删除有序数组中的重复项
给定目标元素为数组中的重复项了,是一个需要动态更新的变量了,将目标元素全部移动到数组尾部,需要保持新数组元素的相对顺序,不需要考虑数组中超出新长度后面的元素。
from typing import List
'''
26. 删除有序数组中的重复项
题目描述:给你一个 升序排列 的数组nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次,返回删除后数组的新长度。元素的 相对顺序 应该保持一致。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有k个元素,那么nums的前k个元素应该保存最终结果。
将最终结果插入nums 的前 k 个位置后返回 k 。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
题眼:“27. 移除元素"的特例——元素值val为 有重复的元素(是个需要更新的变量) 移动到末尾 + 升序排列 + 保持非零元素的相对顺序
思路:双指针之快慢指针,慢指针维护新数组,快指针遍历原数组,不等于val值时进行元素交换,更新慢指针——可以保留原元素在新数组中的相对顺序
'''
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
slow, fast = 1, 1 # 慢指针标记处理好的序列末尾;快指针遍历原数组:第一个元素一定会被留下
val = nums[slow-1] # val可以直接用nums[slow-1]代替
while fast < len(nums):
if nums[fast] != val: # 改成nums[fast] > val是利用上了 升序排列 的条件
nums[slow] = nums[fast] # 元素赋值/交换都可以
slow += 1
val = nums[slow-1]
fast += 1
return slow
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
nums = [int(n) for n in in_line[1].split(']')[0].split('[')[1].split(',')]
print(obj.removeDuplicates(nums), nums)
except EOFError:
break
80. 删除有序数组中的重复项 II
1、给定目标元素为数组中的重复项,是一个需要动态更新的变量了,同时要求“出现次数超过两次的元素只出现两次”,将目标元素全部移动到数组尾部,需要保持新数组元素的相对顺序,不需要考虑数组中超出新长度后面的元素。
2、如果按照本篇总结的做题顺序刷下来,到这一题还是同样的套路,仅仅需要在上题基础上稍微调整算法细节即可(好的刷题顺序不仅有助于掌握算法核心本质思想,更有助于面对难度加大的题目时更顺利的做出来)
from typing import List
'''
80. 删除有序数组中的重复项 II
题目描述:给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 不需要考虑数组中超出新长度后面的元素。
题眼:“26. 删除有序数组中的重复项”的延申——元素值val为 有重复的元素(是个需要更新的变量) 移动到末尾 + 升序排列 + 超过两次的元素只出现两次
思路:双指针之快慢指针,慢指针维护新数组,快指针遍历原数组,不等于val值时进行元素交换,更新慢指针——可以保留原元素在新数组中的相对顺序
'''
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
slow, fast = 2, 2 # 慢指针标记处理好的序列末尾;快指针遍历原数组:前两个元素一定会被留下
val = nums[slow-2] # val可以直接用nums[slow-2]代替
while fast < len(nums):
if nums[fast] != val: # 改成nums[fast] > val是利用上了 升序排列 的条件
nums[slow] = nums[fast] # 元素赋值/交换都可以
slow += 1
val = nums[slow-2]
fast += 1
return slow
if __name__ == "__main__":
obj = Solution()
while True:
try:
in_line = input().strip().split('=')
nums = [int(n) for n in in_line[1].split(']')[0].split('[')[1].split(',')]
print(obj.removeDuplicates(nums), nums)
except EOFError:
break
个人总结体会
1、一开始做“基于数组的双指针(一、基于元素移除的类型)”这类题目时,通过看题解,代入式地在思维上接受了双指针的算法思路,对于为什么用双指针一直没太多领悟。
2、通过多次回顾与总结,明白了对于一个数组而言,本身需要第一个指针负责遍历每一个元素,再结合题目要求O(1)的空间复杂度,自然而然地需要第二个指针负责维护新数组了。
3、而对于最关键的双指针分别更新的条件,第一个负责遍历的指针自然是随着每次循环访问数组元素更新,第二个负责维护新数组的指针就需要在满足一定条件下去更新了。
4、维护新数组的指针在进行操作时的细节:可以有元素赋值和元素交换两种选择,就根据具体题目灵活选择即可。