KMP和Manache
1.KMP算法
KMP算法是一种改进的字符串匹配算法,其关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的
1.1 实例
问题 :给一个字符串 str1 和一个字符串 str2 ,要在 str1 中找到 str2 第一次出现的位置 ,要求 :时间复杂度 O(n)。
例 :str1 :abcabcdefg,str2 :abcd,那么我们就返回3(数组从0索引开始)
(1)分析:
题目要求的时间复杂度为 O(n),所以利用传统方法去匹配字符串肯定是不行的,我们就需要用到KMP算法。要使用KMP算法,我们就需要先了解一下最大相等的前缀和后缀,如下 图(1) 所示 :str2的最大相等前缀和后缀为 abc。我们现在需要定义一个数组,这个数组来存储 str2 从0位置开始到n-1位置,每个位置之前的子字符串最大相等前缀和后缀的字符数量信息,如 图(2) 所示。我们需要使用KMP算法来解决问题,如下 图(3) 所示 :str2 从 0 位置开始直到倒数第二个字符都与 str1 从 i 位置开始到 y 之前的位置都相等,y与str2的最后一个位置不等,现在我们应该令x跳到str2的最大前缀的后一个字符,也就是 s ,用 s 去和 y 比较。当str2当前字符前边最大相等前缀和后缀为0时,当前字符跳到0位置,与y再去比较,不相等时,然后从0位置开始str2从y的下一个位置开始再次匹配。
那么我们如何得到图(2)中维护最大相等前缀和后缀信息的数组呢?
首先我们人为规定 :数组0位置信息为-1,1位置保存信息为0。后续的信息如何确定?我们现在假设要求 i 位置的信息,那么我们可以根据 i-1 位置的信息来确定。我们根据 i-1 位置的信息可以找到他的最大前缀下一个位置的元素,将其与 i 位置的元素比较,如果相等的话 i 位置的信息就为 i-1 位置的信息+1。如果不相等,就跳到 i-1 位置的信息可以找到他的最大前缀下一个位置的元素,然后根据他的信息再次做判断,以此类推。
我们举个例子,如 图(4) 所示 :当 i-1 位置的字符等于next[i-1] 位置的字符时,那么 next[i] = next[i-1] + 1(也就是8+1=9) ;如果不相等, i-1 就跳到 next[i-1] 位置的字符也就是 e 这个位置开始比较。如果 i-1 位置的字符等于 next[e] 位置的字符时,那么 next[i] = next[e] + 1(也就是3+1=4) ;如果不相等, 就再从 e 跳到 next[e] 位置的字符也就是 s 这个位置开始比较。以此一直类推,直到第一个字符,如果第一个字符与 i 位置的字符相等,next[i]就等于1,否则等于0。
(2)代码
public class FindString {
public static void main(String[] args) {
String s="abcabcde";
String m="abcd";
int index=getIndexOf(s,m);
System.out.println("m在s中第一次出现的位置是 : "+index);
}
//str1.length>=str2.length
public static int getIndexOf(String s,String m) {
if(s==null||m==null||s.length()<1||m.length()<1) {
return -1;
}
char [] str1=s.toCharArray();
char [] str2=m.toCharArray();
int i1=0;//str1的当前位置
int i2=0;//str2的当前位置
int [] next=getNextArray(str2);//str2的前缀信息数组
while(i1<str1.length && i2<str2.length) {
if(str1[i1]==str2[i2]) {
i1++;
i2++;
}else if(next[i2]==-1) {//str2中比对的位置已经无法往前跳了,就开始从i1后一个位置开始比对
i1++;
}else {
i2=next[i2];
}
}
//i1越界或者i2越界了
return i2==str2.length?i1-i2:-1;
}
public static int[] getNextArray(char[] ms) {
if(ms.length==1) {
return new int[] {-1};
}
int[] next=new int[ms.length];
next[0]=-1;
next[1]=0;
int i=2;//next数组的位置
int cn=0;//前缀和后一个位置,前缀和数量信息
while(i<next.length) {
if(ms[i-1]==ms[cn]) {//如果相等 cn++并且i++
next[i++]=++cn;
}else if(cn>0) {
cn=next[cn];
}else {
next[i++]=0;
}
}
return next;
}
}
2.Manacher算法
(1)题目:
找一个字符串的最大回文子串长度
(2)样例:
input :abcacwe
output :cac
(3)分析:
(1)经典解法: 如下图(1)所示,给输入的字符串加入特殊字符,然后依次遍历,找以每个字符为中心的回文子串。找出的最大长度 ➗ 2,就是原输入字符串的最大回文子串的长度。
(2)Manacher解法: 使用Manacher算法,我们需要一个回文半径数组来保存以每个字符为中心求得的回文半径,R来表示以C字符为中心的回文右边界。当当前字符 i 在 R 外时,i 向两边扩找回文串;当 i 在R 内时,有如下图(2)所示的三种情况:
(4)代码
public class MaxPalindrome {
public static void main(String[] args) {
String str="abcscba";
int max=maxLcpsLength(str);
System.out.println("该字符串的最大回文子串的长度是 :"+max);
}
//将输入的字符串转换为 需要的字符串 abc --> #a#b#c#
public static char[] manacherString(String str) {
char[] chs=str.toCharArray();
char[] res=new char[str.length()*2+1];
int index=0;
for(int i=0;i<res.length;i++) {
res[i]=(i&1)==0?'#':chs[index++];//奇数 & 1==1,偶数 & 1==0
}
return res;
}
//得到最大回文串的长度
public static int maxLcpsLength(String s) {
if(s==null||s.length()==0) {
return 0;
}
char[] str=manacherString(s);//abc --> #a#b#c#
int [] pArr=new int[str.length];//回文半径数组
int C=-1;//中心
int R=-1;//回文右边界在往右一个位置,最右的有效区是 R-1
int max=Integer.MIN_VALUE;//扩出来的最大值
for(int i=0;i<str.length;i++) {//每一个位置都求回文半径
//i至少的回文区域,先给pArr[i]
pArr[i]=R>i?Math.max(pArr[2*C-i], R-i):1;
while(i+pArr[i]<str.length && i-pArr[i]>-1) {
if(str[i+pArr[i]]==str[i-pArr[i]]) {
pArr[i]++;
}else {
break;
}
}
//如果当前的回文半径大于之前
if(i+pArr[i]>R) {
R=i+pArr[i];
C=i;
}
max=Math.max(max, pArr[i]);
}
return max-1;
}
}