字符串算法:回文 / 子串 / KMP 思想
本节目标
- 掌握中心扩展法求最长回文
- 理解 KMP 的"前缀函数"思想(不必背代码)
- 会用手写
strStr/ 实现字符串匹配
最长回文子串(中心扩展法):回文中心可能是字符(奇数长)或两字符之间(偶数长),从每个中心向两侧扩展。
// 运行环境:Node.js 14+
// 保存为 dsa-l9.js,执行:node dsa-l9.js
function longestPalindrome(s) {
let start = 0, maxLen = 1
function expand(l, r) { // 从中心 l,r 向两侧扩
while (l >= 0 && r < s.length && s[l] === s[r]) { l--; r++ }
const len = r - l - 1
if (len > maxLen) { maxLen = len; start = l + 1 }
}
for (let i = 0; i < s.length; i++) {
expand(i, i) // 奇数长中心
expand(i, i + 1) // 偶数长中心
}
return s.slice(start, start + maxLen)
}
console.log(longestPalindrome('babad')) // 'bab' 或 'aba'KMP 思想(前缀函数 / LPS):当模式串 p 在文本 t 中失配时,利用"已匹配部分的前缀=后缀"特性,让 p 的指针不回退到开头,而是跳到"最长公共前后缀"的下一个位置,从而避免重复比较。下面是实现:
// 手写 strStr:在 haystack 中找 needle 首次出现下标(KMP 版 O(n+m))
function strStr(haystack, needle) {
if (needle === '') return 0
const n = needle.length
const lps = new Array(n).fill(0) // longest prefix-suffix
for (let i = 1, len = 0; i < n; i++) {
while (len && needle[i] !== needle[len]) len = lps[len - 1]
if (needle[i] === needle[len]) lps[i] = ++len
}
for (let i = 0, j = 0; i < haystack.length; i++) {
while (j && haystack[i] !== needle[j]) j = lps[j - 1]
if (haystack[i] === needle[j]) j++
if (j === n) return i - n + 1
}
return -1
}
console.log(strStr('hello', 'll')) // 2名词解释
- 回文(Palindrome):正着读和反着读一样的字符串,如
abba、level。中心扩展法从每个可能的"中心"向两边比对。 - 前缀函数 / LPS(Longest Prefix Suffix):对模式串每个位置,记录"该位置前子串的最长、且不等于自身的、相等前后缀长度"。它是 KMP 不回退的关键。
- 子串(Substring):原字符串中连续的一段。和"子序列"(可不连续)不同,子串必须连续。
课后练习
- 中心扩展法求最长回文为什么是 O(n²)?
- 答案:有 2n-1 个中心,每个中心最多扩展 O(n) 次,故 O(n²);但实现简单、常数小,面试常优先写它。
- KMP 相比暴力
indexOf优势在哪?- 答案:暴力失配后模式串指针回退到 0 重比,最坏 O(n·m);KMP 靠 LPS 让模式串指针只前进不倒退,文本串每个字符最多被比一次,O(n+m)。
总结
字符串算法是把"看似朴素的比对"变成"有数学凭据的高效算法"的典范。最长回文的中心扩展法是我最喜欢的入门题:它用"枚举中心 + 向两侧扩"把问题化解得极其直观,虽然复杂度 O(n²),但思路干净、几乎不会写错,面试时往往比硬上 Manacher 更稳妥。KMP 则是另一座山峰——你不必背下它的代码,但必须理解"前缀函数 LPS"这个灵魂:它让模式串在失配时不退回到开头,而是跳到"已匹配部分能复用的最长前后缀"处。这种"记住自己历史"的思想,后来在 BM 算法、甚至一些 DP 里都能见到。字符串题的通用心法:先想清"能不能用哈希指纹/双指针/预处理表减少重复比较",再动手。