算法小白算法刷题记录【二】
前言
第二篇尝试,继续。修改过程是本文最重要的价值体现,怎么想怎么改才是关键。
记录二:移除元素。
力扣题目【27】
一、题目阅读和理解
1.题目阅读
给一个数组 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,和val相等的元素都被删掉。数组即使空了两个,也不重要
除了返回的 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。
注意:返回的元素可以任意顺序。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
2.理解题目
- “原地”移除元素:
每个元素先比较之后,如果=val,删去,留下空位;如果不等于val;看前面有没有删掉的空位移到前面去。 - “返回的元素顺序任意”:所以我可以按上一句话来操作,不在乎排序。
- “返回k”:如果是固定数组,长度不变,不能用sizeof(这是全部个数,不是删掉的个数);如果用vector可以自动管理内存,那么删除之后,size()可以的。
- 理解评测通过条件:
- 调用实现那一行,说明参数和返回值。
- k用expectedNums.length断言,所以数组的长度得刚好是剩余元素的个数(第3点后一种)。
- 元素是否正确的断言:元素删除是对原数组的直接操作。
二、第一次尝试下笔
1.代码实现
先上代码:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int k = 0; // 返回值先摆上
int equalNum = 0;
vector<int> equalIndex; // 记录元素=val的下标
int i = 0;//遍历vector
//整个while把所有元素不等于val都放到了前面,每个元素都是“原地”删除
while(i < nums.size()){
if(nums[i] == 1000){
i++;
continue;
}else if(nums[i] == val){
for(int j = (nums.size()-1);j >= i;j--){
if((nums[j] != val) && (nums[j] != 1000)){
nums[i] = nums[j];
nums[j] =1000; // 占着位,代表移动过,不让后面的元素前移,还不能删。
k++; //有个不相等的元素放过来了。
break;
}else if(j == i){
nums[i] = 1000; //保证最后一个相等的元素被删掉
}
}
}else{
k++;
}
i++;
}
//所有1000都弹出
for(int i = nums.size()-1;i >=0; i--){
if(nums[i] == 1000)
nums.pop_back();
}
return k;
}
};
2.修改过程()
第一版本
最开始想直接nums.erase()删除元素,但是vector直接删除元素,会移动位置,自动管理内存。不符合“原地”。
所以想到遍历找出所有元素=val的下标。
int k;//返回值
vector<int> equalIndex;//放所有=val的元素下标
for(int i=0;i <nums.size();i++{
if(nums[i] == val){
equalIndex.push_back(i); //记下元素等于val的下标
}
}
行不通:我找出下标,重点是我删除得原地不能让vector元素移动,找下标没用啊。怎么删才是关键。
第二版本(怎么删呢?)
-
直接用erase()是不行的。想到:我把后面不等于val的元素拿到这个位置不就行了?
第一步:while()此时条件还没确定,放着。while里面第一个if(nums[i] == val),在这个if里面:遍历i后面,找一个不相等的元素。我先是正向找的:
for(int j = 0;j < nums.size();j++){
if(nums[j] != val) //可是我正向找,循环条件行不通,逻辑不正常,循环不下去。
};所以确定倒着找第一个不等于val的元素。
for(int j = (nums.size()-1);j >= i;j--){ //行得通,挪到前面不影响 if(nums[j] == val){ continue; }else{ nums[i] = nums[j]; break; } } //第一次写成了这样,正向思维,if改成不等于更简单。 改进:当前的while内容 if(nums[i] == val){ for(int j = (nums.size()-1);j > i;j--){ if(nums[j] != val){ nums[i] = nums[j]; //原来的nums[j]没有处理,并且没有记录k break; } } }else{ k++: } i++;
2.等于val的元素用nums[j]替换,可是num[j]我没处理,这不合理,num[j]已经被挪到前面了,所以我用一个不在范围的值1000,改掉nums[j]。
-
while多了if(nums[i] == 1000);
-
if((nums[j] != val) && (nums[j] != 1000))
-
记录挪到前面那个元素,k+1
所以当前while内容修正:
while(i < nums.size()){ //遍历整个nums,所以确定while条件。 if(nums[i] == 1000){ i++; //换下一轮 continue; }else if(nums[i] == val){ for(int j = (nums.size()-1);j > i;j--){ if((nums[j] != val) && (nums[j] != 1000)){ nums[i] = nums[j]; nums[j] =1000; // 占着位,代表移动过,不让后面的元素前移,还不能删。 k++; //有个不相等的元素放过来了。 break; } } }else{ //正向遍历时直接不等于val的元素。 k++: } i++; }
-
好像到这里没有问题了,单走一遍用例,发现nums最后一个等于val的元素没有处理。原因:走到break;后面没有不等于val的元素了,j = i+1;进不了for循环。所以
- for循环条件j > i,改成 j >=i;让最后一个等于val的元素换成1000.
所以最终得到了 1.代码实现 的while循环,实现“原地”删除元素。可以回过头再看一遍。(一点一点改出来的为什么这样写)
大胆的pop元素
while结束后,nums后面都是1000替换的无效元素,所以用for循环倒着pop值为1000的元素。
3.代码随想录学习
学习内容:学到了“双指针”法。
1.数组内存连续,删除中间的元素需要后面的元素向前覆盖。vector.earse()的确有覆盖前移的操作,所以时间复杂度是O(n)。
我的误区:earse前移之后,末尾空下的空间被自动回收,但视频说没有作处理。
2.vector.size()求长度,我以为是:回收了内存,才确定长度。其实是:没回收内存,确定元素个数。
3.解法一:外层for循环找到要删除的元素:找到之后内层for循环把后面的元素覆盖前移。
4.解法二:双指针法(没想到过)
(1)定义快指针int fast;和慢指针int slow; 操作同一个数组,指针只是指向的意思,不是指针变量,int变量即可。
(2)实现:相当于earse()
slow = 0;
for(int fast = 0; fast<nums.size() ; fast++){
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
}
return slow;
不认同点:
他认为该题目只是数组删除元素,需要覆盖前移。直接erase()就是实现这个过程。
我认为这只是删除操作,不是题目中说的“原地”删除,我理解的原地删除是——找到元素位置之前,它不应该被移动,覆盖前移也算移动。
为什么我觉得我理解正确?
因为题目中“原地”加粗,并且后面“元素的顺序可能发生改变。”这句话说明我理解正确。如果只是覆盖前移,顺序可以认为没有变化。
欢迎讨论“原地”删除的认同。
总结
- 数组删除需要覆盖前移操作,使用“双指针”实现更简单方便。
- 关于原地删除操作,个人理解实现while过程,是真正的原地。但也是覆盖的方法。
(欢迎讨论指正)
(转载须标明出处)