又是群里小伙伴在问,想到俺以前也只是看过相关文章,知道个大概,如果面试到,还真不一定能写出来。便想着试试。
目前我看过两种思路:
1.类动态规划的方式
2.比较前缀和后缀
无论是哪种思路,匹配原理都是一样的,不过求状态转移数组的方式不一样。
第一种,类动态规划的思路
这种思路就与昨天做的骑士拨号器算法题类似,不过“拨号”范围变为了256个ASCII码,而状态转移的规则由匹配串得来。
比如匹配串为ababacba
下表表示了如果主串字符为啥时,下一个字符应该用匹配串的哪一位去匹配。(0~n)
尝试匹配字符\匹配串 | a | b | a | b | a | c | b | a |
---|---|---|---|---|---|---|---|---|
a | 1 | 1 | 3 | 1 | 5 | 1 | 1 | 8 |
b | 0 | 2 | 0 | 4 | 0 | 4 | 7 | 0 |
c | 0 | 0 | 0 | 0 | 0 | 6 | 0 | 0 |
其他省略… | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
可以发现如果某位匹配不上时,就会跳转到上次匹配上的情况,而且一个字符匹配上了,其他字符肯定是匹配不上的必然需要状态回转。利用这个现象我们就可以写出来dp数组和它的转移方程了。
dp数组定义: dp[i][j]:到达i状态时(成功匹配第i-1个字符时,也就是说i待匹配),当前字符为j时状态应该如何转移(下一个字符应该匹配匹配串的哪一位)。
转移方程: dp[i][j] = 如果匹配得上 dp[i][j]=i+1; 如果匹配不上 dp[i][j] = dp[上一次匹配上的状态][j]
注意由于刚才提到一个字符匹配上必然其他字符匹配不上,所以上一次匹配上的状态是本次真实字符的情况。
初始化: dp[0][0位置时的字符] =1;其他都为0
/**
* @创建人 YDL
* @创建时间 2020/5/24 17:08
* @描述
*/
public class KMP {
private char[] matched;
private char[] s;
private int len;
private int[][] dp;
public KMP(String matched, String s) {
this.matched = matched.toCharArray();
this.s = s.toCharArray();
len = matched.length();
dp = new int[len][256];
}
private void kmp(){
dp[0][matched[0]] = 1;
int pre = 0;
for(int i=1;i<len;i++){
for(int j=0;j<256;j++){
if(j==matched[i])
dp[i][j] = i+1;
else
dp[i][j] = dp[pre][j];
}
pre = dp[pre][matched[i]];
}
}
public int serch(){
kmp();
for(int i=0,inx =0;i<s.length;i++){
inx = dp[inx][s[i]];
if(inx==len){
return i-len+1;
}
}
return -1;
}
public static void main(String[] args) {
KMP kmp = new KMP("abababaca","abababababababaca");
System.out.println(kmp.serch());
}
}
比较前缀和后缀
这个求状态转移的方式,就是求某位置为止的子串最长相同前后缀。这个公共的前后缀长度就是最少回退的内容,举个例子
尝试匹配字符\匹配串 | a | b | a | b | a | c | b | a |
---|---|---|---|---|---|---|---|---|
当前子串最长公共前后缀 | 0 | 0 | 1 | 2 | 3 | 0 | 0 | 1 |
因为后缀与前缀的公共串的存在,那么如果没匹配上那么当前位置形成的后缀就可以替代之前的前缀,自然就无需再重复比较。
那么如果当前没有匹配上,且已经匹配到当前串的某个位置了,状态转移就可以这么写(就是将最大公共前后缀的位置往前移一位辣)
尝试匹配字符\匹配串 | a | b | a | b | a | c | b | a |
---|---|---|---|---|---|---|---|---|
当前子串最长公共前后缀 | 0 | 0 | 1 | 2 | 3 | 0 | 0 | 0 |
next | 0 | 0 | 0 | 1 | 2 | 3 | 0 | 0 |
代码如下:
private void kmp(){
next[0] = 0;
for(int i=1;i<matched.length-1;i++){
int left=0,right = 1,ans =0;
while(right<=i){
if(matched[left]==matched[right]){
left++;right++;ans++;
}else {
left = 0;
right++;
ans = 0;
}
}
next[i+1] = ans;
}
}
但是这种写法很粗糙,求next数组时,相当于也用的暴力算法求的最大公共前后缀。那么有没有什么取巧的办法呢?
这不就是缩小化子串匹配问题吗
那么按照差不多的思路
后面就不一一画图了,总之被匹配串相当于后缀,匹配串相当于前缀。如果能够匹配上就同时前进,如果匹配不上就跳转上一次能够匹配上的情况。
优化后的完整版。
/**
* @创建人 YDL
* @创建时间 2020/5/24 17:08
* @描述
*/
public class KMP {
private char[] matched;
private char[] s;
private int len;
private int[] next;
public KMP(String matched, String s) {
this.matched = matched.toCharArray();
this.s = s.toCharArray();
len = matched.length();
next = new int[len];
}
private void kmp(){
next[0] = -1;
for(int i=1,inx=-1;i<matched.length;i++){
if(inx==-1||matched[i]==matched[inx]){
inx++;
next[i]=inx;
}else {
inx = next[inx];
i--;
}
}
}
public int serch(){
kmp();
for(int i=0,inx=0;i<s.length;i++){
if(next[inx]==-1||s[i]==matched[inx])
inx++;
else {
i--;
inx = next[inx];
}
if(inx==len){
return i-inx+1;
}
}
return -1;
}
public static void main(String[] args) {
KMP kmp = new KMP("abababaca","abababababababaca");
System.out.println(kmp.serch());
}
}