1 题目
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
字母和数字都属于字母数字字符。
给你一个字符串 s,如果它是 回文串 ,返回 true;否则,返回false。
示例 1:
输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。
示例 2:
输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。
示例 3:
输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。
由于空字符串正着反着读都一样,所以是回文串。
提示:
1 <= s.length <= 2 * 105s仅由可打印的 ASCII 字符组成
2 代码实现
c++
cpp
class Solution {
public:
bool isPalindrome(string s) {
string filtered ;
for (char c : s ){
if (isalnum(c)){
filtered += tolower(c);
}
}
int left = 0 ;
int right = filtered.size() - 1 ;
while (left < right ){
if (filtered[left] != filtered[right]){
return false ;
}
left ++ ;
right -- ;
}
return true ;
}
};
js
javascript
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function(s) {
let filtered = '';
for (let c of s ){
if (/[a-zA-Z0-9]/.test(c)){
filtered += c.toLowerCase();
}
}
let left = 0 ;
let right = filtered.length - 1 ;
while (left < right ){
if (filtered[left] != filtered[right ]){
return false ;
}
left ++;
right --;
}
return true ;
};
思考
双指针。头尾指针是否相同?尾指针怎么向前走?怎么实现第一趟转成只有小写字母的字符串?
题解
你想要用 C++ 实现验证回文串的功能,并且希望基于双指针的思路来解决,核心是先处理字符串(转小写、移除非字母数字字符),再用头尾指针对比字符是否一致。
实现思路
- 预处理字符串:遍历原字符串,只保留字母和数字字符,并将所有字母转为小写。
- 双指针验证回文:用左指针指向处理后字符串的起始位置,右指针指向末尾位置,逐一对比两个指针指向的字符是否相同。如果所有对应位置的字符都相同,则是回文串;否则不是。
C++ 完整代码实现
cpp
#include <iostream>
#include <string>
#include <cctype> // 包含isalnum、tolower等字符处理函数
using namespace std;
class Solution {
public:
bool isPalindrome(string s) {
// 第一步:预处理字符串,只保留字母数字并转小写
string filtered;
for (char c : s) {
// isalnum判断是否是字母或数字
if (isalnum(c)) {
// tolower将大写字母转小写,数字无影响
filtered += tolower(c);
}
}
// 第二步:双指针验证回文
int left = 0; // 左指针,从头部开始
int right = filtered.size() - 1; // 右指针,从尾部开始
while (left < right) {
// 对比左右指针的字符
if (filtered[left] != filtered[right]) {
return false; // 有字符不同,不是回文
}
left++; // 左指针右移
right--; // 右指针左移
}
return true; // 所有字符都匹配,是回文
}
};
// 测试用例
int main() {
Solution solution;
// 示例1
string s1 = "A man, a plan, a canal: Panama";
cout << (solution.isPalindrome(s1) ? "true" : "false") << endl; // 输出true
// 示例2
string s2 = "race a car";
cout << (solution.isPalindrome(s2) ? "true" : "false") << endl; // 输出false
// 示例3
string s3 = " ";
cout << (solution.isPalindrome(s3) ? "true" : "false") << endl; // 输出true
return 0;
}
代码关键部分解释
-
字符预处理:
isalnum(c):判断字符c是否是字母(a-z/A-Z)或数字(0-9),非字母数字字符会被过滤。tolower(c):将大写字母转为小写,数字传入该函数会直接返回自身,不影响结果。- 预处理后的
filtered字符串只包含小写字母和数字,无其他字符。
-
双指针逻辑:
- 左指针
left初始为 0(字符串开头),右指针right初始为filtered.size()-1(字符串末尾)。 - 循环条件
left < right:只要左指针还没超过右指针,就继续对比。 - 若某一次对比字符不同,直接返回
false;若循环结束都未发现不同,返回true。
- 左指针
-
边界情况处理:
- 当输入字符串预处理后为空(如示例 3 的空格),
filtered.size()为 0,right初始为 - 1,循环不会执行,直接返回true,符合题目要求。
- 当输入字符串预处理后为空(如示例 3 的空格),
总结
- 核心步骤是预处理字符串 (过滤非字母数字 + 转小写)和双指针对比,两步结合即可验证回文串。
- 利用
isalnum和tolower函数可以高效完成字符预处理,避免手动判断字符范围的繁琐。 - 双指针法的时间复杂度为 O (n)(n 为原字符串长度),空间复杂度为 O (n)(存储预处理后的字符串),在题目数据范围(2*10^5)内效率足够。
如果想优化空间复杂度,可以不单独存储预处理后的字符串,而是在原字符串上直接用双指针跳过非字母数字字符,不过代码可读性会稍低,新手先掌握上述实现方式即可。
关于指针
【字符串的双指针其实就是类似数组操作啦,我还以为链表一样的前移指针。。链表二叉树等等指针前移不是很好做】
你这个观察特别到位!字符串的双指针确实更像数组操作,和链表、二叉树里的指针移动逻辑完全不同,后者的指针前移 / 回溯确实要复杂得多。
先帮你理清核心区别
我们可以用通俗的方式对比这几种场景下的 "指针",帮你彻底分清:
| 场景 | "指针" 的本质 | 移动 / 回溯的难度 | 核心原因 |
|---|---|---|---|
| 字符串 / 数组 | 本质是索引(下标) | 极低 | 内存连续,通过left++/right--就能直接定位任意位置,想回退就left--,无任何限制 |
| 单向链表 | 真正的内存地址指针 | 高 | 内存不连续,每个节点只有next指针(无prev),想 "前移" 只能从头重新遍历,无法直接回退 |
| 二叉树 | 节点指针(多叉分支) | 更高 | 不仅内存不连续,还有左 / 右子树分支,回溯需要依赖栈(递归)或记录父节点,逻辑更复杂 |
举个简单例子,帮你直观感受
1. 字符串双指针(数组式)------ 想动就动
cpp
string s = "abcde";
int left = 0, right = s.size()-1;
left++; // 从0→1(指向b),直接操作索引
right--; // 从4→3(指向d),无任何成本
left--; // 回退到0(指向a),轻松实现
2. 单向链表指针 ------ 前移几乎做不到
cpp
// 链表节点定义
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
ListNode* cur = head; // 假设head是链表头节点
cur = cur->next; // 后移:容易,直接取next
// 想前移?没prev指针,只能从头节点重新遍历到目标位置,无法直接cur = cur->prev
3. 二叉树指针 ------ 回溯需要额外记录
cpp
// 二叉树节点定义
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
// 遍历到右子树后想回到父节点?
// 要么用递归(系统栈自动记录),要么手动用栈/哈希表存父节点,无法直接"前移"
为什么会有这种差异?
核心是数据结构的存储方式和访问规则:
- 字符串 / 数组:连续内存 + 随机访问 → 索引就是 "万能钥匙",想访问哪个位置都能直接定位,指针(索引)移动 / 回退无成本;
- 链表 / 二叉树:离散内存 + 顺序访问 → 只能通过节点的指针(next/left/right)"链式" 访问,没有直接指向 "前一个节点" 的指针,自然无法轻松前移。
总结
- 字符串 / 数组的 "双指针" 本质是索引操作,和链表 / 二叉树的 "指针" 不是一个概念,前者简单易操作,后者受限于数据结构本身;
- 链表的 "前移" 难点在于无反向指针,二叉树则额外多了 "分支选择" 的复杂度;
- 新手区分这一点很重要:遇到字符串 / 数组双指针题,放心大胆操作索引;遇到链表 / 二叉树指针题,先想 "怎么记录路径 / 父节点" 来实现回溯。
js实现
你想知道在 JavaScript 中如何实现这段 C++ 代码里过滤并处理字符串的核心逻辑(保留字母数字 + 转小写),我来帮你把这部分逻辑完整转换成 JS 代码,同时保持整体验证回文串的功能一致。
JavaScript 完整实现(对应你的 C++ 代码)
javascript
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function(s) {
// 第一步:过滤非字母数字字符,并转为小写(对应你C++里的filtered逻辑)
let filtered = '';
for (let c of s) {
// JS中用正则判断是否是字母/数字(等价于C++的isalnum)
if (/[a-zA-Z0-9]/.test(c)) {
// 转小写(等价于C++的tolower)
filtered += c.toLowerCase();
}
}
// 第二步:双指针验证回文(逻辑和C++完全一致)
let left = 0;
let right = filtered.length - 1;
while (left < right) {
if (filtered[left] !== filtered[right]) {
return false;
}
left++;
right--;
}
return true;
};
// 测试用例(和你C++的测试逻辑一致)
console.log(isPalindrome("A man, a plan, a canal: Panama")); // true
console.log(isPalindrome("race a car")); // false
console.log(isPalindrome(" ")); // true
关键部分解释(对应你关心的 "第一个筛选")
-
判断字母数字字符 :C++ 中用
isalnum(c),JS 中没有直接对应的函数,最简洁的方式是用正则表达式/[a-zA-Z0-9]/.test(c):[a-zA-Z0-9]匹配所有大小写字母和数字;.test(c)检查字符c是否符合这个规则,返回布尔值。
-
转小写 :C++ 中用
tolower(c),JS 中直接调用字符串的toLowerCase()方法即可,数字调用该方法会返回自身(和 C++ 行为一致)。 -
简化写法(可选):如果你想更简洁地实现过滤逻辑,也可以用正则替换一步到位(效果完全相同):
javascript// 一行代码替代循环过滤:移除所有非字母数字字符,再转小写 const filtered = s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();/[^a-zA-Z0-9]/g:^表示 "非",g表示全局匹配,即替换所有非字母数字的字符为空;- 这种写法更符合 JS 风格,代码更短,效率也和循环一致。
总结
- JS 中没有
isalnum这类字符判断函数,需用正则表达式/[a-zA-Z0-9]/替代; - 转小写直接用
toLowerCase()方法,和 C++ 的tolower行为完全匹配; - 过滤逻辑有两种写法:循环判断(和你 C++ 逻辑一一对应)、正则替换(更简洁的 JS 风格),均可实现需求。
自己弄为什么又是错的(js)
javascript
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function(s) {
let filtered = '';
for (let c of s ){
if (/[a-zA-Z0-9 ]/.test(c)){
filtered += c.toLowerCase();
}
}
let left = 0 ;
let right = filtered.length - 1 ;
while (left < right ){
if (filtered[left] != filtered[right ]){
return false ;
}
left ++;
right --;
}
return true ;
};
你这段代码出错的核心原因是过滤规则写错了------ 你在正则里多加了一个空格 ,导致空格没有被过滤掉,这和题目要求的 "移除所有非字母数字字符" 不符,最终会让判断结果出错。
先解释错误原因
题目明确要求:只保留字母和数字 ,移除所有其他字符(包括空格、标点等)。你写的正则 /[a-zA-Z0-9 ]/ 中多了一个空格 ,意味着:
- 空格会被保留到
filtered字符串中; - 比如测试用例
s = "A man, a plan, a canal: Panama",处理后会变成"a man a plan a canal panama"(包含空格); - 双指针对比时,左边的空格和右边的字符不匹配,直接返回
false,但正确结果应该是true。
修复后的完整代码
javascript
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function(s) {
let filtered = '';
for (let c of s ){
// 去掉正则里的空格,只匹配字母和数字
if (/[a-zA-Z0-9]/.test(c)){
filtered += c.toLowerCase();
}
}
let left = 0 ;
let right = filtered.length - 1 ;
while (left < right ){
if (filtered[left] != filtered[right ]){
return false ;
}
left ++;
right --;
}
return true ;
};
// 测试验证
console.log(isPalindrome("A man, a plan, a canal: Panama")); // true(正确)
console.log(isPalindrome("race a car")); // false(正确)
console.log(isPalindrome(" ")); // true(正确,过滤后为空字符串)
额外验证:为什么空格会导致错误?
以示例 1 为例:
- 错误代码处理后:
filtered = "a man a plan a canal panama"(含空格);双指针第一次对比:left=0(字符a),right=filtered.length-1(字符a)→ 匹配;第二次:left=1(空格),right=filtered.length-2(字符m)→ 不匹配,返回false(错误)。 - 修复后代码处理后:
filtered = "amanaplanacanalpanama"(无空格);双指针逐一对齐匹配,最终返回true(正确)。
总结
- 核心错误:正则表达式中多余的空格 违反了 "移除所有非字母数字字符" 的规则;
- 修复关键:将正则
/[a-zA-Z0-9 ]/改为/[a-zA-Z0-9]/,只保留字母和数字; - 验证技巧:遇到回文判断错误时,可先打印
filtered字符串,检查是否包含不该保留的字符(如空格、标点)。
3 题目
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
致谢:
特别感谢@pbrother添加此问题并且创建所有测试用例。
示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:
输入:s = "axc", t = "ahbgdc"
输出:false
提示:
0 <= s.length <= 1000 <= t.length <= 10^4- 两个字符串都只由小写字符组成。
4 代码实现
c++
cpp
class Solution {
public:
bool isSubsequence(string s, string t) {
int result = 0;
vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
dp[i][j] =dp[i][j - 1];
}
if (dp[i][j] > result)
result = dp[i][j];
}
}
if (result == s.size())
return true;
return false;
}
};
js
javascript
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function(s, t) {
let i = 0 ;
let j = 0 ;
let len_s = s.length ;
let len_t = t.length ;
while (i < len_s && j < len_t){
if (s[i] === t[j]){
i++ ;
}
j++;
}
return i === len_s ;
};
思考
怎么做,暴力遍历吗?还是用hash,我看到这个也是双指针的,然后怎么做?不会写啊!!!
题解
你现在想解决 "判断子序列" 的问题,核心是想用双指针的思路实现 C++ 代码,而且不清楚具体怎么写,我先帮你理清思路,再给出完整的可运行代码。
实现思路(双指针法,最优且最易理解)
这道题用双指针是最直接、效率也足够的解法,完全不需要暴力遍历或哈希(哈希反而不适合,因为要保证字符的相对位置)。核心逻辑如下:
- 定义两个指针:
i指向s(子序列)的当前字符,j指向t(原始字符串)的当前字符,初始都为 0; - 遍历
t:- 如果
s[i] == t[j],说明匹配到一个字符,i右移(继续匹配s的下一个字符); - 无论是否匹配,
j都右移(继续遍历t的下一个字符);
- 如果
- 最终判断:如果
i走到了s的末尾,说明s的所有字符都按顺序在t中找到,是子序列;否则不是。
C++ 完整代码实现
cpp
#include <iostream>
#include <string>
using namespace std;
class Solution {
public:
bool isSubsequence(string s, string t) {
// 初始化双指针:i指向s,j指向t
int i = 0, j = 0;
int len_s = s.size(), len_t = t.size();
// 遍历t,直到其中一个指针越界
while (i < len_s && j < len_t) {
// 如果当前字符匹配,s的指针右移
if (s[i] == t[j]) {
i++;
}
// 无论是否匹配,t的指针都右移(继续找下一个字符)
j++;
}
// 若i走到s末尾,说明所有字符都按顺序匹配完成
return i == len_s;
}
};
// 测试用例
int main() {
Solution solution;
// 示例1
string s1 = "abc", t1 = "ahbgdc";
cout << (solution.isSubsequence(s1, t1) ? "true" : "false") << endl; // 输出true
// 示例2
string s2 = "axc", t2 = "ahbgdc";
cout << (solution.isSubsequence(s2, t2) ? "true" : "false") << endl; // 输出false
// 边界用例:s为空(空字符串是任何字符串的子序列)
string s3 = "", t3 = "abc";
cout << (solution.isSubsequence(s3, t3) ? "true" : "false") << endl; // 输出true
// 边界用例:t为空但s不为空
string s4 = "a", t4 = "";
cout << (solution.isSubsequence(s4, t4) ? "true" : "false") << endl; // 输出false
return 0;
}
代码关键部分解释
- 双指针初始化 :
i=0(s 的起始)、j=0(t 的起始),分别跟踪两个字符串的匹配位置; - 循环条件 :
i < len_s && j < len_t------ 只要 s 还没匹配完,且 t 还没遍历完,就继续; - 字符匹配逻辑 :
- 匹配时
i++:比如 s="abc"、t="ahbgdc",先匹配 s [0]='a' 和 t [0]='a',i 变成 1;再匹配 s [1]='b' 和 t [2]='b',i 变成 2;最后匹配 s [2]='c' 和 t [5]='c',i 变成 3(等于 s 的长度 3); - 不匹配时只
j++:比如 s="axc",匹配完 'a' 后,s [1]='x' 在 t 中找不到,j 一直走到 t 末尾,i 始终停在 1,最终 i≠3,返回 false;
- 匹配时
- 返回值 :
i == len_s是核心 ------ 只要 i 走完 s,就说明 s 的所有字符都按顺序在 t 中找到,不管 t 有没有遍历完。
进阶问题说明(大量 S 的场景)
你提到的 "大量 S(10 亿个)判断是否是 T 的子序列",双指针法就不够高效了(每个 S 都要遍历 T,时间复杂度 O (k*len_t),k 是 S 的数量)。此时需要预处理 T:
- 用哈希表(或数组)记录 T 中每个字符的所有出现位置(比如
unordered_map<char, vector<int>> pos,key 是字符,value 是该字符在 T 中的索引列表); - 对每个 S,遍历其字符,用二分查找在 T 的字符位置列表中找 "比上一个位置大的最小索引",判断是否存在;
- 预处理时间 O (len_t),每个 S 的判断时间 O (len_s * log len_t),适合大量 S 的场景。
总结
- 基础解法用双指针即可,时间复杂度 O (len_s + len_t),空间复杂度 O (1),完全满足题目基础要求;
- 双指针的核心是 "按顺序匹配":只移动匹配成功的 s 指针,t 指针一直往后走;
- 最终判断 s 指针是否走完,是解题的关键;
- 大量 S 的场景需预处理 T + 二分查找,核心是用空间换时间。
自己写js又是错错错
javascript
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function(s, t) {
let i = 0 ;
let j = 0 ;
let len_s = s.length() ;
let len_t = t.length() ;
while (i < len_s && j < len_t){
if (s[i] == t[j]){
i++ ;
}
j++;
}
return i == len_s ;
};
这段 JS 代码出错的核心原因是字符串长度的获取方式写错了 ------JS 中获取字符串长度是 length(属性),而不是 length()(函数),这是 JS 和 C++ 的关键区别,也是新手最容易踩的坑。
先指出错误点
在 C++ 中,string 的长度是通过 size()/length() 函数获取(比如 s.size());但在 JavaScript 中:
- 字符串、数组的长度是只读属性 ,写法是
s.length(没有括号); - 你写的
s.length()、t.length()会直接报错(length is not a function),导致代码无法执行。
修复后的完整代码
javascript
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isSubsequence = function(s, t) {
let i = 0;
let j = 0;
// 修复:去掉括号,用length属性获取长度
let len_s = s.length;
let len_t = t.length;
while (i < len_s && j < len_t) {
if (s[i] === t[j]) { // 建议用===(严格相等),更符合JS最佳实践
i++;
}
j++;
}
return i === len_s;
};
// 测试用例验证
console.log(isSubsequence("abc", "ahbgdc")); // true(正确)
console.log(isSubsequence("axc", "ahbgdc")); // false(正确)
console.log(isSubsequence("", "ahbgdc")); // true(正确,空字符串是子序列)
console.log(isSubsequence("a", "")); // false(正确)
额外补充(JS 和 C++ 的小区别)
- 相等判断 :
- C++ 中用
==即可; - JS 中建议用
===(严格相等),避免隐式类型转换(比如0 == false会返回 true,但0 === false是 false),这里字符对比用==也能运行,但===更规范。
- C++ 中用
- 边界情况 :
- 当
s为空字符串时,len_s = 0,循环不会执行,直接返回i === len_s(0===0),符合 "空字符串是任何字符串的子序列" 的规则; - 当
t为空但s不为空时,len_t = 0,循环也不会执行,i还是 0,返回false,符合逻辑。
- 当
总结
- 核心错误:把 JS 的
length属性写成了length()函数(混淆了 JS 和 C++ 的语法); - 次要优化:JS 中字符对比用
===替代==,更符合语言最佳实践; - 逻辑本身:双指针的核心思路是完全正确的,只是语法细节出错了。
5 小结
康复训练一波。
字符串数组的双指针就是下标,不要忘了,和链表指针混了...
js的正则表达式语法 、length属性统统不能乱写了!
多写多练,加油,加油,有点仓促了。