字符串匹配算法
文章目录
- 字符串匹配算法
- 一:KMP算法
- 1:算法概述
- 2:部分匹配表
- 3:算法实现
- 二:Moore算法
- 1:算法概述
- 2:代码实现
- 3:完整实现
- 三:马拉车算法
- 1:算法概述
- 2:Manacher算法原理与实现:
- 2.1:Len数组简介与性质
- 2.2:Len数组的计算
- 3:算法实现
一:KMP算法
1:算法概述
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
因为B与A不匹配,搜索词再往后移。
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
接着比较字符串和搜索词的下一个字符,还是相同。
直到字符串有一个字符,与搜索词对应的字符不相同为止。
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。
KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。
怎么做到这一点呢?可以针对搜索词,算出一张部分匹配表。这张表是如何产生的,后面再介绍,这里只要会用就可以了。
已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(“AB”),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。
因为空格与A不匹配,继续后移一位。
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。
2:部分匹配表
核心 -> 找前缀和后缀的公用元素长度
--------------- 以上面的ABCDABD为例 -----------------
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
3:算法实现
package com.cui;
/**
* @author cui haida
* 2025/1/20
* desc -> KMP算法
*/
public class KmpDemo {
public static void main(String[] args) {
String s1 = "BBC ABCDAB ABCDABCDABDE";
String s2 = "ABCDABD";
System.out.println(kmpSearch(s1, s2, kmpNext(s2)));
}
/**
* K字符串匹配
* 使用KMP算法在主字符串s1中查找子字符串s2的第一次出现的位置
* @param s1 主字符串
* @param s2 子字符串
* @param next 子字符串s2的next数组,用于指示在不匹配情况下应该从子字符串的哪个位置继续比较
* @return 如果找到子字符串,返回子字符串在主字符串中第一次出现的位置;如果未找到,返回-1
*/
public static int kmpSearch(String s1, String s2, int[] next) {
// 遍历主字符串s1,从索引1开始,索引0不参与比较
for (int i = 1, j = 0; i < s1.length(); i++) {
// 当前字符不匹配时,根据next数组调整子字符串的索引j
while (j > 0 && s1.charAt(i) != s2.charAt(j)) {
j = next[j - 1];
}
// 当前字符匹配,子字符串索引j前进
if (s1.charAt(i) == s2.charAt(j)) {
j++;
}
// 子字符串完全匹配,返回匹配的起始位置
if (j == s2.length()) {
return i - j + 1;
}
}
// 未找到匹配的子字符串,返回-1
return -1;
}
/**
* 得到一个字符串的部分匹配值
*
* 该方法用于计算KMP算法中的next数组,next数组是KMP算法的关键,
* 它用于指示当当前字符不匹配时,应该从模式串的哪个位置继续比较。
*
* @param s 字符串
* @return 部分匹配值
*/
public static int[] kmpNext(String s) {
// 初始化next数组,长度与输入字符串相同
int[] next = new int[s.length()];
// 字符串的第一个字符的部分匹配值为0
next[0] = 0;
// 遍历字符串,计算每个字符的部分匹配值
for (int i = 1, j = 0; i < s.length(); i++) {
// 当前字符与j位置的字符不匹配时,更新j值
while (j > 0 && s.charAt(i) != s.charAt(j)) {
j = next[j - 1];
}
// 当前字符与j位置的字符匹配时,增加j值
if (s.charAt(i) == s.charAt(j)) {
j++;
}
// 记录当前字符的部分匹配值
next[i] = j;
}
// 返回计算得到的next数组
return next;
}
}
二:Moore算法
1:算法概述
ctrl-F
就是基于这种算法
根据Moore
教授自己的例子来解释这种算法
- 假定字符串为"HERE IS A SIMPLE EXAMPLE",搜索词为"EXAMPLE"。
-
首先,"字符串"与"搜索词"头部对齐,从尾部开始比较。
这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符(整体上)肯定不是要找的结果。
我们看到,"S"与"E"不匹配。这时,“S"就被称为"坏字符”(bad character),即不匹配的字符。
我们还发现,"S"不包含在搜索词"EXAMPLE"之中,这意味着可以把搜索词直接移到"S"的后一位。
-
依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。
但是,"P"包含在搜索词"EXAMPLE"之中。所以,将搜索词后移两位,两个"P"对齐。
因此:后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置
如果"坏字符"不包含在搜索词之中,则上一次出现位置为 -1
- 依然从尾部开始比较,发现"E"与"E"匹配。
- 比较前面一位,"LE"与"LE"匹配。
- 比较前面一位,"PLE"与"PLE"匹配。
-
比较前面一位,"MPLE"与"MPLE"匹配。
我们把这种情况称为"好后缀"(
good suffix
),即所有尾部匹配的字符串。注意,“MPLE”、“PLE”、“LE”、"E"都是好后缀。后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置
"好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"EF"是好后缀,则它的位置以"F"为准,即5(从0开始计算)。 如果"好后缀"在搜索词中只出现一次,则它的上一次出现位置为 -1。比如,"EF"在"ABCDEF"之中只出现一次,则它的上一次出现位置为-1(即未出现)。 如果"好后缀"有多个,则除了最长的那个"好后缀",其他"好后缀"的上一次出现位置必须在头部。
- 比较前一位,发现"I"与"A"不匹配。所以,“I"是"坏字符”
- 那么这时候就要权衡下好后缀和坏字符两个规则,用哪个了-> 当然是用后移多的了
- 坏字符规则"只能移3位
- “好后缀规则"可以移6位[所有的"好后缀”(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出现在头部,所以后移 6 - 0 = 6位]
- 继续从尾部开始比较,“P"与"E"不匹配,因此"P"是"坏字符”。根据"坏字符规则",后移 6 - 4 = 2位。
- 从尾部开始逐位比较,发现全部匹配,于是搜索结束。
更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关
可以预先计算生成坏字符规则表和好后缀规则表。使用时,只要查表比较一下就可以了。
2:代码实现
在上面,我们说到了,在BM算法中有两个关键概念–坏字符和好后缀,所以我们的代码实现将分为三个步骤。
- 利用坏字符算法,计算匹配串可以滑动的距离
- 利用好后缀算法,计算匹配串可以滑动的距离
- 结合坏字符算法和好后缀算法,实现BM算法,查看匹配串在主串中存在的位置
step1: 坏字符算法,经过之前的分析,我们找到坏字符之后,需要查找匹配串中是否出现过坏字符,如果出现多个,我们滑动匹配串,将靠后的坏字符与主串坏字符对齐。如果不存在,则完全匹配。如果我们每次找到坏字符都去查找一次匹配串中是否出现过,效率不高,所以我们可以用一个hash表保存匹配串中出现的字符以及最后出现的位置,提高查找效率。
我们设定的只有小写字母,可以直接利用一个26大小的数组存储,数组下标存储出现的字符(字符-‘a’),数组值存储出现的位置。
int[] modelStrIndex;
private void badCharInit(char[] modelStr) {
modelStrIndex = new int[26];
//-1表示该字符在匹配串中没有出现过
for (int i = 0 ; i < 26 ; i ++) {
modelStrIndex[i] = -1;
}
for (int i = 0 ; i < modelStr.length ; i++) {
//直接依次存入,出现相同的直接覆盖,
//保证保存的时候靠后出现的位置
modelStrIndex[modelStr[i] - 'a'] = i;
}
}
查找坏字符出现位置badCharIndex,未出现,匹配成功,直接返回0。
查找匹配串中出现的坏字符位置modelStrIndex,未出现,滑动到坏字符位置之后,直接返回匹配串的长度。
返回badCharIndex - modelStrIndex。
注:坏字符是指与匹配串字符不匹配的主串字符,是看的主串,但是我们计算的位置,是匹配串中的位置。
/**
* @param mainStr 主串
* @param modelStr 模式串
* @param start 模式串在主串中的起始位置
* @return 模式串可滑动距离,如果为0则匹配上
*/
private int badChar(char[] mainStr, char[] modelStr, int start) {
//坏字符位置
int badCharIndex = -1;
char badChar = '\0';
//开始从匹配串后往前进行匹配
for (int i = modelStr.length - 1 ; i >= 0 ; i --) {
int mainStrIndex = start + i;
//第一个出现不匹配的即为坏字符
if (mainStr[mainStrIndex] != modelStr[i]) {
badCharIndex = i;
badChar = mainStr[mainStrIndex];
break;
}
}
if (-1 == badCharIndex) {
//不存在坏字符,需匹配成功,要移动距离为0
return 0;
}
//查看坏字符在匹配串中出现的位置
if (modelStrIndex[badChar - 'a'] > -1) {
//出现过
return badCharIndex - modelStrIndex[badChar - 'a'];
}
return modelStr.length;
}
**step2:**好后缀算法,经过之前的分析,我们在实现好后缀算法的时候,有一个后缀前缀匹配的过程,这里我们仍然可以事先进行处理。将匹配串一分为二,分别匹配前缀和后缀字串。
ps:开始我的处理是两个数组,将前缀后缀存下来,需要的时候进行匹配,但是在写文章的时候,我突然回过神来,我已经处理了一遍了,为什么不直接标记是否匹配呢?
初始化匹配串前缀后缀是否匹配数组,标志当前长度的前缀后缀是否匹配。
//对应位置的前缀后缀是否匹配
boolean[] isMatch;
public void goodSuffixInit(char[] modelStr) {
isMatch = new boolean[modelStr.length / 2];
StringBuilder prefixStr = new StringBuilder();
List<Character> suffixChar = new ArrayList<>(modelStr.length / 2);
for (int i = 0 ; i < modelStr.length / 2 ; i ++) {
prefixStr.append(modelStr[i]);
suffixChar.add(0, modelStr[modelStr.length - i - 1]);
isMatch[i] = this.madeSuffix(suffixChar).equals(prefixStr.toString());
}
}
/**
* 组装后缀数据
* @param characters
* @return
*/
private String madeSuffix(List<Character> characters) {
StringBuilder sb = new StringBuilder();
for (Character ch : characters) {
sb.append(ch);
}
return sb.toString();
}
step3: 结合坏字符和好后缀算法实现BM算法,起始就是每一次匹配,同时调用坏字符和好后缀算法,如果返回移动距离为0,表示已经匹配成功,直接返回当前匹配的起始距离。其余情况下,滑动坏字符和好后缀算法返回的较大值。如果主串匹配完还没有匹配成功,则返回-1。
注:加了一些日志打印匹配过程
public int bmStrMatch(char[] mainStr, char[] modelStr) {
//初始化坏字符和好后缀需要的数据
this.badCharInit(modelStr);
this.goodSuffixInit(modelStr);
int start = 0;
while (start + modelStr.length <= mainStr.length) {
//坏字符计算的需要滑动的距离
int badDistance = this.badChar(mainStr, modelStr, start);
//好后缀计算的需要滑动的距离
int goodSuffixDistance = this.goodSuffix(mainStr, modelStr, start);
System.out.println("badDistance = " +badDistance + ", goodSuffixDistance = " + goodSuffixDistance);
//任意一个匹配成功即成功(可以计算了坏字符和好后缀之后分别判断一下)
//减少一次操作
if (0 == badDistance || 0 == goodSuffixDistance) {
System.out.println("匹配到的位置 :" + start);
return start;
}
start += Math.max(badDistance, goodSuffixDistance);
System.out.println("滑动至:" + start);
}
return -1;
}
public static void main(String[] args) {
BoyerMoore moore = new BoyerMoore();
char[] mainStr = new char[]{'a','b', 'c', 'a', 'g', 'f', 'a', 'c', 'j', 'k', 'a', 'c', 'k', 'e', 'a', 'c'};
char[] modelStr = new char[]{'a', 'c', 'k', 'e', 'a', 'c'};
System.out.println(moore.bmStrMatch(mainStr, modelStr));
}
3:完整实现
package com.cui.mystring;
import java.util.HashMap;
import java.util.Map;
/**
* @author cui haida
* 2025/1/20
*/
public class BoyerMoore {
public static void main(String[] args) {
String text = "here is a simple example";
String pattern = "example";
BoyerMoore bm = new BoyerMoore();
bm.boyerMoore(pattern, text);
}
/**
* 使用Boyer-Moore算法在文本中查找指定模式的出现位置
* Boyer-Moore算法利用比较多次的字符跳跃来提高查找效率,特别适用于长文本的查找
*
* @param pattern 要查找的模式字符串
* @param text 待查找的文本字符串
*/
public void boyerMoore(String pattern, String text) {
// 模式字符串的长度
int m = pattern.length();
// 文本字符串的长度
int n = text.length();
// bmBc表:用于存储每个字符在模式中的最后位置,以实现快速跳跃
Map<String, Integer> bmBc = new HashMap<String, Integer>();
// bmGs表:用于存储跳跃值,根据匹配情况决定跳跃距离
int[] bmGs = new int[m];
// 预处理阶段:构建bmBc和bmGs表
preBmBc(pattern, m, bmBc);
preBmGs(pattern, m, bmGs);
// 搜索阶段:使用预处理好的表进行模式匹配
int j = 0;
int i = 0;
// 计数器,用于统计字符比较次数
int count = 0;
// 遍历文本,直到没有足够的字符进行比较
while (j <= n - m) {
// 从模式的末尾开始向前匹配,直到找到不匹配的字符或匹配完整个模式
for (i = m - 1; i >= 0 && pattern.charAt(i) == text.charAt(i + j); i--) { // 用于计数
count++;
}
// 如果整个模式都匹配成功,则打印匹配位置并根据bmGs表移动到下一个位置
if (i < 0) {
System.out.println("one position is:" + j);
j += bmGs[0];
} else {
// 如果模式匹配失败,根据bmGs表和bmBc表计算最大跳跃距离
j += Math.max(bmGs[i], getBmBc(String.valueOf(text.charAt(i + j)), bmBc, m) - m + 1 + i);
}
}
// 打印字符比较次数
System.out.println("count:" + count);
}
/**
* 预处理构建坏字符表
* 该方法用于在执行Boyer-Moore算法前,根据模式串预计算每个字符在模式串中最后出现的位置
* 这有助于在匹配过程中快速确定当字符不匹配时,模式串应该移动的位置
*
* @param pattern 模式串,即我们需要在文本中查找的字符串
* @param patLength 模式串的长度,用于计算字符位置
* @param bmBc 一个映射表,存储每个字符最后出现的位置,用于快速查找
*/
private void preBmBc(String pattern, int patLength, Map<String, Integer> bmBc) {
System.out.println("bmbc start process...");
// 从模式串的倒数第二个字符开始向前遍历,最后一个字符不需要处理,因为它在匹配失败时不会导致模式串移动
for (int i = patLength - 2; i >= 0; i--) {
// 检查当前字符是否已经存在于映射表中
// 如果当前字符不在映射表中,则将它添加到映射表中,并设置其值为从当前位置到模式串末尾的距离
// 这个距离用于在匹配失败时确定模式串应该移动的位数
if (!bmBc.containsKey(String.valueOf(pattern.charAt(i)))) {
bmBc.put(String.valueOf(pattern.charAt(i)), (Integer) (patLength - i - 1));
}
}
}
/**
* Boyer-Moore算法的预处理函数,用于构建bmGs(Good Suffix)表
* 此函数计算在模式匹配中,当出现字符不匹配时,模式串应该跳过的位数
* 通过分析模式串的后缀和前缀,来优化匹配过程,减少不必要的比较
*
* @param pattern 模式串,即需要在文本中查找的字符串
* @param patLength 模式串的长度
* @param bmGs 存储每个位置上好后缀的长度,以便在不匹配时确定移动的位数
*/
private void preBmGs(String pattern, int patLength, int[] bmGs) {
int i, j;
int[] suffix = new int[patLength];
suffix(pattern, patLength, suffix);
// 模式串中没有子串匹配上好后缀,也找不到一个最大前缀
for (i = 0; i < patLength; i++) {
bmGs[i] = patLength;
}
// 模式串中没有子串匹配上好后缀,但找到一个最大前缀
j = 0;
for (i = patLength - 1; i >= 0; i--) {
if (suffix[i] == i + 1) {
for (; j < patLength - 1 - i; j++) {
if (bmGs[j] == patLength) {
bmGs[j] = patLength - 1 - i;
}
}
}
}
// 模式串中有子串匹配上好后缀
for (i = 0; i < patLength - 1; i++) {
bmGs[patLength - 1 - suffix[i]] = patLength - 1 - i;
}
// 打印bmGs表,便于调试和理解
System.out.print("bmGs:");
for (i = 0; i < patLength; i++) {
System.out.print(bmGs[i] + ",");
}
System.out.println();
}
/**
* 计算模式字符串的后缀长度数组
* 该方法用于填充suffix数组,suffix[i]表示从模式字符串的第i个字符开始到末尾的子串,与模式字符串的末尾子串的最长公共部分的长度
*
* @param pattern 模式字符串,用于进行匹配
* @param patLength 模式字符串的长度
* @param suffix 存储后缀长度的数组,由方法填充
*/
private void suffix(String pattern, int patLength, int[] suffix) {
// 最后一个字符的后缀长度是模式字符串的长度
suffix[patLength - 1] = patLength;
// 初始化变量q,用于后续的比较
int q = 0;
// 从倒数第二个字符开始,向前遍历模式字符串
for (int i = patLength - 2; i >= 0; i--) {
// 初始化q为当前字符的索引
q = i;
// 在模式字符串中向前比较字符,直到比较失败或者q回到起始位置前
while (q >= 0 && pattern.charAt(q) == pattern.charAt(patLength - 1 - i + q)) {
q--;
}
// 计算当前位置的后缀长度并存储到suffix数组中
suffix[i] = i - q;
}
}
/**
* 根据字符获取BM算法的坏字符表中的位移量
* 如果字符在坏字符表中,则返回该字符对应的位移量;
* 如果字符不在坏字符表中,则返回模式串的长度作为位移量
*
* @param c 单个字符,用于在坏字符表中查找
* @param bmBc 坏字符表,映射字符到其在模式串中的位移量
* @param m 模式串的长度,当字符不在坏字符表中时使用
* @return 字符在坏字符表中对应的位移量或模式串的长度
*/
private int getBmBc(String c, Map<String, Integer> bmBc, int m) {
// 如果在规则中则返回相应的值,否则返回pattern的长度
return bmBc.getOrDefault(c, m);
}
}
三:马拉车算法
1:算法概述
Manacher算法:也叫 “马拉车”算法。
Manacher算法的应用范围要狭窄得多,但是它的思想和拓展kmp算法有很多共通支出,所以在这里介绍一下。Manacher算法是查找一个字符串的最长回文子串的线性算法。
什么是回文串:
所谓回文串,简单来说就是正着读和反着读都是一样的字符串,比如abba,noon等等,一个字符串的最长回文子串即为这个字符串的子串中,是回文串的最长的那个。
计算回文串的几种方式:其它几种方法请点击此链接
计算字符串的最长回文字串最简单的算法就是枚举该字符串的每一个子串,并且判断这个子串是否为回文串,这个算法的时间复杂度为O(n^3)的,显然无法令人满意,稍微优化的一个算法是枚举回文串的中点,这里要分为两种情况,一种是回文串长度是奇数的情况,另一种是回文串长度是偶数的情况,枚举中点再判断是否是回文串,这样能把算法的时间复杂度降为O(n2),但是当n比较大的时候仍然无法令人满意,Manacher算法可以在线性时间复杂度内求出一个字符串的最长回文字串,达到了理论上的下界。
2:Manacher算法原理与实现:
首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。下面举一个例子
2.1:Len数组简介与性质
Manacher算法用一个辅助数组Len[i]表示以字符T[i]为中心的最长回文字串的最右字符到T[i]的长度,比如以T[i]为中心的最长回文字串是T[l,r],那么Len[i]=r-i+1。
对于上面的例子,可以得出Len[i]数组为:
Len数组有一个性质,那就是Len[i]-1就是该回文子串在原字符串S中的长度,至于证明,首先在转换得到的字符串T中,所有的回文字串的长度都为奇数,那么对于以T[i]为中心的最长回文字串,其长度就为2*Len[i]-1,经过观察可知,T中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有Len[i]个分隔符,剩下Len[i]-1个字符来自原字符串,所以该回文串在原字符串中的长度就为Len[i]-1。
有了这个性质,那么原问题就转化为求所有的Len[i]。下面介绍如何在线性时间复杂度内求出所有的Len。
2.2:Len数组的计算
首先从左往右依次计算Len[i],当计算Len[i]时,Lenj已经计算完毕。设P为之前计算中最长回文子串的右端点的最大值,并且设取得这个最大值的位置为po,分两种情况:
第一种情况:i<=P
那么找到i相对于po的对称位置,设为j,
那么如果Len[j],如下图:
那么说明以j为中心的回文串一定在以po为中心的回文串的内部,且j和i关于位置po对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样,即Len[i]>=Len[j]。因为Len[j]如果Len[j]>=P-i 由对称性,说明以i为中心的回文串可能会延伸到P之外,而大于P的部分我们还没有进行匹配,所以要从P+1位置开始一个一个进行匹配,直到发生失配,从而更新P和对应的po以及Len[i]。
第二种情况: i>P
如果i比P还要大,说明对于中点为i的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的po以及Len[i]。
3:算法实现
package com.cui.mystring;
public class ManacherDemo {
public static void main(String[] args) {
// 示例调用
String result = manacher("abcbabcd");
System.out.println("最长回文子串: " + result);
}
/**
* 使用Manacher算法找到字符串中的最长回文子串
* Manacher算法通过在每个字符间插入特殊字符(例如#),将问题转化为每个字符(包括插入的特殊字符)为中心的回文子串问题
* 这种转换确保了回文子串的长度总是奇数,简化了算法的实现
*
* @param str 输入字符串
* @return 最长回文子串
*/
public static String manacher(String str) {
if (str == null || str.length() == 0) {
return "";
}
// 预处理字符串,使其适合Manacher算法的要求
StringBuilder sb = new StringBuilder();
sb.append("#");
for (char c : str.toCharArray()) {
sb.append(c).append("#");
}
String processedStr = sb.toString();
int n = processedStr.length();
int[] next = new int[n];
int C = 0; // 中心位置
int R = 0; // 回文右边界的最远位置
for (int i = 0; i < n; i++) {
int mirror = 2 * C - i; // 对称位置
if (i < R) {
next[i] = Math.min(R - i, next[mirror]);
}
// 尝试扩展回文半径
while (i + next[i] < n && i - next[i] >= 0 && processedStr.charAt(i + next[i]) == processedStr.charAt(i - next[i])) {
next[i]++;
}
// 更新中心位置和最远右边界
if (i + next[i] > R) {
C = i;
R = i + next[i];
}
}
// 找到最大回文半径及其对应的中心位置
int maxLen = 0;
int centerIndex = 0;
for (int i = 0; i < n; i++) {
if (next[i] > maxLen) {
maxLen = next[i];
centerIndex = i;
}
}
// 计算原字符串中的最长回文子串
int start = (centerIndex - maxLen) / 2;
return str.substring(start, start + maxLen - 1);
}
}