字符串算法是前端面试和算法入门里非常高频的一类题。它看起来简单,但里面经常藏着几个重要能力:熟悉语言 API、理解字符串不可变性、会使用双指针、能把问题拆成更小的判断函数。
在 JavaScript 中,字符串相关题目尤其适合练基础。因为 JS 的字符串本身没有 reverse() 方法,但数组有;字符串可以像对象一样访问 .length 和下标;函数又可以通过 call 改变运行时的 this 指向。这些语言特性,刚好能和字符串算法放在一起理解。
这篇文章从三个常见题目出发:反转字符串、判断回文字符串、最多删除一个字符后判断是否能成为回文。
1. 反转字符串
最基础的题目是:给定字符串 abc,输出 cba。
JavaScript 字符串没有直接提供 reverse() 方法:
js
const str = 'abc';
// str.reverse(); // 报错,字符串没有 reverse 方法
但是数组有 reverse(),所以常见写法是:
js
const str = 'abc';
const res = str.split('').reverse().join('');
console.log(res); // cba
这段代码分三步:
split(''):把字符串拆成字符数组,'abc'变成['a', 'b', 'c']。reverse():反转数组,得到['c', 'b', 'a']。join(''):把数组重新拼回字符串,得到'cba'。
这是最容易写出来的 API 解法。它的时间复杂度是 O(n),空间复杂度也是 O(n),因为生成了一个新的数组。
2. 为什么字符串可以使用 .length?
在 JS 中,字符串是基本数据类型:
js
const str = 'abc';
console.log(typeof str); // string
但我们仍然可以写:
js
console.log(str.length); // 3
console.log(str[0]); // a
这是因为 JS 底层会临时把基本类型包装成对应的包装对象,例如 String。也就是说,当我们访问 str.length 时,JS 会临时做类似这样的处理:
js
const temp = new String('abc');
temp.length;
访问结束后,这个临时包装对象会被释放。这个机制让基本类型也能像对象一样使用属性和方法,写起来更统一、更自然。
如果想更准确地判断对象类型,可以使用:
js
Object.prototype.toString.call('abc'); // [object String]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call({}); // [object Object]
这里的 call 可以把 Object.prototype.toString 这个方法"借给"别的值使用,并指定方法执行时的 this。
3. call 和 this 指向
理解 call,可以先看一个简单例子:
js
const o = {
name: '张三',
say: function () {
console.log(this.name);
}
};
const obj = {
name: '李四'
};
o.say(); // 张三
o.say.call(obj); // 李四
call 的第一个参数会指定函数运行时的 this。所以 o.say.call(obj) 并不是把函数复制到 obj 里,而是让 say 在执行时把 this 指向 obj。
这也是为什么下面这种写法可以用来判断类型:
js
Object.prototype.toString.call([1, 2, 3]); // [object Array]
本质上,是让 Object.prototype.toString 在数组这个对象上执行,从而得到更精确的类型标签。
4. 判断一个字符串是否是回文
回文字符串指的是正着读和倒着读都一样的字符串,比如:
text
yessey
level
abba
最直接的思路是:把字符串反转后,判断是否和原字符串相等。
js
function isPalindrome(str) {
const reversedStr = str.split('').reverse().join('');
return reversedStr === str;
}
console.log(isPalindrome('level')); // true
console.log(isPalindrome('hello')); // false
这个写法很直观,但会额外创建数组和反转后的字符串,所以空间复杂度是 O(n)。
5. 更优雅的解法:双指针
回文的本质是对称性。第一个字符要等于最后一个字符,第二个字符要等于倒数第二个字符,以此类推。
所以我们不一定要真的反转字符串,只需要从两边向中间比较:
js
function isPalindrome(str) {
let left = 0;
let right = str.length - 1;
while (left < right) {
if (str[left] !== str[right]) {
return false;
}
left++;
right--;
}
return true;
}
这个解法的时间复杂度依然是 O(n),但空间复杂度变成了 O(1),因为没有创建额外数组。
双指针是字符串题里非常重要的思维方式。只要题目出现"对称""两端""区间""子串判断",都可以先想想双指针能不能解决。
6. 回文字符串的衍生题:最多删除一个字符
题目描述:
给定一个非空字符串 s,最多删除一个字符,判断它是否能成为回文字符串。
例如:
text
aba -> true,本身就是回文
abca -> true,删除 c 后变成 aba
abc -> false,无论删哪个都不能成为回文
这个题比普通回文多了一个条件:允许删除一个字符。
思路仍然从双指针开始。我们从两边向中间比较:
- 如果
str[left] === str[right],继续往中间走。 - 如果
str[left] !== str[right],说明出现了冲突。
由于最多只能删除一个字符,所以冲突出现时只有两种选择:
- 删除左边字符,看
left + 1到right是否是回文。 - 删除右边字符,看
left到right - 1是否是回文。
只要其中一种成立,整个字符串就满足条件。
代码如下:
js
function validPalindrome(str) {
function isPalindrome(left, right) {
while (left < right) {
if (str[left] !== str[right]) {
return false;
}
left++;
right--;
}
return true;
}
let left = 0;
let right = str.length - 1;
while (left < right) {
if (str[left] !== str[right]) {
return isPalindrome(left + 1, right) || isPalindrome(left, right - 1);
}
left++;
right--;
}
return true;
}
这个实现里有一个很关键的设计:内部函数 isPalindrome(left, right) 不是重新判断整个字符串,而是判断一个指定区间是不是回文。
这样当我们遇到不匹配字符时,就能快速验证"删左边"或"删右边"是否可行。
7. 为什么只需要分两种情况?
假设字符串两端比较时出现了不相等:
text
... x ...... y ...
^ ^
left right
如果这个字符串最多删除一个字符后能成为回文,那么当前这对不相等的字符 x 和 y 至少要删掉其中一个。因为如果两个都不删,它们永远无法在回文结构中匹配。
所以冲突点只需要检查:
js
isPalindrome(left + 1, right)
或者:
js
isPalindrome(left, right - 1)
不需要尝试删除其他位置的字符。这就是这个题能保持 O(n) 时间复杂度的原因。
8. 复杂度分析
对于普通回文判断:
js
function isPalindrome(str) {
let left = 0;
let right = str.length - 1;
while (left < right) {
if (str[left] !== str[right]) return false;
left++;
right--;
}
return true;
}
时间复杂度是 O(n),空间复杂度是 O(1)。
对于最多删除一个字符的版本:
js
function validPalindrome(str) {
// ...
}
虽然冲突时会调用一次或两次 isPalindrome,但每个指针整体仍然只会在线性范围内移动,所以时间复杂度还是 O(n),空间复杂度是 O(1)。
9. 写字符串算法时的几个习惯
写字符串算法时,可以养成几个固定习惯。
第一,先确认是否需要修改原字符串。JS 字符串是不可变的,所以很多操作都会产生新字符串。
第二,能用双指针就优先考虑双指针。它通常比反转、切片、复制数组更省空间。
第三,把重复逻辑封装成小函数。比如 validPalindrome 里的 isPalindrome(left, right),既让代码更清楚,也方便复用区间判断逻辑。
第四,注意边界情况。例如空字符串、单字符字符串、两个字符字符串、已经是回文的字符串、只差一个字符的字符串。
可以用这些测试用例检查:
js
console.log(validPalindrome('aba')); // true
console.log(validPalindrome('abca')); // true
console.log(validPalindrome('abc')); // false
console.log(validPalindrome('deeee')); // true
总结
字符串算法看起来只是处理字符,但它背后训练的是非常重要的基础能力。
反转字符串让我们熟悉 split、reverse、join 这类 API;回文判断让我们理解对称结构;双指针让我们学会用更少的空间解决问题;最多删除一个字符的回文题,则进一步训练了"遇到冲突时拆分情况"的算法思维。
在 JavaScript 中学习字符串算法,还能顺带理解包装类、this、call、对象类型判断这些语言基础。算法和语言不是分开的,真正写代码时,它们会自然地连在一起。
掌握这些题,不只是为了面试刷题,更是为了建立一种处理问题的方式:先看结构,再找规律,最后把规律写成清晰、可验证的代码。