Leetcode 125 验证回文串 | 判断子序列

1 题目

125. 验证回文串

如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串

字母和数字都属于字母数字字符。

给你一个字符串 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 * 105
  • s 仅由可打印的 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++ 实现验证回文串的功能,并且希望基于双指针的思路来解决,核心是先处理字符串(转小写、移除非字母数字字符),再用头尾指针对比字符是否一致。

实现思路

  1. 预处理字符串:遍历原字符串,只保留字母和数字字符,并将所有字母转为小写。
  2. 双指针验证回文:用左指针指向处理后字符串的起始位置,右指针指向末尾位置,逐一对比两个指针指向的字符是否相同。如果所有对应位置的字符都相同,则是回文串;否则不是。

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;
}

代码关键部分解释

  1. 字符预处理

    • isalnum(c):判断字符c是否是字母(a-z/A-Z)或数字(0-9),非字母数字字符会被过滤。
    • tolower(c):将大写字母转为小写,数字传入该函数会直接返回自身,不影响结果。
    • 预处理后的filtered字符串只包含小写字母和数字,无其他字符。
  2. 双指针逻辑

    • 左指针left初始为 0(字符串开头),右指针right初始为filtered.size()-1(字符串末尾)。
    • 循环条件left < right:只要左指针还没超过右指针,就继续对比。
    • 若某一次对比字符不同,直接返回false;若循环结束都未发现不同,返回true
  3. 边界情况处理

    • 当输入字符串预处理后为空(如示例 3 的空格),filtered.size()为 0,right初始为 - 1,循环不会执行,直接返回true,符合题目要求。

总结

  1. 核心步骤是预处理字符串 (过滤非字母数字 + 转小写)和双指针对比,两步结合即可验证回文串。
  2. 利用isalnumtolower函数可以高效完成字符预处理,避免手动判断字符范围的繁琐。
  3. 双指针法的时间复杂度为 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)"链式" 访问,没有直接指向 "前一个节点" 的指针,自然无法轻松前移。

总结

  1. 字符串 / 数组的 "双指针" 本质是索引操作,和链表 / 二叉树的 "指针" 不是一个概念,前者简单易操作,后者受限于数据结构本身;
  2. 链表的 "前移" 难点在于无反向指针,二叉树则额外多了 "分支选择" 的复杂度;
  3. 新手区分这一点很重要:遇到字符串 / 数组双指针题,放心大胆操作索引;遇到链表 / 二叉树指针题,先想 "怎么记录路径 / 父节点" 来实现回溯。

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

关键部分解释(对应你关心的 "第一个筛选")

  1. 判断字母数字字符 :C++ 中用 isalnum(c),JS 中没有直接对应的函数,最简洁的方式是用正则表达式 /[a-zA-Z0-9]/.test(c)

    • [a-zA-Z0-9] 匹配所有大小写字母和数字;
    • .test(c) 检查字符 c 是否符合这个规则,返回布尔值。
  2. 转小写 :C++ 中用 tolower(c),JS 中直接调用字符串的 toLowerCase() 方法即可,数字调用该方法会返回自身(和 C++ 行为一致)。

  3. 简化写法(可选):如果你想更简洁地实现过滤逻辑,也可以用正则替换一步到位(效果完全相同):

    javascript 复制代码
    // 一行代码替代循环过滤:移除所有非字母数字字符,再转小写
    const filtered = s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
    • /[^a-zA-Z0-9]/g^ 表示 "非",g 表示全局匹配,即替换所有非字母数字的字符为空;
    • 这种写法更符合 JS 风格,代码更短,效率也和循环一致。

总结

  1. JS 中没有 isalnum 这类字符判断函数,需用正则表达式 /[a-zA-Z0-9]/ 替代;
  2. 转小写直接用 toLowerCase() 方法,和 C++ 的 tolower 行为完全匹配;
  3. 过滤逻辑有两种写法:循环判断(和你 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(正确)。

总结

  1. 核心错误:正则表达式中多余的空格 违反了 "移除所有非字母数字字符" 的规则;
  2. 修复关键:将正则 /[a-zA-Z0-9 ]/ 改为 /[a-zA-Z0-9]/,只保留字母和数字;
  3. 验证技巧:遇到回文判断错误时,可先打印 filtered 字符串,检查是否包含不该保留的字符(如空格、标点)。

3 题目

392. 判断子序列

给定字符串 st ,判断 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 <= 100
  • 0 <= 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++ 代码,而且不清楚具体怎么写,我先帮你理清思路,再给出完整的可运行代码。

实现思路(双指针法,最优且最易理解)

这道题用双指针是最直接、效率也足够的解法,完全不需要暴力遍历或哈希(哈希反而不适合,因为要保证字符的相对位置)。核心逻辑如下:

  1. 定义两个指针:i 指向 s(子序列)的当前字符,j 指向 t(原始字符串)的当前字符,初始都为 0;
  2. 遍历 t
    • 如果 s[i] == t[j],说明匹配到一个字符,i 右移(继续匹配 s 的下一个字符);
    • 无论是否匹配,j 都右移(继续遍历 t 的下一个字符);
  3. 最终判断:如果 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;
}

代码关键部分解释

  1. 双指针初始化i=0(s 的起始)、j=0(t 的起始),分别跟踪两个字符串的匹配位置;
  2. 循环条件i < len_s && j < len_t------ 只要 s 还没匹配完,且 t 还没遍历完,就继续;
  3. 字符匹配逻辑
    • 匹配时 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;
  4. 返回值i == len_s 是核心 ------ 只要 i 走完 s,就说明 s 的所有字符都按顺序在 t 中找到,不管 t 有没有遍历完。

进阶问题说明(大量 S 的场景)

你提到的 "大量 S(10 亿个)判断是否是 T 的子序列",双指针法就不够高效了(每个 S 都要遍历 T,时间复杂度 O (k*len_t),k 是 S 的数量)。此时需要预处理 T

  1. 用哈希表(或数组)记录 T 中每个字符的所有出现位置(比如unordered_map<char, vector<int>> pos,key 是字符,value 是该字符在 T 中的索引列表);
  2. 对每个 S,遍历其字符,用二分查找在 T 的字符位置列表中找 "比上一个位置大的最小索引",判断是否存在;
  3. 预处理时间 O (len_t),每个 S 的判断时间 O (len_s * log len_t),适合大量 S 的场景。

总结

  1. 基础解法用双指针即可,时间复杂度 O (len_s + len_t),空间复杂度 O (1),完全满足题目基础要求;
  2. 双指针的核心是 "按顺序匹配":只移动匹配成功的 s 指针,t 指针一直往后走;
  3. 最终判断 s 指针是否走完,是解题的关键;
  4. 大量 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++ 的小区别)

  1. 相等判断
    • C++ 中用 == 即可;
    • JS 中建议用 ===(严格相等),避免隐式类型转换(比如 0 == false 会返回 true,但 0 === false 是 false),这里字符对比用 == 也能运行,但 === 更规范。
  2. 边界情况
    • s 为空字符串时,len_s = 0,循环不会执行,直接返回 i === len_s(0===0),符合 "空字符串是任何字符串的子序列" 的规则;
    • t 为空但 s 不为空时,len_t = 0,循环也不会执行,i 还是 0,返回 false,符合逻辑。

总结

  1. 核心错误:把 JS 的 length 属性写成了 length() 函数(混淆了 JS 和 C++ 的语法);
  2. 次要优化:JS 中字符对比用 === 替代 ==,更符合语言最佳实践;
  3. 逻辑本身:双指针的核心思路是完全正确的,只是语法细节出错了。

5 小结

康复训练一波。

字符串数组的双指针就是下标,不要忘了,和链表指针混了...

js的正则表达式语法 、length属性统统不能乱写了!

多写多练,加油,加油,有点仓促了。

相关推荐
List<String> error_P1 小时前
蓝桥杯高频考点练习:模拟问题“球队比分类”
数据结构·python·算法·模拟·球队比分
daxi1501 小时前
C语言从入门到进阶——第8讲:VS实用调试技巧
c语言·开发语言·c++·算法·蓝桥杯
m0_531237171 小时前
C语言-数组
c语言·开发语言·算法
啊阿狸不会拉杆1 小时前
《计算机视觉:模型、学习和推理》第 4 章-拟合概率模型
人工智能·python·学习·算法·机器学习·计算机视觉·拟合概率模型
ADDDDDD_Trouvaille1 小时前
2026.2.20——OJ92-94题
c++·算法
宇木灵2 小时前
C语言基础-四、函数
c语言·开发语言·前端·学习
hansang_IR2 小时前
【题解】类欧几里得算法
c++·数学·算法·类欧几里得
陈天伟教授2 小时前
人工智能应用- 人工智能交叉:06.解析蛋白质宇宙
人工智能·神经网络·算法·机器学习·推荐算法
We་ct2 小时前
LeetCode 114. 二叉树展开为链表:详细解题思路与 TS 实现
前端·数据结构·算法·leetcode·链表·typescript