最近在整理算法笔记时,发现字符串相关的面试题真是常青树。今天就借着我的学习笔记,和大家聊聊字符串算法中的几个经典问题,以及JS中一些容易被忽略的底层细节。
一、反转字符串,你真的会了吗?
先来个开胃菜——反转字符串。别看它简单,里面藏着不少JS的“坑”和设计哲学。
最直接的解法
javascript
const str = 'abc';
const res = str.split('').reverse().join('');
console.log(res); // 'cba'
核心思路就三步:
split('')—— 把字符串拆成字符数组reverse()—— 反转数组join('')—— 把数组拼回字符串
为什么不能直接在字符串上调用 reverse()?
因为字符串是简单数据类型(基本类型),而 reverse() 是数组原型上的方法。JS 的字符串并没有提供 reverse API,但数组有。
包装类的“魔法”
你可能好奇过:既然 str 是简单数据类型,为什么 str.length 能用?
javascript
const str = 'abc';
console.log(str.length); // 3
这背后其实是 JS 的包装类在悄悄工作。当我们试图访问基本类型的属性时,JS 底层会临时用 new String(str) 把它包装成一个 String 对象实例,调用完属性/方法后,再把它“打回原形”变回简单数据类型。
就像灰姑娘的魔法,到了点就恢复原样 ✨
javascript
const str2 = new String("abc"); // 这才是真正的字符串对象
console.log(str2.length); // 3
console.log(Object.prototype.toString.call(str2)); // "[object String]"
二、一切皆对象,Object.prototype.toString 的妙用
在学习过程中,有一个知识点让我豁然开朗 —— Object.prototype.toString 的作用。
javascript
let o = { a: 1, b: 2 };
o.toString(); // "[object Object]"
JSON.stringify(o); // '{"a":1,"b":2}'
toString() 的职责是把对象序列化(字符串化) ,而 JSON.stringify 则是更严格的 JSON 序列化。
但为什么 [1,2,3].toString() 返回的是 '1,2,3',而普通对象的 toString 返回 "[object Object]"?
因为不同的子类型重写了 toString 方法,用来细化类型的区分。这也是为什么我们可以用 Object.prototype.toString.call(obj) 来精准判断一个变量的类型:
javascript
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call({}) // "[object Object]"
顺便提一句笔记里的重点:call 可以让一个方法借给其他对象使用,这是 JS 函数式特性的体现。
javascript
let o = {
name: '胡老板',
say: function() {
console.log('我是' + this.name);
}
};
let obj = { name: '张三' };
o.say.call(obj); // '我是张三' —— this 被指向了 obj
三、判断回文字符串
回文字符串:正着读和反着读都一样。比如 'abcba'、'racecar'。
解法一:API 流(简洁但不一定高效)
javascript
function isPalindrome(str) {
return str === str.split('').reverse().join('');
}
解法二:双指针(推荐)
利用对称特性,从两端往中间比较:
javascript
function isPalindrome(str) {
const len = str.length;
for (let i = 0; i < len / 2; i++) {
if (str[i] !== str[len - 1 - i]) {
return false;
}
}
return true;
}
小细节:为什么把
len存成变量而不是每次都str.length?因为str.length在循环中每次访问都有开销(涉及包装类的临时转换),而存在栈内存的变量访问更快。虽然微乎其微,但好习惯要从细节养成。
四、进阶:最多删除一个字符的回文判断
这是回文字符串的经典衍生题,也是面试中的常客:
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
比如 'abca',删除 'c' 或 'b' 后可以得到 'aba' 或 'aca',都是回文。
思路分析
双指针从两端向中间移动,遇到不匹配的字符时,有两种删除策略:
- 删除左边的字符,检查
[i+1, j]区间是否是回文 - 删除右边的字符,检查
[i, j-1]区间是否是回文
只要其中一种成立,就返回 true。
代码实现
javascript
function validPalindrome(s) {
const len = s.length;
let i = 0;
let j = len - 1;
while (i < j && s[i] === s[j]) {
i++;
j--;
}
// 删除左边一个,检查 [i+1, j]
if (isPalindrome(i + 1, j)) {
return true;
}
// 删除右边一个,检查 [i, j-1]
if (isPalindrome(i, j - 1)) {
return true;
}
// 辅助函数:检查 s 在 [i, j] 区间是否是回文
function isPalindrome(i, j) {
while (i < j) {
if (s[i] !== s[j]) {
return false;
}
i++;
j--;
}
return true;
}
return false; // 两种删除策略都失败
}
举例理解
以 'aacvdafafdvcaa' 为例:
双指针一直向内移动,直到遇到第一对不相等的字符时停下,此时 i 和 j 指向的就是需要“二选一”删除的位置。然后分别尝试删左或删右,看剩下的部分是否回文。
写在最后
字符串算法看似简单,但要想写得优雅且考虑周全,需要对 JS 的数据类型、原型链、底层包装类等有深入理解。
这四道题从基础到进阶,正好构成了一个完整的学习路径:
- 反转字符串 —— 熟悉数组和字符串的 API 配合
- 包装类机制 —— 理解 JS 底层的设计哲学
- 回文判断 —— 掌握双指针技巧
- 删除一个字符的回文判断 —— 举一反三,灵活运用
希望这篇文章对你有所帮助。如果有什么问题或者想探讨更多细节,欢迎在评论区交流~