文章目录
- 一、反转字符串
-
- [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) ,像灰姑娘的玻璃鞋------到点就消失。同样的机制也适用于 Number 和 Boolean。
可以用 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 中精确判断数据类型 的"终极武器",比typeof和instanceof更可靠。它利用的是: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 核心思路:贪心 + 双指针 + 分支验证
关键洞察:
-
先用双指针从两端向中间走,遇到不相等的字符时停下
-
此时有两个候选方案:跳过左边 OR 跳过右边
-
分别验证跳过后的子串是否为回文,任意一个验证通过即返回 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) | 只用了几个指针变量,无额外数组 |
四、全文总结
本文从一道看似简单的"反转字符串"切入,串联了三个层次的知识点:
- API 层面 :
split → reverse → join三段式反转,掌握字符串与数组的转换技巧 - 原理层面 :JS 包装类机制------简单类型为何能像对象一样调用属性,
Object.prototype.toString.call()精确类型判断 - 算法层面:回文判断的双指针解法,以及"最多删一个字符"的贪心 + 分支验证思路
五、核心知识点复盘
| 知识点 | 要点 |
|---|---|
| 字符串反转 | 字符串不可变,借助数组 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'],反转后还是自己,达不到效果。
字符串算法万变不离其宗,核心是对对称性的理解 和双指针的灵活运用。掌握文中这三道题的思路演变,面试中的字符串问题基本都能迎刃而解。