字符串算法入门:从反转字符串到回文判断,面试不再慌

字符串算法入门:从反转字符串到回文判断,面试不再慌

开篇:字符串,程序员最熟悉的"陌生人"

每天写代码,你一定和字符串打过无数次交道------拼接 SQL、解析 JSON、处理用户输入......字符串无处不在。

但你有没有想过:为什么 JavaScript 里数组有 reverse() 方法,而字符串没有? 'abc'.length 明明能取到长度,可 'abc' 不是一个基本数据类型吗,它哪来的 .length

这些问题看似基础,背后却藏着 JS 语言设计的精妙之处。而围绕字符串的算法题------反转字符串、回文判断------更是面试中的"常客"。

今天,我们就从这些基础问题出发,把字符串算法的核心套路彻底搞懂。


一、先备知识:JS 中的字符串到底是什么?

在进入算法之前,我们先搞清楚一个更根本的问题。

1.1 基本类型,哪来的属性和方法?

javascript 复制代码
let str = 'abc';        // 简单数据类型
console.log(str.length); // 3  ← 凭什么?

str 明明是一个简单数据类型(原始类型),存放在栈内存 中。按理说它不应该有属性和方法。那 .length 是怎么来的?

答案藏在 JS 的包装类机制里:

javascript 复制代码
// 当你写 str.length 时,JS 底层偷偷做了这件事:
// 1. 临时将 str 包装成 String 对象:new String('abc')
// 2. 从这个 String 实例上读取 .length 属性
// 3. 用完之后,自动销毁这个临时对象,把 str 恢复为简单数据类型

// 就像灰姑娘的魔法------到时间就自动消失!

📌 知识点 --- 包装类(Wrapper Class) : JS 中有三种基本类型拥有对应的包装类:StringNumberBoolean。 当你尝试在基本类型上调用属性或方法时,JS 引擎会自动装箱(Autoboxing) ------临时创建一个对应的包装对象,调用完后再拆箱回去。 这样做的好处是:既保留了基本类型的高效(存栈内存、不可变),又让开发者可以用面向对象的方式统一操作它们。

javascript 复制代码
let str1 = 'abc';                    // 简单类型,存栈内存
let str2 = new String('abc');        // 对象实例,存堆内存

// 用 Object.prototype.toString 来区分类型
Object.prototype.toString.call(str1); // "[object String]"
Object.prototype.toString.call(str2); // "[object String]"
Object.prototype.toString.call([]);   // "[object Array]"
Object.prototype.toString.call({});   // "[object Object]"

1.2 Object.prototype.toString --- 类型判断的"终极方案"

你可能会问:typeof 不行吗?

javascript 复制代码
typeof 'abc';     // "string"  ✅ 还可以
typeof [1,2,3];   // "object"  ❌ 没法区分数组和普通对象
typeof null;      // "object"  ❌ 历史遗留 bug

Object.prototype.toString.call() 是 JS 中判断变量具体子类型的最可靠方式。它的原理是:

📌 知识点 --- call 方法call 可以让一个函数"借用"给另一个对象执行,并指定函数运行时的 this 指向。 比如 Object.prototype.toString.call([]) 的意思是:把 Object.prototype 上的 toString 方法借给数组用,this 指向这个数组,于是返回 "[object Array]"。 这种"函数借用"的模式在 JS 底层和高级编程中非常常见。

javascript 复制代码
// 函数借用示例
let boss = {
    name: '邰老板',
    say() { console.log(this.name); }
};

let colleague = { name: '甜总' };

boss.say();              // "邰老板"
boss.say.call(colleague); // "甜总"  ← this 被 call 改变了指向!

二、反转字符串:看似简单,实则暗藏玄机

2.1 为什么字符串没有 reverse 方法?

这是个经典的面试闲聊题。答案很简单:

字符串在 JS 中是不可变的(immutable) 。你不能原地修改字符串中的某个字符,任何"修改"操作实际上都是创建了一个新字符串。而 reverse 是一个原地修改的方法,与字符串的不可变性冲突。

javascript 复制代码
let s = 'abc';
s[0] = 'x';         // 无效!严格模式下甚至报错
console.log(s);     // 还是 "abc"

// 数组则不同------它是可变的
let arr = [1, 2, 3];
arr[0] = 99;        // 可以!
console.log(arr);   // [99, 2, 3]

2.2 三种语言对比

语言 字符串 reverse 说明
Python s[::-1] 切片语法,一行搞定
JavaScript str.split('').reverse().join('') 迂回战术
Java new StringBuilder(s).reverse().toString() 借助可变类

JS 没有直接的 reverse,但我们可以"借道"数组:

javascript 复制代码
const str = 'abc';
// 分三步走:拆分 → 反转 → 拼接
const reversed = str.split('').reverse().join('');
console.log(reversed); // "cba"

📌 知识点 --- 三步走拆解

  1. split(''):将字符串按空字符拆成字符数组 → ['a', 'b', 'c']
  2. reverse():数组原地反转 → ['c', 'b', 'a']
  3. join(''):将数组按空字符拼接回字符串 → 'cba'

💡 延伸思考split()join() 是一对"互逆"操作。split 把字符串变数组,join 把数组变字符串。这在各种字符串处理场景中都是一对黄金搭档。


三、回文字符串:对称之美的算法表达

3.1 什么是回文字符串?

回文(Palindrome):正着读和反着读一模一样的字符串。

arduino 复制代码
"yessey"  → 正着读:y-e-s-s-e-y,反着读:y-e-s-s-e-y → ✅ 是回文
"hello"   → 正着读:h-e-l-l-o,反着读:o-l-l-e-h → ❌ 不是回文
"上海自来水来自海上" → 中文也一样!

3.2 解法一:利用反转(API 流)

最直觉的思路:反转后和原串比较。

javascript 复制代码
function isPalindrome(str) {
    const reversedStr = str.split('').reverse().join('');
    return reversedStr === str;
}

一行核心代码,非常简洁。但这种方法遍历了两次字符串 (一次反转,一次比较)且额外创建了数组

能不能更高效?当然------

3.3 解法二:双指针法(对称性思维)

回文字符串的本质是对称------站在中间往两边看,每一位都应该和"镜子里的自己"一模一样。

ini 复制代码
 y  e  s  s  e  y
 ↑              ↑
 i=0          j=5   ← 首位对首位,必须相等

    y  e  s  s  e  y
       ↑     ↑
       i=1  j=4      ← 第二位对第二位,必须相等

利用这个对称性,我们只需要一个循环,用两个指针从两端向中间逼近

javascript 复制代码
function isPalindrome(str) {
    const len = str.length;
    // 只需要遍历到中间位置
    for (let i = 0; i < len / 2; i++) {
        if (str[i] !== str[len - i - 1]) {
            return false; // 只要有一对不相等,立刻判定不是回文
        }
    }
    return true;
}

📌 知识点 --- 双指针(Two Pointers) : 双指针是字符串和数组问题中最常用的技巧之一。核心思想是用两个游标(ij)同时扫描,而不是嵌套循环。

  • 对撞指针 (本文用法):一个从头往后,一个从尾往前,在中间相遇。时间复杂度 O(n) ,空间复杂度 O(1)
  • 快慢指针:两个指针同向移动,一快一慢(常用于链表问题)。

对比反转法:虽然时间复杂度同样是 O(n),但双指针法不需要额外创建数组,且可以在发现不匹配的第一时间提前返回(短路优化),实际效率更高。

解法 时间复杂度 空间复杂度 额外特点
反转 + 比较 O(n) O(n) 代码最简洁
双指针 O(n) O(1) 可提前返回,更高效

四、进阶:最多删除一个字符的回文判断

4.1 问题描述

这是回文字符串的经典衍生问题,面试中出现频率很高:

给定一个非空字符串 s最多删除一个字符。判断是否能成为回文字符串。

arduino 复制代码
"aba"   → ✅ 本身就是回文
"abca"  → ✅ 删除 'c' 得到 "aba",是回文
"abc"   → ❌ 删哪个都不行

4.2 贪心 + 双指针

核心思路:先用双指针从两端向中间走,找到第一对不匹配的字符后,你有两个选择:

  1. 跳过左边的字符:判断 [i+1, j] 范围是否是回文
  2. 跳过右边的字符:判断 [i, j-1] 范围是否是回文

只要其中任意一个成立,就说明可以通过删除一个字符变成回文。

css 复制代码
"a  b  c  a"
 ↑        ↑
 i        j
 a == a ✅ → i++, j--

"a  b  c  a"
    ↑  ↑
    i  j
    b ≠ c ❌ → 第一对不匹配!

尝试 1:跳过左边 b → 判断 [c, a] 是否回文 → ❌
尝试 2:跳过右边 c → 判断 [b, a] 是否回文 → ❌
结论:两个都不行 → ❌ 不是回文
javascript 复制代码
function validPalindrome(s) {
    const len = s.length;
    let i = 0, j = len - 1;

    // 第一步:双指针找到第一对不匹配的位置
    while (i < j && s[i] === s[j]) {
        i++;
        j--;
    }

    // 如果 i >= j,说明本身就是回文
    if (i >= j) return true;

    // 第二步:尝试跳过左边 或 跳过右边
    // 只要有一个成立,就返回 true
    if (isPalindromeRange(i + 1, j)) return true;
    if (isPalindromeRange(i, j - 1)) return true;

    return false;

    // 辅助函数:判断 [st, ed] 范围内的子串是否是回文
    function isPalindromeRange(st, ed) {
        while (st < ed) {
            if (s[st] !== s[ed]) return false;
            st++;
            ed--;
        }
        return true;
    }
}

📌 知识点 --- 贪心思想(Greedy) : 这道题体现了贪心算法的核心------在每个决策点,只考虑当前最优选择。 当我们遇到第一对不匹配的字符时,我们不需要回溯到更早的位置,因为前面的字符都已经匹配过了(最优子结构)。我们只需要尝试两种"跳过"方案------这是一个局部决策,但足以决定全局结果。

时间复杂度 :O(n),每个字符最多被访问两次 空间复杂度:O(1),只用了几个指针变量

4.3 执行过程可视化

"abca" 为例,追踪整个过程:

css 复制代码
初始:i=0, j=3
  s[0]='a', s[3]='a' → 相等 ✅ → i=1, j=2

第一轮后:i=1, j=2
  s[1]='b', s[2]='c' → 不等 ❌

  → 尝试 1:isPalindromeRange(2, 2)  单字符 'c' → true ✅
  → 直接返回 true!

五、延伸:这道题的变体与考察点

面试官往往会在基础回文题上不断加码:

变体 描述 难度
基础回文 判断一个字符串是否回文
最多删一个字符 本文讲解的题目 ⭐⭐
最长回文子串 找出字符串中最长的回文子串 ⭐⭐⭐
回文子串个数 统计所有回文子串的数量 ⭐⭐⭐
分割成回文串 将字符串分割,使每段都是回文 ⭐⭐⭐⭐

你会发现,双指针 + 中心扩展是解决回文类问题最核心的套路。掌握这个思路,大部分回文题都能迎刃而解。


六、核心知识点总结

字符串与 JS 语言特性

要点 说明
不可变性 字符串是不可变的,任何"修改"都创建新串
包装类 str.length 本质是 JS 底层自动装箱 new String(str)
栈 vs 堆 简单类型存栈内存,对象存堆内存
类型判断 Object.prototype.toString.call() 是最可靠的方式
函数借用 call 改变函数运行时的 this 指向

字符串算法核心套路

技巧 适用场景 复杂度
split + reverse + join 反转字符串 O(n) 时间 / O(n) 空间
双指针(对撞) 回文判断、两数之和、反转数组 O(n) 时间 / O(1) 空间
贪心 + 双指针 允许删除/修改的变体回文题 O(n) 时间 / O(1) 空间

JavaScript 字符串常用 API 速查

javascript 复制代码
// 基础操作
str.length          // 长度
str[0]              // 下标访问(只读!)
str.charAt(0)       // 同下标访问

// 查找
str.indexOf('a')    // 子串位置,找不到返回 -1
str.includes('a')   // 是否包含,返回 boolean
str.startsWith('a') // 是否以某串开头
str.endsWith('a')   // 是否以某串结尾

// 截取
str.slice(0, 2)     // 子串 [0, 2),支持负数
str.substring(0, 2) // 子串 [0, 2),不支持负数
str.substr(0, 2)    // ⚠️ 已废弃,从0开始取2个字符

// 转换
str.split('')       // 拆成数组
str.toLowerCase()   // 转小写
str.toUpperCase()   // 转大写
str.trim()          // 去首尾空格
str.replace('a','b')// 替换(只替换第一个匹配)

写在最后

字符串算法看似简单,但细节很多。回看今天的内容,我们实际上覆盖了:

  • 语言层面 :JS 的包装类、值类型 vs 引用类型、栈内存 vs 堆内存、call 函数借用
  • 算法层面:双指针技巧、贪心思想、回文的对称性本质
  • 工程层面:不可变性的设计考量、API 的时间空间取舍

很多开发者觉得"字符串有什么好学的",结果面试碰到回文题就卡壳。其实不在于题有多难,而在于你是否真正理解了字符串的本质特性 (不可变)以及处理字符串的核心技巧(双指针)。

下次当你顺手写下 str.split('').reverse().join('') 时,希望你不再只是"背 api",而是真正理解每一步在做什么、JS 底层又是怎么配合的。

面试不慌,从基础开始 😉


如果这篇文章对你有帮助,欢迎点赞、收藏!有问题也可以在评论区一起讨论~

相关推荐
云技纵横1 小时前
一个 @Async,把 @Transactional 的事务边界打穿了
后端·面试
想要成为糕糕手1 小时前
Harness Engineering:大模型时代的“马鞍”——从记忆层开始,让AI真正为你所用
面试·ai编程·claude
kyriewen16 小时前
我手写了一个 EventEmitter,面试官追问了 6 个问题——第 4 个我没答上来
前端·javascript·面试
先吃饱再说17 小时前
判断回文字符串,从一行代码到双指针优化
算法
她的男孩17 小时前
后台接口加密别只会 HTTPS,ForgeAdmin 的 RSA + SM4/AES 源码拆解
后端·面试·开源
Randyliu18 小时前
20260508-Agent搭建记录以及对ReAct框架的理解
面试·agent
ZzT19 小时前
公司用 AI 筛简历,他写了个 AI 帮你挑公司
面试·aigc·ai编程
黄敬峰19 小时前
深入理解算法核心:从递归思想、数组扁平化到快速排序
算法