面试常客:字符串算法从入门到进阶

18 阅读4分钟

最近在整理算法笔记时,发现字符串相关的面试题真是常青树。今天就借着我的学习笔记,和大家聊聊字符串算法中的几个经典问题,以及JS中一些容易被忽略的底层细节。


一、反转字符串,你真的会了吗?

先来个开胃菜——反转字符串。别看它简单,里面藏着不少JS的“坑”和设计哲学。

最直接的解法

javascript

const str = 'abc';
const res = str.split('').reverse().join('');
console.log(res); // 'cba'

核心思路就三步:

  1. split('') —— 把字符串拆成字符数组
  2. reverse() —— 反转数组
  3. 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 的数据类型、原型链、底层包装类等有深入理解。

这四道题从基础到进阶,正好构成了一个完整的学习路径:

  1. 反转字符串 —— 熟悉数组和字符串的 API 配合
  2. 包装类机制 —— 理解 JS 底层的设计哲学
  3. 回文判断 —— 掌握双指针技巧
  4. 删除一个字符的回文判断 —— 举一反三,灵活运用

希望这篇文章对你有所帮助。如果有什么问题或者想探讨更多细节,欢迎在评论区交流~