算法踩坑-同向快慢指针
算法学习
同向快慢指针深度拆解:LeetCode26 有序去重 VS LeetCode27 移除指定元素
博客地址模板适配
https://blog.dogeggcode.cyou/blog/algo1,风格:实战踩坑导向、代码对比、VSCode调试验证、避坑清单
前言
原地修改数组是算法入门高频考点,同向快慢指针是最优解(时间O(N)、空间O(1))。很多初学者刷完26题,照搬代码写27直接翻车,我自己调试时踩满一堆逻辑坑,本文结合两道同源题型,拆解指针起点、判断逻辑、返回值三大核心差异,统一快慢指针通用模板,附带VSCode调试排错方法。
一、两道原题题干速览
LeetCode 26 删除有序数组中的重复项
条件:数组升序有序,重复元素相邻;要求原地去重,每个元素只保留1次,返回有效长度。
示例:nums=[1,1,2] → 返回长度2,数组前两位[1,2]
LeetCode 27 移除元素
条件:数组无序/有序均可,任意位置都可能出现目标值val;要求原地删除所有等于val的数字,返回有效长度。
示例:nums=[0,1,2,2,3,0,4,2], val=2 → 返回长度5,数组前五位[0,1,3,0,4]
二、LeetCode26 标准正确代码 & 逻辑解析
完整Java实现(我写的可AC版本)
class Solution {
public int removeDuplicates(int[] nums) {
int n = nums.length;
if ( n == 0 ) {
return 0;
}
// 快慢指针统一从下标1开始
int fast = 1 , slow = 1;
while ( fast < n ) {
// 判断:快指针和前一位有效元素不重复,才存入慢指针
if ( nums[fast-1] != nums[fast] ) {
nums[slow] = nums[fast];
++slow;
}
++fast;
}
// slow直接等于有效元素总个数,无需+1
return slow;
}
}
核心逻辑拆解
- 指针起点 slow=1、fast=1 数组有序,第一个元素nums[0]一定保留,天然是合法有效数字。slow代表「下一个可写入有效数字的空位」,初始跳过已占用的0号位。
- 判断条件
nums[fast-1] != nums[fast]利用有序特性:重复数字一定挨在一起,fast-1是上一个存入slow区间的最后一个数字,两者不同说明是全新不重复元素,需要保存。 - 返回值 slow slow记录一共写入了多少个有效数字,数值直接等于数组有效长度。
推演示例 nums=[1,1,2]
初始 slow=1,fast=1
- fast=1:
nums[0]==nums[1]重复,仅fast++ → fast=2 - fast=2:
nums[1]!=nums[2],nums[1]=2,slow=2,fast++ → fast=3 循环结束,return slow=2,结果正确。
三、LeetCode27 标准正确代码 & 逻辑解析
完整可AC Java代码(我多次改错后的最终版本)
class Solution {
public int removeElement(int[] nums, int val) {
int slow = 0 ;
int n = nums.length;
if (n == 0) {
return 0;
}
// fast从头完整遍历所有元素
for (int fast = 0 ; fast < n ; fast ++){
// 判断:当前fast元素不是要删除的值,才存入slow
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow ++;
}
}
// 直接返回slow,禁止画蛇添足+1
return slow;
}
// 本地VSCode调试入口
public static void main(String[] args) {
int[] nums = {0,1,2,2,3,0,4,2};
int k = new Solution().removeElement(nums,2);
System.out.println("有效长度k="+k);
System.out.println("前k个元素:"+Arrays.toString(Arrays.copyOf(nums,k)));
}
}
核心逻辑拆解
- 指针起点 slow=0、fast=0 数组无有序保障,第一个元素有可能等于val,需要直接丢弃,不能默认保留,因此快慢指针全部从0开始遍历。
- 判断条件
nums[fast] != val无相邻对比逻辑,只筛选:当前遍历元素不等于待删除值,才复制到slow的有效区间。 - 返回值 slow slow每存入一个合法数字自增1,最终数值就是有效元素总数。
样例推演 nums=[0,1,2,2,3,0,4,2], val=2
循环结束后slow=5,前5位[0,1,3,0,4],和题目预期完全匹配。
四、两道题核心差异对比(踩坑根源)
| 对比维度 | LeetCode26 有序去重 | LeetCode27 移除指定值 | 你踩过的坑 |
|---|---|---|---|
| 指针起始下标 | slow=1 fast=1 | slow=0 fast=0 | 照搬26的1作为起点,第一个val会被错误保留 |
| 判断逻辑 | 对比fast-1和fast相邻元素是否重复 |
直接判断nums[fast]是否等于val |
用fast-1对比,连续val覆盖后依旧残留目标值 |
| 前置前提 | 数组升序有序,首元素必保留 | 无序,任意元素都可能删除 | 忽略有序前提,把相邻对比逻辑套用到无序数组 |
| slow含义 | 下一个写入空位,跳过已占用0号位 | 从0开始,从头收集所有合法数字 | 计数逻辑混乱,返回时错误写slow+1 |
| 返回值 | return slow | return slow | 两道题都额外+1,有效长度全部算错 |
最致命误区复盘
之前修改多版代码全部WA,本质是把26有序数组的特殊逻辑,直接套用到无序过滤题型:
- 错误用
fast=1跳过首元素,首元素为val时无法删除; - 错误判断
nums[fast-1]==val就覆盖,连续val场景只会原地替换,无法清除; - 循环结束无脑
return slow+1,有效长度多算1位。
五、通用同向快慢指针万能模板(覆盖26、27、283移动零)
模板定义
slow:写指针,维护合法元素区间,代表下一个合法数字的存储位置fast:读指针,完整遍历数组每一个元素,筛选符合条件的值
通用框架
public int template(int[] nums, int filterVal){
int slow = 0;
for(int fast = 0; fast < nums.length; fast++){
// 条件:保留当前fast元素的规则,根据题目修改
if( 符合保留规则 ){
nums[slow++] = nums[fast];
}
}
// slow一定等于有效长度,永远不要+1
return slow;
}
模板适配两道题
- LeetCode27适配:保留规则
nums[fast] != val - LeetCode26适配:需要有序前置判断,特殊修改起点与对比逻辑(属于模板特例)
六、VSCode调试排错实操(针对这道题)
1. 调试配置
安装扩展Extension Pack for Java,代码内写入main测试用例,右键main选择Debug Java启动。
2. 关键断点设置
- slow/fast初始化行:观察初始指针数值;
- if判断条件行:每次进入分支查看数组变化;
- return slow行:循环结束查看slow最终值。
3. 变量面板观察要点
- Local区域展开nums数组,实时查看原地修改后的数值;
- 监视窗口添加
slow、fast、nums[fast],跟踪指针变化;
4. 快速定位BUG技巧
- 输出前k位包含待删除值 → 判断条件写反/指针起点错误;
- 返回长度比预期大1 → return多写
+1; - 连续目标值无法清除 → 用了
fast-1相邻对比逻辑。
七、避坑总结(刷题必看)
- 有序数组去重(26)是特例:利用相邻重复特性,指针从1开始;无序过滤类题目(27、283)全部统一指针从0起步;
- slow自增时机:只有存入合法数字时才++,不要在匹配删除条件时计数;
- 返回值铁律:同向快慢指针通用模板中,slow直接等于有效长度,禁止额外+1;
- 不要盲目照搬AC代码:看清题目是否有序、首元素是否一定保留,再复用逻辑。
拓展习题
- LeetCode283 移动零(完全适配通用模板)
- LeetCode83 删除排序链表中的重复项(链表版26逻辑)
- LeetCode80 删除有序数组中的重复项II(进阶限制最多保留2个重复元素)
博客底部标签
#算法 #双指针 #LeetCode #Java #数组原地修改 #VSCode调试 需要我把这篇博客精简成掘金Markdown紧凑版,或者补充你调试时完整的逐轮变量推演表格吗?