概念
快慢指针
快慢指针是一种常用的编程技巧,主要用于处理数组或链表等线性结构。这种技术使用两个指针(或者说两个索引)来遍历数据结构:一个指针移动得比另一个快,因此被称为“快指针”,而另一个被称为“慢指针”。
2 快慢指针的一些常见应用
- 找到链表的中间节点: 假设我们有一个包含七个节点的链表:1 → 2 → 3 → 4 → 5 → 6 → 7,我们希望找到中间的节点。
初始状态:
1(s,f) → 2 → 3 → 4 → 5 → 6 → 7
第一步(快指针移动两步,慢指针移动一步):
1 → 2(s) → 3 → 4(f) → 5 → 6 → 7
第二步(快指针移动两步,慢指针移动一步):
1 → 2 → 3(s) → 4 → 5(f) → 6 → 7
第三步(快指针移动两步,慢指针移动一步):
1 → 2 → 3 → 4(s) → 5 → 6 → 7(f)
当快指针到达链表的末尾时,慢指针恰好在链表的中间。
- 检测链表中的循环: 假设我们有一个包含环的链表:1 → 2 → 3 → 4 → 5 → 6 → 3(3后面的箭头表示这是一个环,回到了3)。
初始状态:
1(s,f) → 2 → 3 → 4 → 5 → 6
↖______________↙
第一步(快指针移动两步,慢指针移动一步):
1 → 2(s) → 3 → 4(f) → 5 → 6
↖______________↙
第二步(快指针移动两步,慢指针移动一步):
1 → 2 → 3(s) → 4 → 5(f) → 6
↖______________↙
第三步(快指针移动两步,慢指针移动一步):
1 → 2 → 3 → 4(s) → 5 → 6(f)
↖______________↙
第四步(快指针移动两步,慢指针移动一步):
1 → 2 → 3(f,s) → 4 → 5 → 6
↖______________↙
当快慢指针相遇时,说明链表存在环。
- 数组去重:
假设我们有一个有序数组
[1, 1, 2, 2, 3, 4, 4],我们希望删除重复的元素。
初始状态:
1(s,f), 1, 2, 2, 3, 4, 4
第一步(快指针移动,因为快慢指针指向的值相同):
1(s), 1(f), 2, 2, 3, 4, 4
第二步(快指针移动,快慢指针指向的值不同,交换元素并移动慢指针):
1, 2(s,f), 2, 3, 4, 4
......
最后一步(数组完全遍历后):
1, 2, 3, 4(s), 2, 4, 4(f)
当快指针遍历完数组后,慢指针前面的部分就是不重复的元素。
题目
给你一个数组 nums **和一个值 val,你需要 原地 移除所有数值等于 val **的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 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],也会被视作正确答案。
示例 2:
输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,4,0,3]
解释: 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 1000 <= nums[i] <= 500 <= val <= 100
解题思路🙋🏻 ♀️
Q: 为什么不能直接遍历去数呢? 或者通过直接删除数组的元素来计算?(我们大家都会想到直接删除,然后获取出不包含 val 的元素个数)
空间复杂度为 O(1) 我们无法新增空间
时间复杂度可能增加:在数组中删除元素通常需要 O(n) 的时间复杂度,其中 n 是数组的长度。因为在删除元素后,你需要将后面的所有元素向前移动一位以填补空位。如果你在遍历数组的同时删除元素,那么总的时间复杂度可能会达到 O(n^2),这比使用双指针的方法要慢。
可能会导致错误:在遍历数组的同时修改数组(例如删除元素)可能会导致错误或者未定义的行为。例如,你可能会错过一些元素,或者遍历到不应该遍历到的元素。这是因为当你删除一个元素时,后面的元素会向前移动,这可能会影响到你的遍历。
思路
有一个数组 nums = [0, 1, 2, 2, 3, 0, 4, 2],并且 val = 2。
- 初始状态:快慢指针都指向数组的第一个元素。
nums = [0,1,2,2,3,0,4,2]
s,f
- 移动快指针:当
nums[fast]不等于val时,我们复制nums[fast]到nums[slow]的位置,并将slow和fast都向前移动一步。
nums = [0,1,2,2,3,0,4,2]
s,f
- 快指针遇到
val:当nums[fast]等于val时,我们只移动fast指针。
nums = [0,1,2,2,3,0,4,2]
s f
- 再次移动快指针:当
nums[fast]不等于val时,我们复制nums[fast]到nums[slow]的位置,并将slow和fast都向前移动一步。
nums = [0,1,3,2,3,0,4,2]
s f
- 重复这个过程:我们会重复上述过程,直到
fast指针遍历完整个数组。在每次nums[fast]不等于val的情况下,我们都会将nums[fast]的值复制到nums[slow]的位置,并将slow指针向前移动一步。
nums = [0,1,3,0,4,2,4,2]
s f
- 返回结果:最后,我们返回
slow指针的位置,它表示数组中不等于val的元素的数量。
在这个例子中,数组中不等于 val 的元素的数量是 5,因此我们返回 5。同时,数组的前5个元素被修改为所有不等于 val 的元素,这就相当于我们已经删除了所有等于 val 的元素。
边界思考🤔
-
数组为空:如果数组
nums为空,函数应该直接返回 0,因为没有任何元素需要删除。 -
数组为空
var nums: [Int] = []
let val = 3
let len = removeElement(&nums, val) // 返回 0
这里,因为 nums 是空的,所以函数应该返回 0。
代码
第一遍错误
这道题第一遍写错了.
if nums[fast] == val {
fast += 1
}
当 nums[fast] == val 时,fast 指针会前进一步,然后代码将立即检查 nums[fast] != val。如果 nums[fast] 在前进一步后仍然等于 val,那么 nums[fast] != val 的检查将失败,而 fast 指针不会再前进。这将导致 fast 指针陷入死循环,无法前进。
import Foundation
class Solution {
func removeElement(_ nums: inout [Int], _ val: Int) -> Int {
if nums.count == 0 {
return 0
}
var fast = 0
var slow = 0
while fast <= nums.count - 1 {
// 当快指针 == val 的时候, 直接跳过. 因为要把不等于 val 才置换
if nums[fast] == val {
fast += 1
}
// 当快指针 != val 的时候, 交换值, 一起 +1
if nums[fast] != val {
nums[slow] = nums[fast]
slow += 1
fast += 1
}
}
return slow
}
}
修改后版本
import Foundation
class Solution {
// 定义一个名为 removeElement 的方法,它接受一个整数数组(通过引用传递)和一个整数值,并返回一个整数
func removeElement(_ nums: inout [Int], _ val: Int) -> Int {
// 如果数组为空,则直接返回0
if nums.count == 0 {
return 0
}
// 定义两个指针:快指针 fast 和慢指针 slow,都初始化为0
var fast = 0
var slow = 0
// 当快指针没有遍历完数组时,执行循环体
while fast <= nums.count - 1 {
// 当快指针指向的元素不等于 val 时
if nums[fast] != val {
// 将快指针指向的元素复制到慢指针指向的位置
nums[slow] = nums[fast]
// 将慢指针向前移动一步
slow += 1
}
// 将快指针向前移动一步
fast += 1
}
// 返回慢指针的值,这就是数组中不等于 val 的元素的数量
return slow
}
}
时空复杂度分析
o (n)