算法踩坑-相向左右指针 | 刷题踩坑连载 03
算法踩坑-相向左右指针 | 刷题踩坑连载 03
本系列持续更新算法刷题过程中亲手踩过的真实坑点,不照搬标准答案,只记录翻车现场、错误根源和修正思路。
上一篇拆解了双指针的第一大类——同向快慢指针,本篇继续补齐双指针的第二大分支:相向左右指针(对撞指针)。覆盖从基础交换到带过滤的回文判断三道入门题,从语法认知到指针逻辑,从基础框架到进阶过滤,所有亲手踩过的坑全汇总在这里。
一、三道入门题核心速览
相向左右指针的核心逻辑是:两个指针分别从数组/字符串的两端出发,向中间靠拢相遇,根据题目要求收缩搜索空间或完成交换、比较。三道经典入门题对应三个梯度的应用场景:
- LeetCode 167 两数之和 II - 输入有序数组:利用有序性,通过两端和与目标值的大小关系收缩指针,是对撞指针的经典入门题
- LeetCode 344 反转字符串:两端元素两两交换,最基础的对撞指针框架,用来巩固指针移动规则
- LeetCode 125 验证回文串:在基础对撞框架上增加无效字符过滤、忽略大小写的逻辑,属于进阶应用
二、语法认知坑:字符/字符串/数组的API大乱炖
从之前的整型数组题转到字符、字符串题,最容易翻车的不是算法逻辑,而是不同数据类型的API混用。以下全是编译阶段直接报错的低级坑,初学者几乎必踩。
1. 多变量声明重复写类型
错误表现:同一行声明两个同类型变量,重复写类型关键字,比如 int left = 0 , int right = s.length;
错误原因:Java 语法规定,同一行声明多个同类型变量时,类型关键字只需要写一次,用逗号分隔变量名即可。重复写 int 会被编译器识别为语法错误。
修正:int left = 0, right = s.length - 1;
2. char 与 String 类型混淆
错误表现:用小写的 string 声明单个字符的临时变量,比如 string temp;
错误根源:对两个类型的定位完全不清楚:
char是 Java 基本数据类型,只能存单个字符,用单引号包裹,比如'a'String是引用类型(类),表示一串字符组成的序列,用双引号包裹,比如"abc"反转字符串、字符交换场景,临时变量只需要存单个字符,用char类型即可,完全不需要 String。
3. 数组与字符串取值方式完全混用
错误表现:两个方向的混用都踩了:
- 字符数组
char[]调用.charAt[]取元素,还把方法的圆括号写成了方括号 - 字符串
String直接用s[下标]的方式访问字符 核心结论:两者取值语法完全不通用: - 数组(包括 char[]、int[]):直接用
数组名[下标]访问元素 - 字符串 String:必须调用方法
s.charAt(下标),方法用圆括号,返回值是 char 类型
4. 长度的属性与方法搞混
错误表现:字符串取长度写 s.length,漏掉括号
对比记忆:
- 数组:
length是属性,不带括号,比如nums.length - 字符串:
length()是方法,必须带括号,比如s.length()两者规则不同,写反了编译器会直接报找不到符号。
5. 布尔常量大小写错误
错误表现:判断条件里写 == True
错误原因:Java 中布尔值是小写关键字 true / false,大写会被识别成未定义的变量,直接编译失败。
三、指针逻辑坑:对撞指针专属的边界与方向翻车
语法改对之后,第二大翻车点就是指针的初始位置和移动方向,属于思路对但细节错,一运行就越界或死循环。
1. 右指针初始值越界:忘记减 1
错误表现:把右指针初始化为数组/字符串的长度,第一次访问元素就抛出下标越界异常。
触发场景:反转字符串、回文判断、两数之和所有对撞指针题型都会踩。
本质原因:Java 数组和字符串的下标都从 0 开始,最后一个元素的下标永远是「长度 - 1」。直接用长度作为初始值,相当于一上来就指向了数组外面。
修正铁律:所有从末尾开始的指针,初始值一律写 长度 - 1。
2. 右指针移动方向完全写反
错误表现:右指针写 right++,越跑越远,要么死循环要么持续越界。
错误根源:混淆了同向指针和相向指针的移动规则:
- 同向快慢指针:两个指针都向右移动,都是
++ - 相向左右指针:两个指针往中间收缩,左指针右移
left++,右指针左移right--记忆点:对撞指针就是两个指针“面对面走”,方向一定相反。
3. 循环条件边界误用
不同题型的循环终止条件有细微差别,写错不会报错但会影响结果:
- 交换类(如反转字符串):用
left < right即可,当两指针相遇时,中间元素不需要和自己交换 - 求和类(如两数之和II):必须用
left < right,不能写<=,否则会出现同一个元素被使用两次的情况 - 回文判断类:
left < right和left <= right都能通过,相遇时单个字符不影响回文结果
四、进阶场景坑:回文过滤逻辑的典型翻车
做 LeetCode 125 验证回文串时,在基础对撞框架上增加了无效字符过滤、忽略大小写的逻辑,很容易把指针移动顺序写乱,属于对撞指针的进阶翻车点。
1. 把静态方法当成实例方法调用
错误表现:写 s.charAt(left).isLetterOrDigit() 来判断字符是否有效
错误根源:char 是基本数据类型,不能调用任何方法。判断字符类型、转换大小写,都是 Character 包装类的静态方法,需要把字符作为参数传入。
正确写法:
- 判断是否为字母/数字:
Character.isLetterOrDigit(c) - 转为小写:
Character.toLowerCase(c)
2. 无效字符过滤的指针移动逻辑混乱
错误表现:在循环末尾统一执行 left++ 和 right--,嵌套的判断分支里又额外移动指针,导致有效字符被跳过、过滤逻辑完全失效。
正确分层逻辑:必须遵循“哪边无效移哪边,都有效再一起移”的原则:
- 左指针指向无效字符 → 只移动左指针,进入下一轮循环
- 右指针指向无效字符 → 只移动右指针,进入下一轮循环
- 两边都是有效字符 → 比较是否相等,不相等直接返回 false;相等则两个指针同时向中间移动
3. 过滤时缺失边界保护
潜在翻车点:如果输入全是空格、符号这类无效字符,指针会一直移动,最终超出边界触发越界。
修正方案:每次移动过滤指针时,都要加上 left < right 的边界约束,保证两指针交叉后立刻停止,不会继续越界移动。
五、相向左右指针通用模板
1. 基础对撞模板(无过滤场景:反转、有序求和)
public void baseTemplate(char[] s) {
int left = 0, right = s.length - 1;
while (left < right) {
// 核心处理逻辑:交换、比较求和等
// ...
// 固定相向移动
left++;
right--;
}
}
2. 带过滤对撞模板(回文类场景)
public boolean filterTemplate(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
// 跳过左侧无效字符
while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
// 跳过右侧无效字符
while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
// 比较有效字符
if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) {
return false;
}
left++;
right--;
}
return true;
}
六、对撞指针通用自检三步法
写完代码按这三步自查,能规避 90% 以上的低级错误:
- 语法自检:先分清输入是数组还是字符串,取值方式、长度写法、变量类型是否匹配
- 指针自检:右指针初始值有没有减 1?移动方向是不是相向?循环条件是否匹配题型
- 逻辑自检:带过滤的场景指针移动是否分层?有没有边界保护?比较规则是否符合题目要求
双指针的两大分支(同向快慢、相向左右)到这里就全部收尾,下一篇会进入滑动窗口算法——本质是同向快慢指针的进阶变体,继续更新本系列的踩坑记录。