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

文章目录

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

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


一、反转字符串

1.1 问题引入

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

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

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

1.2 解法:split → reverse → join

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

conststr='abc';// 1. split('') → 将字符串打散为字符数组 ['a', 'b', 'c']// 2. reverse() → 反转数组 ['c', 'b', 'a']// 3. join('') → 将数组重新拼接为字符串 'cba'constreversed=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 的"包装类"机制

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

letstr='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来验证:

// 简单类型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 怎么反转",用双指针

functionreverseString(str){constarr=str.split('');// 字符串不可变,先转数组letleft=0;letright=arr.length-1;while(left<right){// 交换左右字符[arr[left],arr[right]]=[arr[right],arr[left]];left++;right--;}returnarr.join('');}console.log(reverseString('abc'));// 'cba'

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

2.1 什么是回文串

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

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

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

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

functionisPalindrome(str){returnstr===str.split('').reverse().join('');}console.log(isPalindrome('yessey'));// trueconsole.log(isPalindrome('hello'));// false

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

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

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

functionisPalindrome(str){constlen=str.length;for(leti=0;i<len/2;i++){// 左指针 i,右指针 len - 1 - iif(str[i]!==str[len-1-i]){returnfalse;// 发现不对称,直接返回 false}}returntrue;// 完整遍历完,是对称的}console.log(isPalindrome('yessey'));// trueconsole.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 完整代码实现

/** * 验证回文字符串 Ⅱ —— 最多删除一个字符 * @param {string} s * @return {boolean} */functionvalidPalindrome(s){constlen=s.length;letleft=0;letright=len-1;// 第一阶段:双指针向中间收缩,直到遇到不等字符while(left<right&&s[left]===s[right]){left++;right--;}// 如果 left >= right,说明完整走完,本身就是回文if(left>=right){returntrue;}// 第二阶段:分支验证// 方案A:跳过左边字符,验证 s[left+1 ... right]// 方案B:跳过右边字符,验证 s[left ... right-1]returnisPalindrome(s,left+1,right)||isPalindrome(s,left,right-1);}/** * 辅助函数:验证 s[st...ed] 是否为回文(闭区间) * @param {string} s * @param {number} st - 起始索引 * @param {number} ed - 结束索引 * @return {boolean} */functionisPalindrome(s,st,ed){while(st<ed){if(s[st]!==s[ed]){returnfalse;}st++;ed--;}returntrue;}// 测试用例console.log(validPalindrome('aba'));// true — 本身就是回文console.log(validPalindrome('abca'));// true — 删 b 或删 cconsole.log(validPalindrome('abc'));// false — 删一个不够console.log(validPalindrome('deeee'));// true — 删第一个 dconsole.log(validPalindrome('eeeed'));// true — 删最后一个 d

3.4 易错点分析(重要)

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

// ❌ 错误写法:条件判断写反while(left<right&&s[left]!==s[right]){...}// ✅ 正确写法:相等时收缩,不等时停下while(left<right&&s[left]===s[right]){left++;right--;}

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

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

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

// ✅ 闭区间 [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'],反转后还是自己,达不到效果。


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