给定文本串以及模式串,判断文本串中是否含有模式串,如果含有则返回模式串在文本串的首地址,如果不包含则返回-1即可。
例如:
文本串String txt = "abcababcabababccdabsadasas"; 模式串String pat = "ababcabababc"; 返回3
文本串String txt = "aaabaaabaaabaaabaaab"; 模式串String pat = "aaaab"; 返回-1。
主要内容
暴力求解
利用双指针,i从文本串起始位置开始遍历,对于文本串的每一个位置,模式串从该位置依次往后匹配,如果模式串能够全部匹配,则返回i位置即可;如果模式串不匹配,则移动i,如果i到达尾部还不匹配,则返回-1;
public static int search1(String txt, String pat){
int N = txt.length();
int M = pat.length();
//文本串进行每一位进行循环
for(int i=0; i<=N-M; i++){
int j;
for(j=0; j<M; j++){
if(txt.charAt(i+j) != pat.charAt(j)) break;
}
if(j == M)
return i;
}
return -1;
}
暴力求解的另一种策略:
i指向文本串S j指向模式串P 从起始位置i=0 j=0开始匹配,如果S[i]==S[j],则i++,j++;如果S[i]!=S[j],则说明从文本串的i位置开始不会匹配模式串,应该将i更新为本次匹配i初始位置的下一个位置 j指向模式串的0位置。
public static int search2(String txt, String pat){
int N= txt.length();
int M = pat.length();
int j = 0;
int i = 0;
//文本串以及模式都往后移动
while(i<N && j<M){
//如果匹配,则文本以及模式串都往后移动一个位置
if(txt.charAt(i)==pat.charAt(j)){
j++;
i++;
}else{
//如果不匹配,则文本回退j个位置 模式串清零
//回退j个位置回到本次不匹配的起始位置,还应该加一,使得文本串往后移动一次
i = i-j+1;
j = 0;
}
}
if(j == M) return i-M;
return -1;
}
由于出现不匹配情况时,i,j指针都需要回退,该算法时间复杂度为O(MN)
KMP算法核心思想
如下图所示,S为文本串 P为模式串 初始位置i=0 j=0;
当i=7 j=7时,出现不匹配,暴力求解的思路是,i退回到初始位置下一个位置1 j=0,此时显然不匹配,继续i=2,3,..直到i=5时,此时j=0,有S[i]=S[j],则i++,j++继续开始匹配,直到i=7,j=2时,即从i=1,2,3,4过程中,j=0,该过程是没有必要的,因为必定不相等;i=5,j=0 i=6 j=1也是没有必要比较的,因为必定相等,即当i=7,j=7时出现不匹配,此时可以保持i不变,将j直接回退到2位置继续比较即可。
参考下图,暴力求解i,j回退之后再次比较,实质是模式串的前缀与模式串不匹配字符之前的后缀来进行比较。
例如 i=1 j=0 其实是比较S[1,6]与P[0,5] 又因为S[1,6]=P[1,6] 即该过程是比较P[0,5]与P[1,6];同理i=2, j=0是比较P[0,4]与P[2,6]。并且该过程只与模式串有关,如果事先对于模式串进行处理,求得模式串的不同位置,最大前缀与后缀,则可以实现i保持不变,j回退到最大前缀后缀位置处。
初始情况:
如果S[i]==S[j],则i++ j++ 继续匹配下一个字符,直到S[i]!=P[j]
此时i保持不变,j回退到某个位置,继续进行比较,j应该回退到模式串未匹配字符之前的字符串的最大前缀后缀;
假定对于模式串存在某一个最大的K,有
P[0,k-1]=P[j-k,j-1] 即此时模式串未匹配字符之前有最大前缀后缀P[0,k-1]。
因为S[i]!=P[j] 说明S[i-j, i-1]=P[0, j-1];
因为P[0,k-1]=P[j-k,j-1] 即S[j-k,j-1]=P[j-k,j-1]=P[0,k-1]
即如果模式串P中存在最大前缀后缀P[0,k-1],则P[0,k-1]与S[j-k,j-1]必定相等,因为之前已经比较过,则将j回退到P[k]位置处即可
KMP算法的核心思想:
当S[i]==P[j]时,i++ j++;
当S[i]!=P[j] 时,此时i保持不变 j回退到P[0,j-1]中的最大前缀后缀位置处,继续进行S[i]与P[j]比较;
假定next数组用来保存每个j位置不匹配时,j应该回退的位置j=next[j],则KMP算法可以如下:
public static int kmp(String txt, String pat){
int N = txt.length();
int M = pat.length();
int[] next = getNext1(pat);
int i = 0;
int j = 0;
while(i<N && j<M){
if(j==-1 || txt.charAt(i)==pat.charAt(j)){
i++;
j++;
}else{
j = next[j];
}
}
if(j == M) return i-M;
return -1;
}
next数组求解
对于模式串的每个位置,都需要求解最大前缀后缀,即是否存在某个K,使得模式串满足:
P[0,k-1]=P[j-k,j-1]
假定模式串长度为M,则定义next数组来保存最大前缀后缀k的值.
next[j]=k,说明当S[i]!=P[j]时,此时j=next[j],即P[0,k-1]=P[j-k,j-1]
定义next[0]=-1,此时说明j的下一个位置无论是多少都无法满足i位置处匹配模式串,即i位置处所有情况已经处理都不匹配,此时可以将i后移一位;j指向0位置处,即若j==-1时,i++,j++即可;
next[j](0<=j<=M-1)数组定义如下:
next[0]=-1,表示文本串需要后移一位;
如果存在P[0,k-1]=P[j-k,j-1] 0<k<j,则 next[j]=max{k|P[0,k-1]=P[j-k,j-1] 0<k<j},表示存在最大前缀后缀;
其他情况next[j]=0,表示从模式串初始位置开始匹配;
next数组的求解过程一
当出现不匹配时,求出不匹配字符的前面字符串的最大前缀后缀;
假定模式串为ababcabababc 由以上定义可知 next[0]=-1 next[1]=0
当j==2时,求解过程如下:固定模式串P作为文本串,此时文本串指针指向2,将模式串指针指向1,此时比较P[0],P[1],由于不相等,即子串“ab”没有符合条件的k,next[j]=0
当j==3时,固定模式串P,将模式串从1位置处依次后移,判断是否含有最大前缀后缀
即此时需要判断“aba”是否含有最大前缀后缀,比较“ab”ba" 再比较“a“”a” 该比较过程分别对应下图模式串的后移不同位置
即要求某一个字符串的最大前缀后缀,等价于模式串作为文本串固定,模式串从1位置处依次后移一位,判断是否有最大前缀后缀。
j=4,5,6,7,8,9,10,11情况依次如下:
将上述探索过程,使用代码来求解:
以下是利用暴力方法来探索模式串每一个子串是否为最大前缀后缀,时间复杂度过大
public static int[] getNext1(String pat){
int M = pat.length();
int[] next = new int[M];
next[0] = -1;
next[1] = 0;
int j = 2;
//next[0] [1]由定义可以直接确定
//对于j>1 next[j]使用暴力匹配求解,模式串与模式串右移一位开始匹配,直到发现最大相同前后缀
while(j<M){
System.out.println(Arrays.toString(next));
for(int i=1; i<j; i++){
if(pat.substring(i, j).equals(pat.substring(0, j-i))){
next[j] = j - i;
break;
}
}
j++;
}
System.out.println(Arrays.toString(next));
return next;
}
利用递推式来求解next数组
分析next数组暴力求解过程,可以发现求解next过程也是一个字符串匹配问题,此时文本串与模式串均为模式串;
next数组定义如下:
next[0]=-1;
如果存在这样的k满足P[0,k-1]=P[j-k,j-1] 0<k<j, 则next[j]=max{k|P[0,k-1]=P[j-k,j-1] 0<k<j};
其他情况next[j]=0;
next数组的递推式
next[j]=k的含义是P[0,k-1]=P[j-k,j-1]
如果有P[k]=P[j],则有P[0,k]=P[j-k,j]
由next定义得next[j+1]=k+1
即求解模式串next数组过程中,将模式串与模式串进行匹配,初始条件是next[0]=-1,
next[j]=k
如果P[j]==P[k],则说明next[j+1]=k+1
如果P[j]!=P[k],则j应该回退到next[j]位置。
以模式串“aaab”为例分析:
如上所示,将模式串作为文本串,同时也作为模式串,i指向文本串 j指向模式串,初始状态next[0]=-1 i=0 j=-1,为了便于观察,任何时刻i与j指针都要对齐。与KMP算法思路相同
如果j==-1,说明此时i位置需要移动,j回到0位置处 即i=1 j=0 此时next[j]=j=0;
下一步比较P[1]与P[0],相等,则i++, j++有next[j] =j;
i不断移动直到i=3 j=2时,此时P[3]!=P[2],即此时P[1,3]!=P[0,2]。因此此时最大前缀后缀长度不会增加,j需要回退到next[j]位置1处,继续进行匹配,直到i=4,结束,此时next数组求解为{-1,0,1,2}
求解代码如下:
//获取next数组
public static int[] getNext2(String pat){
int M = pat.length();
int[] next = new int[M];
next[0] = -1;
//文本串--模式串从0位置开始
int i = 0;
//模式串从-1位置开始 j==-1 说明文本串以及模式串均需要后移
int j = -1;
while(i<M-1){
//System.out.println(i+" "+j+" "+Arrays.toString(next));
//j==-1 表示当前文本串i位置不可能出现匹配,应该i++ j++,使得文本串后移一位,模式串指向初始位置0,开始匹配
//S[i]==S[j] 说明出现最大相同前后缀 将前后缀长度更新到next[i]中
if(j==-1 || pat.charAt(i)==pat.charAt(j)){
i++;
j++;
next[i] = j;
}
//如果出现不匹配,则j指针回退
else{
j = next[j];
}
}
System.out.println(Arrays.toString(next));
return next;
}
next数组求解优化
假定文本串为aabaabaabaabaab 模式串为aaab,利用上述求解过程有next={-1,0,1,2}
具体匹配过程如下:
如上所示,当i=2,j=2时出现不匹配,此时j回退到j=1,该位置继续不匹配j继续回退j=0,该位置不匹配,继续回退到-1,由于j=-1,此时i后移一位,j=0。观察整个过程 j=1 j=0回退过程是没有必要的,
当S[i]!=P[j]时,将j=next[j]=k,即下一步比较S[i]与P[k],如果有P[j]==P[k],则S[i]!=P[k]必定成立,即此时不需要进行比较,即在求解next过程中,如果某个位置有P[j]==P[k],则说明next[j]=next[k]即可,即如果出现不匹配时,P[j]!=S[i],此时j回退j=next[j],j应该回退到某个位置k此时有P[k]!=P[j],如果P[k]==P[j],则应该继续回退以前位置,此处比较时没有意义的。
//获取next数组
public static int[] getNext3(String pat){
int M = pat.length();
int[] next = new int[M];
next[0] = -1;
//文本串--模式串从0位置开始
int i = 0;
//模式串从-1位置开始 j==-1 说明文本串以及模式串均需要后移
int j = -1;
while(i<M-1){
//System.out.println(i+" "+j+" "+Arrays.toString(next));
//j==-1 表示当前文本串i位置不可能出现匹配,应该i++ j++,使得文本串后移一位,模式串指向初始位置0,开始匹配
//S[i]==S[j] 说明出现最大相同前后缀 将前后缀长度更新到next[i]中
if(j==-1 || pat.charAt(i)==pat.charAt(j)){
i++;
j++;
//判断下一个位置是否相同,如果相同,则直接next[i]=next[j]
if(pat.charAt(i)!=pat.charAt(j))
next[i] = j;
else
next[i] = next[j];
}
//如果出现不匹配,则j指针回退
else{
j = next[j];
}
}
System.out.println(Arrays.toString(next));
return next;
}
KMP算法
KMP最终代码实现如下所示: 时间复杂度为O(M+N)
public static int kmp(String txt, String pat){
int N = txt.length();
int M = pat.length();
int[] next = getNext3(pat);
int i = 0;
int j = 0;
while(i<N && j<M){
if(j==-1 || txt.charAt(i)==pat.charAt(j)){
i++;
j++;
}else{
j = next[j];
}
}
if(j == M) return i-M;
return -1;
}
//获取next数组
public static int[] getNext3(String pat){
int M = pat.length();
int[] next = new int[M];
next[0] = -1;
//文本串--模式串从0位置开始
int i = 0;
//模式串从-1位置开始 j==-1 说明文本串以及模式串均需要后移
int j = -1;
while(i<M-1){
//System.out.println(i+" "+j+" "+Arrays.toString(next));
//j==-1 表示当前文本串i位置不可能出现匹配,应该i++ j++,使得文本串后移一位,模式串指向初始位置0,开始匹配
//S[i]==S[j] 说明出现最大相同前后缀 将前后缀长度更新到next[i]中
if(j==-1 || pat.charAt(i)==pat.charAt(j)){
i++;
j++;
//判断下一个位置是否相同,如果相同,则直接next[i]=next[j]
if(pat.charAt(i)!=pat.charAt(j))
next[i] = j;
else
next[i] = next[j];
}
//如果出现不匹配,则j指针回退
else{
j = next[j];
}
}
System.out.println(Arrays.toString(next));
return next;
}
next数组
next[j]=k 说明对于模式串P 满足P[0,k-1]=P[j-k, j-1]。此时如果S[i]!=P[j],则j=next[j]=k位置处继续匹配;
参考链接:
https://blog.csdn.net/xiaohuanglv/article/details/85178138