1、字符之间插入特殊字符
回文串的中心点有两种,如果长度为奇数,则回文串中心为最中间的那个字符,如 “aba” 的 “b”;如果长度为偶数,则回文串中心为最中间的两个字符的分界,如 “abba” 的 “bb”。为了统一,马拉车算法首先将字符串的每个字符之间(包括首尾两端)插入一个特殊符号,如#,这个符号必须是原字符串中所没有的。
比如我们的原字符串为
s = "google"
那么插入#号之后,变为了
ss = "#g#o#o#g#l#e#"
这样做之后,字符串的长度肯定是奇数,因为插入的#号的个数一定等于字符个数+1,因此总长度是偶数+奇数=奇数。这样,循环时便不用考虑原字符串长度的奇偶性了。
2、计算半径数组 p
接下来,我们需要想办法计算出一个数组 p,这个数组的长度与处理后的字符串 ss 等长,其中 p[i] 表示以 ss[i] 为中心的最长回文子串的半径(不包括 p[i] 本身),暂且把它成为半径数组。如果 p[i] = 0,则说明回文子串就是 ss[i] 本身。
比如 “#a#b#” 的半径数组为 [0, 1, 0, 1, 0]。
为了在搜索回文子串时避免总是判断是否越界,我们在 ss 的首尾两端加上两个不同的特殊字符,保证这两个特殊字符不会出现在 ss 中。比如为 $ 和 ^。则 ss 变为了
ss = "$#g#o#o#g#l#e#^"
数组 p 的最大半径,就是我们要寻找的最长回文子串的半径。因此只要计算出了数组 p,最后答案就呼之欲出了。
如何计算数组 p
马拉车算法在计算数组 p 的整个流程中,一直在更新两个变量:
id
:回文子串的中心位置
mx:回文子串的最后位置
使用这两个变量,便可以用一次扫描来计算出整个数组 p,关键公式为:
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1
① mx > i, 则 p[i] = min( p[2 * id - i] , mx - i ) //j=2 * id - i]是i关于id的对称点
1)当 mx - i > P[j] 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以必有 P[i] = P[j],见下图。
2)当 P[j] >= mx - i 的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] = mx - i。至于mx之后的部分是否对称,就只能老老实实去匹配了。
②对于 mx <= i 的情况,此时镜像对预判位置起不到作用,只能从长度为1开始对比,所以p[i] = 1
。