这是跟着代码随想录的顺序学习算法的第二天。
后续继续深入学习双指针法之后会再考虑临时想到的这个模型能不能套用。
以下是学习题解时自己的一些理解与笔记,有错误欢迎指正与讨论。
双指针法移除元素
参考相关链接:
27. 移除元素
26. 删除有序数组中的重复项
283. 移动零
代码随想录
题解均来自力扣官网后的题解,或者代码随想录,又或者是自己写的
笔记
注意到一点,快指针永远是在遇到最近的非 val 元素时候进行操作,慢指针永远是指向索引号最小的 val 元素。 |
---|
这是因为在正常没有出现 val 的元素之前,快慢指针永远都是指向一起的,导致快慢的原因是 val 元素的出现,慢指针遇到 val 之后会停下来,而快指针不会停下来。 所以,才导致了红字部分。 |
先让慢指针跟着快指针一起移动,同时在遇到目标元素之前慢指针每次移动前都将取快指针的元素来替换自己指向的元素(此时两者指向的元素是一样的)。
遇到了目标元素后,慢指针留下继续指着目标元素,而快指针则继续往下找最近的一个非目标元素来给慢指针替换,此时慢指针继续移动继续替换(此时两者指向的元素是不一样的),直到遇到下一个目标元素才暂停下来。
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};
场景模拟
看代码时候脑子里自动模拟的场景,记录一下帮助看得懂用得上的人理解和记忆=-=。
nums = [0,1,2,2,3,0,4,2], val = 2
可以把快指针和慢指针看作是一对母子,妈妈走得快所以假设为快指针,儿子走得慢所以假设为慢指针,母子二人一起走过 nums
这一条路,一路上有各种水果 (数组元素) nums[i]
,儿子只想吃其中的一种水果 val
。
但妈妈不希望孩子挑食,看到孩子不喜欢吃的水果 val != nums[fastIndex]
,也一边走一边摘水果给孩子 nums[slowIndex++] = nums[fastIndex]
,而此时孩子也只是跟着拿过水果放到书包里面并且跟着走 slowIndex++
。
当妈妈发现儿子喜欢吃的水果之后**( val == nums[fastIndex]
),没有拿给儿子(无操作),希望儿子自己努力拿到,而此时够不着果的儿子就站在树下看着喜欢的水果不动**(此时慢指针指向目标元素)。
妈妈继续往下边走边看是什么水果(遇到 val
后继续进入下一个循环),当看到儿子不喜欢的水果时候 val != nums[fastIndex]
,,将水果摘下来,抛入儿子的背包 nums[slowIndex++] = nums[fastIndex]
,儿子感到被人往后扒拉了一下,清醒过来继续跟着妈妈走 slowIndex++
,当遇到下一棵有喜欢水果的树时,就又走不动路了,等着妈妈的高抛水果来扒拉一下叫醒再继续走。
于是到最后,儿子没有吃到一个喜欢的水果,背着一背包不喜欢的按原来顺序排列的水果结束了一路的摘果(结束摘果时,儿子走的步数或者说包里装的水果数即为 slowIndex
,包里的水果按序排练即为需要的 nums
部分数组。 )
(强行补充设定:
因为妈妈每一步抬头看是什么水果,所以走路速度才和儿子一样,一次走一个数组元素;
儿子书包是哆啦A梦的百宝袋可以一直装东西(因为改成拿了就丢掉感觉有些浪费=-=);
“狠心”的妈妈最后带着儿子去超市买更好的水果了(补充成good ending了😀)
)
自己的解法
先排序,然后将利用昨天刚学的二分法找出目标元素左右边界,再将右边界后的元素向前移动。
此算法会改变数组原本的顺序。
时间复杂度高的原因是 在排序的历遍过程中其实就可以完成元素的移除。
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
// 排序之后,用二分法选出左右边界
// 将右边界后的元素都向前移动至左边界
// 排序
for (int i = 0; i < nums.size(); i++) {
for (int j = i + 1; j < nums.size(); j++){
if (nums[j] < nums[i]) {
int temp = nums[j];
nums[j] = nums[i];
nums[i] = temp;
}
}
}
// 选出左右边界,移动元素
int left = leftRound(nums, val);
int right = rightRound(nums, val);
int sub = right - left + 1;
if (left == -1 || right == -1) {
return nums.size();
}
for (int i = right + 1; i < nums.size(); i++) {
nums[i - sub] = nums[i];
}
return nums.size() - sub;
}
int leftRound(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
if (left == nums.size() || nums[left] != target){
return -1;
}
return left;
}
int rightRound(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
left = mid + 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
if (left <= 0 || nums[left - 1] != target){
return -1;
}
return left - 1;
}
};
其他双指针
26. 删除有序数组中的重复项
按照好不好吃的顺序依次往后,只有看到了更加好吃的才会高抛水果扒拉一下。
这里的小于也可以理解为前面的不等于,因为没有遇到更好吃的,目前遇到的就是喜欢的水果,所以站着不动,即喜欢的水果 val
永远是下一个更好吃的水果 nums[slowIndex] < nums[fastIndex]
。
慢指针只有在特定情况下才移动,所以才叫慢指针。
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int slowIndex = 0;
for (int fastIndex = 1; fastIndex < nums.size(); fastIndex++) {
if (nums[slowIndex] < nums[fastIndex]) {
nums[++slowIndex] = nums[fastIndex];
}
}
return slowIndex + 1;
}
};
283. 移动零
最开始我也是这么做出来的,但后面思考能不能不要用两个for循环,我都历遍了一次了,能不能一次历遍就搞定呢?
然后想最开始27. 移除元素的做法中,快指针在历遍的时候是在出现 val
后,把后续的非 val
元素给一路覆盖过来了,也就是说在这个地方造成了这个数组整体元素的缺失, 缺失了 val
元素。
但事实上,27题中是只关注 slowIndex
前面的数组部分,而不关心后面的数组元素,那要怎么样才能让前面的部分不出现 val
的同时又能让 val
保留下来呢?交换。
事实上我们注意到 fastIndex
只是负责历遍数组,找到其中的非 val
元素,找到之后往前面扔给 slowIndex
,这之后就不再管目前 fastIndex
这个位置的元素了,进入下一次的循环 fastIndex++
,所以说,这个地方的元素便是不会影响后面历遍的存在,于是乎就进行交换,把前面的元素换过来,这样既改动了位置又不会出现元素缺失的现象。
注意到一点,快指针永远是在遇到最近的非 val
元素时候进行操作,慢指针永远是指向索引号最小的 val
元素。
这是因为在正常没有出现 val
的元素之前,快慢指针永远都是指向一起的,导致快慢的原因是 val
元素的出现,慢指针遇到 val
之后会停下来,而快指针不会停下来。 所以,才导致了红字部分。
用母子摘水果模型来看的话,目标就是让儿子喜欢的水果 val
都在最后面而不是没有摘下来,可以理解为儿子被母亲的高抛水果扒拉一下之后,很生气,转手飞起来摘下跟前的树下喜欢的水果 val
不管三七二十一高抛回去(交换),然后继续往下走,此时,母亲在往下走之前把儿子丢来的喜欢的水果 val
挂在树上(交换),这样一来,母亲没给摘的喜欢的水果都在最后的树上。
(这是一个魔幻的模型,只是为了试着找个有意思的角度理解和解释这个问题的本质。)
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (nums[fastIndex] != 0) {
nums[slowIndex++] = nums[fastIndex];
}
}
// 将slowIndex之后的冗余元素赋值为0
for (int i = slowIndex; i < nums.size(); i++) {
nums[i] = 0;
}
}
};
作者:carlsun-2
链接:https://leetcode-cn.com/problems/move-zeroes/solution/283-yi-dong-ling-shuang-zhi-zhen-xiang-jie-by-ca-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 自己的做法
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (nums[fastIndex] != 0) {
int temp = nums[fastIndex];
nums[fastIndex] = nums[slowIndex];
nums[slowIndex++] = temp;
}
}
}
};