手撕字符串算法:反转、回文、验证回文 Ⅱ 完整拆解

文章目录

  • 一、反转字符串
    • [1.1 问题引入](#1.1 问题引入)
    • [1.2 解法:split → reverse → join](#1.2 解法:split → reverse → join)
    • [1.3 深入原理:JS 的"包装类"机制](#1.3 深入原理:JS 的"包装类"机制)
    • [1.4 补充:手写 reverse(不用 API)](#1.4 补充:手写 reverse(不用 API))
  • 二、判断一个字符串是否是回文字符串
    • [2.1 什么是回文串](#2.1 什么是回文串)
    • [2.2 解法一:API 流(简单直观)](#2.2 解法一:API 流(简单直观))
    • [2.3 解法二:双指针(更优解)](#2.3 解法二:双指针(更优解))
  • 三、回文字符串的衍生问题
    • [3.1 问题描述](#3.1 问题描述)
    • [3.2 核心思路:贪心 + 双指针 + 分支验证](#3.2 核心思路:贪心 + 双指针 + 分支验证)
    • [3.3 完整代码实现](#3.3 完整代码实现)
    • [3.4 易错点分析(重要)](#3.4 易错点分析(重要))
    • [3.5 复杂度分析](#3.5 复杂度分析)
  • 四、全文总结
  • 五、核心知识点复盘
  • [六、常见问题 / 避坑指南](#六、常见问题 / 避坑指南)

字符串是算法面试中的"高频考点",看似简单,实则暗藏许多细节。本文从反转字符串 出发,深入到回文判断 及其衍生变体,穿插 JS 底层原理,帮你一举拿下字符串相关面试题。


一、反转字符串

1.1 问题引入

在 JavaScript 中,字符串是不可变(immutable)的简单数据类型。一个常见面试题是:

给定一个字符串 "abc",如何将其反转为 "cba"

新手可能会直觉地写出 str.reverse(),但字符串原型上根本没有 reverse 方法------那是数组才有的 API。

1.2 解法:split → reverse → join

最经典的"三段式"解法:

js 复制代码
const str = 'abc';

// 1. split('')   → 将字符串打散为字符数组  ['a', 'b', 'c']
// 2. reverse()   → 反转数组                  ['c', 'b', 'a']
// 3. join('')    → 将数组重新拼接为字符串     'cba'
const reversed = str.split('').reverse().join('');

console.log(reversed); // 'cba'

思路解析:

步骤 方法 输入 输出
拆分 split('') 'abc' ['a', 'b', 'c']
反转 reverse() ['a', 'b', 'c'] ['c', 'b', 'a']
合并 join('') ['c', 'b', 'a'] 'cba'

关键点: split('') 用空字符串分割,能将每个字符拆成独立数组元素。如果写成 split() 不传参,会返回 ['abc'],后续反转就失效了。

1.3 深入原理:JS 的"包装类"机制

这里有一个值得追问的面试点:

js 复制代码
let str = 'abc';            // 简单数据类型
console.log(str.length);    // 3 --- 简单类型为什么能访问属性?

str 明明是简单数据类型(存在栈内存),按理说不应该有属性。但 JS 为了统一面向对象的编程体验,在底层做了"包装":

复制代码
str.length 访问流程:
  1. JS 引擎临时 new String(str)  →  将简单类型包装为 String 对象
  2. 返回 String 实例的 .length 属性
  3. 自动销毁临时对象,str 恢复为简单类型

这个过程被称为包装类(Wrapper Class) ,像灰姑娘的玻璃鞋------到点就消失。同样的机制也适用于 NumberBoolean

可以用 Object.prototype.toString 来验证:

js 复制代码
// 简单类型
Object.prototype.toString.call('abc');     // '[object String]'
Object.prototype.toString.call(123);       // '[object Number]'
Object.prototype.toString.call(true);      // '[object Boolean]'

// 引用类型
Object.prototype.toString.call([1,2,3]);   // '[object Array]'
Object.prototype.toString.call({a:1});     // '[object Object]'

核心知识点: Object.prototype.toString.call() 是 JS 中精确判断数据类型 的"终极武器",比 typeofinstanceof 更可靠。它利用的是:JS 一切皆是对象,都继承自 Object,而 toString 方法能返回内部的 [[Class]] 属性,从而区分不同子类型。

1.4 补充:手写 reverse(不用 API)

如果面试官追问"不借助数组 API 怎么反转",用双指针

js 复制代码
function reverseString(str) {
    const arr = str.split('');  // 字符串不可变,先转数组
    let left = 0;
    let right = arr.length - 1;

    while (left < right) {
        // 交换左右字符
        [arr[left], arr[right]] = [arr[right], arr[left]];
        left++;
        right--;
    }

    return arr.join('');
}

console.log(reverseString('abc')); // 'cba'

二、判断一个字符串是否是回文字符串

2.1 什么是回文串

回文串(Palindrome):正着读和反着读完全一样的字符串。

例如 "yessey",从左到右和从右到左都是 y-e-s-s-e-y

2.2 解法一:API 流(简单直观)

利用第一节的反转技巧,一行搞定:

js 复制代码
function isPalindrome(str) {
    return str === str.split('').reverse().join('');
}

console.log(isPalindrome('yessey')); // true
console.log(isPalindrome('hello'));  // false

时间复杂度: O(n),空间复杂度:O(n)(反转时创建了新字符串)。

2.3 解法二:双指针(更优解)

不需要额外空间,左右各一个指针向中间靠拢:

js 复制代码
function isPalindrome(str) {
    const len = str.length;

    for (let i = 0; i < len / 2; i++) {
        // 左指针 i,右指针 len - 1 - i
        if (str[i] !== str[len - 1 - i]) {
            return false; // 发现不对称,直接返回 false
        }
    }

    return true; // 完整遍历完,是对称的
}

console.log(isPalindrome('yessey')); // true
console.log(isPalindrome('hello'));  // false

思路图解:

复制代码
"yessey"
 ↑    ↑
 i    len-1-i

第1轮: y === y ✅ → i++
第2轮: e === e ✅ → i++
第3轮: s === s ✅ → i++ (i=3, len/2=3, 循环结束)
返回 true

面试重点: 双指针只需遍历一半 len/2,时间 O(n),空间 O(1),是面试官更想看到的解法。API 法虽然简洁,但创建了新数组和新字符串,空间开销更大。


三、回文字符串的衍生问题

3.1 问题描述

LeetCode 680. 验证回文字符串 Ⅱ

给定一个非空字符串 s最多删除一个字符。判断是否能成为回文字符串。

复制代码
示例1: "aba"  → true(本身就是回文)
示例2: "abca" → true(删除 'b' 或 'c' 后得到 "aca" 或 "aba")
示例3: "abc"  → false(删一个不够)

3.2 核心思路:贪心 + 双指针 + 分支验证

关键洞察:

  1. 先用双指针从两端向中间走,遇到不相等的字符时停下

  2. 此时有两个候选方案:跳过左边 OR 跳过右边

  3. 分别验证跳过后的子串是否为回文,任意一个验证通过即返回 true

    "a b c a"
    ↑ ↑
    L R

    第1轮: a === a ✅ → L++, R--
    第2轮: b !== c ❌ → 停下,分支验证:
    方案A: 跳过左边 → 验证 s[L+1 ... R] = "ca" → 不是回文
    方案B: 跳过右边 → 验证 s[L ... R-1] = "bc" → 不是回文
    结果: false(删一个不够)

    "a b c b a"
    ↑ ↑
    L R
    完整走完 while → 本身就是回文 → true

3.3 完整代码实现

js 复制代码
/**
 * 验证回文字符串 Ⅱ ------ 最多删除一个字符
 * @param {string} s
 * @return {boolean}
 */
function validPalindrome(s) {
    const len = s.length;
    let left = 0;
    let right = len - 1;

    // 第一阶段:双指针向中间收缩,直到遇到不等字符
    while (left < right && s[left] === s[right]) {
        left++;
        right--;
    }

    // 如果 left >= right,说明完整走完,本身就是回文
    if (left >= right) {
        return true;
    }

    // 第二阶段:分支验证
    // 方案A:跳过左边字符,验证 s[left+1 ... right]
    // 方案B:跳过右边字符,验证 s[left ... right-1]
    return isPalindrome(s, left + 1, right) || isPalindrome(s, left, right - 1);
}

/**
 * 辅助函数:验证 s[st...ed] 是否为回文(闭区间)
 * @param {string} s
 * @param {number} st  - 起始索引
 * @param {number} ed  - 结束索引
 * @return {boolean}
 */
function isPalindrome(s, st, ed) {
    while (st < ed) {
        if (s[st] !== s[ed]) {
            return false;
        }
        st++;
        ed--;
    }
    return true;
}

// 测试用例
console.log(validPalindrome('aba'));   // true  --- 本身就是回文
console.log(validPalindrome('abca'));  // true  --- 删 b 或删 c
console.log(validPalindrome('abc'));   // false --- 删一个不够
console.log(validPalindrome('deeee')); // true  --- 删第一个 d
console.log(validPalindrome('eeeed')); // true  --- 删最后一个 d

3.4 易错点分析(重要)

易错点 1:左右指针收缩逻辑要清晰

js 复制代码
// ❌ 错误写法:条件判断写反
while (left < right && s[left] !== s[right]) { ... }

// ✅ 正确写法:相等时收缩,不等时停下
while (left < right && s[left] === s[right]) {
    left++;
    right--;
}

易错点 2:分支验证别忘了本身是回文的情况

js 复制代码
// "aba" 这样的字符串,while 循环会完整走完
// left=1, right=1 → left >= right → 直接返回 true
// 如果不加这个判断,就会错误地进入分支验证

易错点 3:辅助 isPalindrome 用闭区间而非开区间

js 复制代码
// ✅ 闭区间 [st, ed],更符合直觉
isPalindrome(s, left + 1, right)  // 跳过左边
isPalindrome(s, left, right - 1)  // 跳过右边

// ❌ 如果用开区间容易搞混边界

3.5 复杂度分析

指标 说明
时间复杂度 O(n) 最坏情况两次遍历(主循环 + 一次 isPalindrome)
空间复杂度 O(1) 只用了几个指针变量,无额外数组

四、全文总结

本文从一道看似简单的"反转字符串"切入,串联了三个层次的知识点:

  1. API 层面split → reverse → join 三段式反转,掌握字符串与数组的转换技巧
  2. 原理层面 :JS 包装类机制------简单类型为何能像对象一样调用属性,Object.prototype.toString.call() 精确类型判断
  3. 算法层面:回文判断的双指针解法,以及"最多删一个字符"的贪心 + 分支验证思路

五、核心知识点复盘

知识点 要点
字符串反转 字符串不可变,借助数组 split→reverse→join
包装类 简单类型访问属性时,JS 临时 new String() 再销毁
类型判断 Object.prototype.toString.call() 是判断类型的终极方法
回文判断 双指针向中间收缩,比较对称字符,时间 O(n) 空间 O(1)
回文 Ⅱ 遇到不等时分支验证,跳过左或跳过右,短路求值

六、常见问题 / 避坑指南

Q1:为什么不用 str.reverse()

JS 字符串原型上没有 reverse 方法。字符串是不可变的,reverse 是数组的方法。先 split 转数组再操作。

Q2:split('')split() 有什么区别?

split('') 按每个字符拆分;split() 不传参返回整个字符串作为单元素数组 ['abc'],反转后还是自己,达不到效果。


字符串算法万变不离其宗,核心是对对称性的理解双指针的灵活运用。掌握文中这三道题的思路演变,面试中的字符串问题基本都能迎刃而解。

相关推荐
Darling噜啦啦1 小时前
前端存储与 this 指向完全指南:从 LocalStorage 实战到 call/apply/bind 深度解析
前端·javascript
To_OC1 小时前
从一行报错开始,把字符串反转、回文算法连带着包装类一起捋明白
javascript·算法·api
LCG米1 小时前
机器人控制系统与运动规划:从RRT算法到ROS move_base实战
算法·机器人
QiLinkOS1 小时前
第三视觉理解徐玉生与他的商业活动(26)
大数据·c++·人工智能·算法·开源协议
手写码匠2 小时前
手写 LLM 结构化输出引擎 —— 从 JSON Schema 约束到类型安全的数据提取
人工智能·深度学习·算法·aigc
zhiSiBuYu05172 小时前
重排序(Rerank)提升检索准确率实战指南
开发语言·python·算法
蜡台2 小时前
Node 安装 awesome-qr 失败解决
javascript·vue·qrcode·awesome-qr
月疯2 小时前
华为手环的部分功能
算法
郭梧悠2 小时前
算法:有效的括号
python·算法·leetcode