一、字符串算法的两个底层直觉
搞懂字符串题,先要建立两个最朴素的认知:不可变性 和 ASCII 映射。理解了它们,绝大多数题目都能套上模板。
1. 字符串的「不可变性」(Immutability)
在 JavaScript 中,字符串是按值传递且不可变的。你无法像操作数组那样直接通过 s[0] = 'a' 来修改某一位的字符。
破局点 :当遇到需要 修改、翻转、替换 字符串中特定位置字符的题目时,第一反应永远是------先转成数组处理,最后再拼回来。
套路代码:
jsx
let arr = s.split('')
// ...在数组上做各种操作...
return arr.join('')
2. 字符的「ASCII 映射」(CharCode)
很多字符串题目限定了「只包含小写字母」,这意味着字符的种类被严格限制在了 26 个。
破局点 :不要总是依赖
Map或Object。利用字符的 ASCII 码差值,构建一个长度为 26 的数组作为「计分板」,是空间复杂度最优(真正的O(1))的解法。
套路代码:
jsx
const index = char.charCodeAt(0) - 97 // 97 是 'a' 的 ASCII 码
二、核心题型与通用模板
结合已通关的题目,字符串考察主要分为两大阵营。
阵营 A:字母异位词与词频统计(哈希 / 数组计分板)
这类题目的本质是:忽略字符的顺序,只关注字符出现的种类和频率。
通用模板:26 位定长数组计分板
如果题目只有小写字母,直接初始化一个长度为 26、全为 0 的数组。遍历第一个字符串时执行 +1,遍历第二个字符串时执行 -1。最后检查数组是否全为 0。
jsx
const count = new Array(26).fill(0)
for (const ch of s) count[ch.charCodeAt(0) - 97]++
for (const ch of t) count[ch.charCodeAt(0) - 97]--
return count.every(n => n === 0)
对应练习题(按难度递增)
- LeetCode 242. 有效的字母异位词:最基础的计分板应用,跑通模板即可。
- LeetCode 383. 赎金信:变种题,检查计分板中是否有任何一项小于 0。
- LeetCode 49. 字母异位词分组 :进阶应用,需要将计分板转换回字符串(或使用排序后的字符串)作为
Map的 Key,把具有相同特征的字符串分到同一个数组里。 - LeetCode 438. 找到字符串中所有字母异位词:综合应用题。将「计分板」与「定长滑动窗口」结合,在父串上滑动,每次只维护进出窗口的两个字符的状态。
阵营 B:回文串与反转问题(双指针)
这类题目的本质是:关注字符的对称性和顺序颠倒。
通用模板:相向双指针
定义 left = 0 和 right = s.length - 1,两个指针不断向中间靠拢,在遍历过程中进行对比或交换操作。
jsx
let left = 0, right = s.length - 1
while (left < right) {
// 对比 or 交换 s[left] 与 s[right]
left++
right--
}
对应练习题
- LeetCode 344. 反转字符串 :最纯粹的双指针交换。考察 JS 数组中
[arr[i], arr[j]] = [arr[j], arr[i]]的解构交换语法。 - LeetCode 125. 验证回文串:大厂高频题。考察正则表达式去除非字母数字字符,配合双指针忽略大小写进行对比。
三、前端必备 String API 备忘录
在刷题和面试时,以下 API 是必须形成肌肉记忆的:
| API 方法 | 核心用途 | 算法常见场景 |
|---|---|---|
split('') |
字符串转数组 | 涉及元素交换、反转时必须先执行此步 |
join('') |
数组转字符串 | 输出最终结果 |
substring(start, end) |
截取子串 | 滑动窗口提取特定片段 |
charCodeAt(index) |
获取字符 ASCII 码 | 计算索引差值,构建定长数组「桶」 |
toLowerCase() |
统一转小写 | 忽略大小写的回文 / 异位词比较 |
match(/[a-z0-9]/ig) |
正则提取有效字符 | 过滤标点符号和空格(如 LeetCode 125) |
写在最后
字符串题看似花样繁多,但拆开来看无非两类:频率问题 用计分板,顺序 / 对称问题 用双指针。再叠加一个「不可变 → 转数组」的预处理直觉,几乎可以覆盖大部分常见考点