JavaScript 面试常考的字符串算法:从反转字符串到回文判断

字符串算法是前端面试和算法入门里非常高频的一类题。它看起来简单,但里面经常藏着几个重要能力:熟悉语言 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

这段代码分三步:

  1. split(''):把字符串拆成字符数组,'abc' 变成 ['a', 'b', 'c']
  2. reverse():反转数组,得到 ['c', 'b', 'a']
  3. 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],说明出现了冲突。

由于最多只能删除一个字符,所以冲突出现时只有两种选择:

  1. 删除左边字符,看 left + 1right 是否是回文。
  2. 删除右边字符,看 leftright - 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

如果这个字符串最多删除一个字符后能成为回文,那么当前这对不相等的字符 xy 至少要删掉其中一个。因为如果两个都不删,它们永远无法在回文结构中匹配。

所以冲突点只需要检查:

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

总结

字符串算法看起来只是处理字符,但它背后训练的是非常重要的基础能力。

反转字符串让我们熟悉 splitreversejoin 这类 API;回文判断让我们理解对称结构;双指针让我们学会用更少的空间解决问题;最多删除一个字符的回文题,则进一步训练了"遇到冲突时拆分情况"的算法思维。

在 JavaScript 中学习字符串算法,还能顺带理解包装类、thiscall、对象类型判断这些语言基础。算法和语言不是分开的,真正写代码时,它们会自然地连在一起。

掌握这些题,不只是为了面试刷题,更是为了建立一种处理问题的方式:先看结构,再找规律,最后把规律写成清晰、可验证的代码。

相关推荐
巴勒个啦1 小时前
D3.js 入门实战:用力导向图可视化项目依赖关系
javascript
ITOM运维行者1 小时前
从零搭建企业级服务器监控体系:踩坑实录与架构设计
前端·后端
monologues1 小时前
深入 Vue 3 源码:响应式系统的精妙设计与编译优化
前端
hunterandroid1 小时前
Paging 3 分页:从手动分页到声明式加载
前端
用户4099322502121 小时前
Vue状态管理入门第四章:组合式store和SSR风险
前端·vue.js·后端
Csvn2 小时前
CSS :has() 选择器实战:没有它之前我们写了多少冗余 JS
前端·css
不好听6132 小时前
JavaScript 的 this 到底指向谁?
javascript·面试
梨子同志2 小时前
TypeScript
前端
星栈2 小时前
LiveView 表单真香,但 changeset 也真会坑人:实时校验、错误展示、前后端校验合一
前端·前端框架·elixir