字符串算法:回文 / 子串 / KMP 思想

本节目标

最长回文子串(中心扩展法):回文中心可能是字符(奇数长)或两字符之间(偶数长),从每个中心向两侧扩展。

// 运行环境: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

名词解释

课后练习

  1. 中心扩展法求最长回文为什么是 O(n²)?
    • 答案:有 2n-1 个中心,每个中心最多扩展 O(n) 次,故 O(n²);但实现简单、常数小,面试常优先写它。
  2. KMP 相比暴力 indexOf 优势在哪?
    • 答案:暴力失配后模式串指针回退到 0 重比,最坏 O(n·m);KMP 靠 LPS 让模式串指针只前进不倒退,文本串每个字符最多被比一次,O(n+m)。

总结

字符串算法是把"看似朴素的比对"变成"有数学凭据的高效算法"的典范。最长回文的中心扩展法是我最喜欢的入门题:它用"枚举中心 + 向两侧扩"把问题化解得极其直观,虽然复杂度 O(n²),但思路干净、几乎不会写错,面试时往往比硬上 Manacher 更稳妥。KMP 则是另一座山峰——你不必背下它的代码,但必须理解"前缀函数 LPS"这个灵魂:它让模式串在失配时不退回到开头,而是跳到"已匹配部分能复用的最长前后缀"处。这种"记住自己历史"的思想,后来在 BM 算法、甚至一些 DP 里都能见到。字符串题的通用心法:先想清"能不能用哈希指纹/双指针/预处理表减少重复比较",再动手。