验证回文串 II 图解题解
这道题到底在问什么
- 输入
- s = "abcdeexdcba"
- 输出
- true(删掉那个多出来的 'x',剩下的串正反一样)
最优解:一步一步想明白
- 3记住三件事:相等就往里收;第一次撞到不等时,分别试「删左」和「删右」;两条岔路有一条通,答案就是 true。下面每一帧都在套它。
- 4开始:左指针 l 停在下标 0,右指针 r 停在最后一格。删除名额还留着,先按普通回文那样一对一对地比。
- 5左边 'a' 和右边 'a' 一样,这一对配上了,可以放心往里收。
- 6l 前进到下标 1、r 后退到下标 9,继续比下一对(绿色是已经确认相等的部分)。
- 7左边 'b' 和右边 'b' 一样,这一对配上了,可以放心往里收。
- 8l 前进到下标 2、r 后退到下标 8,继续比下一对(绿色是已经确认相等的部分)。
- 9左边 'c' 和右边 'c' 一样,这一对配上了,可以放心往里收。
- 10l 前进到下标 3、r 后退到下标 7,继续比下一对(绿色是已经确认相等的部分)。
- 11左边 'd' 和右边 'd' 一样,这一对配上了,可以放心往里收。
- 12l 前进到下标 4、r 后退到下标 6,继续比下一对(绿色是已经确认相等的部分)。
- 13撞上了:左边 'e' 和右边 'x' 对不上(标红)。普通回文到这就失败了,但我们还有一次删除机会,下面分两条岔路试。
- 14岔路一:假设删掉左边那个 'e'(标灰表示当它不存在),那么要比的就变成「下标 5 到 6」这一段,看它本身是不是回文。
- 15删左这条路在 'e' 与 'x' 上又对不上了(这次没有删除名额可用了),所以「删左」走不通。
- 16岔路二:换一种删法,假设删掉右边那个 'x'(标灰当它不存在),要比的就变成「下标 4 到 5」这一段,再看它是不是回文。
- 17删右这条路上,'e' 和 'e' 相等,这一对配上了。
- 18删右这段的指针交错而过,一路都相等。
- 19删右这条路一路对上,剩下的部分是回文,删右可行——只要有一条岔路通,整体答案就成立。
- 20结论:删掉那个对不上的 'x',剩下的字符正读倒读完全一样,所以答案是 true。一次删除机会刚好用在了刀刃上。
⚠️ 容易写错的地方
✗ 错:撞到不等就直接返回 false
✓ 对:撞到不等时分别试删左、删右
题目允许删一个字符,不试这两条岔路会把本可补救的串误判为非回文
✗ 错:只试了「删左」或只试了「删右」
✓ 对:两条岔路用「或」都试
到底该删哪一个,事先并不知道;只试一条会漏掉另一条能通的情况
✗ 错:分支检查时用字符串切片复制子串
✓ 对:给 isPal 传 i、j 下标,原地比较
每次切片都新建字符串,空间退化成 O(n),且没必要
完整代码(Python / C++ / Java)
Python
def validPalindrome(s):
def isPal(i, j): # 普通回文判断
while i < j:
if s[i] != s[j]: return False
i += 1; j -= 1
return True
l, r = 0, len(s) - 1
while l < r:
if s[l] != s[r]: # 撞到不等:用掉删除名额
return isPal(l + 1, r) or isPal(l, r - 1)
l += 1; r -= 1
return TrueC++
class Solution {
bool isPal(const string& s, int i, int j){
while (i < j){ if (s[i]!=s[j]) return false; i++; j--; }
return true;
}
public:
bool validPalindrome(string s){
int l = 0, r = s.size() - 1;
while (l < r){
if (s[l] != s[r])
return isPal(s, l+1, r) || isPal(s, l, r-1);
l++; r--;
}
return true;
}
};Java
class Solution {
private boolean isPal(String s, int i, int j) {
while (i < j) { if (s.charAt(i)!=s.charAt(j)) return false; i++; j--; }
return true;
}
public boolean validPalindrome(String s) {
int l = 0, r = s.length() - 1;
while (l < r) {
if (s.charAt(l) != s.charAt(r))
return isPal(s, l+1, r) || isPal(s, l, r-1);
l++; r--;
}
return true;
}
}复杂度
时间
O(n)
主扫描最多走半趟;撞到不等后两个分支各最多再走半趟,合起来仍是线性
空间
O(1)
只用 l、r 两个下标,不额外开数组(递归/切片版本可能多花空间,这里用下标避免)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 验证回文串 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么撞到不等时,只需要用一次删除机会、不用递归地继续允许删除?+
因为题目规定最多删一个字符。主循环前面全是相等的对,机会一直没用;第一次撞到不等就是唯一一次该用机会的地方,分支里再撞到不等就没机会了,直接 false。
分支里的 isPal 为什么不再允许删字符?+
删除名额总共只有一个,进分支就意味着这唯一的一次已经用在「跳过左/右字符」上了,所以分支内部是严格的普通回文判断,不能再删。
如果改成『最多删 k 个字符』,这套思路还能直接用吗?+
不能直接套。k=1 时分两支即可;k 更大时分支会指数级膨胀,一般改用区间 DP,状态记『区间 [i,j] 最少删几个能成回文』。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 验证回文串 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。