基于有限状态自动机分析KMP字符串匹配算法
一 定义
1 主串S = s1s2…sn, 即由n个字符组成的字符串
2 模式串T=t1t2…tm,即由m个字符组成的字符串
3 字符串匹配问题定义:给定主串S与模式串T,若S中含有T,返回T第一次出现的位置,否则返回-1.
二 普通做法
for(int i = 0; i < n; i++){
for(int j = 0; j < m && s[i + j]==t[j]; j++);
if(j == m) return i;
}
return -1;
三 有限状态自动机
有限状态自动机可以用5元式定义,即输入字符集,状态集,初始状态集,接受状态集,状态转移函数.
我们常用的正则表达式其实就是有限状态自动机,能够用有限状态自动机表示的语言即为正则语言.
字符串匹配本质也是一个模拟有限状态自动机的过程,即构造一个FA, 该FA只接受所有含有字符串T的字符串.若输入串S被该FA接受,则表明S含有T.
假设T=”ababcab”
其上述普通做法所模拟的FA可以描述如下:
每次内层循环开始,状态为0,若下一个字符串为a,则状态转为1,也即j++,直到j=7,即识别了字符串”ababcab”,到达了接受状态7. 相反,假设在状态2时,下一个输入字符不是’a’,则匹配失败,此时应该重新回归起始状态,在代码中的具体体现就是退出内层循环,i++,再进入内层循环,此时j=0,即重新从状态0开始。
但是上述FA不是通常意义上的FA,其效率非常低,有两个原因:
1) 每次匹配失败,状态重新回归0,即j=0。
2) 每次匹配失败,回退1…j个字符,即i=i+1,本来已经读取到了i+j。
上述两个回退分别体现在内循环与外循环,,比如我们在j=2时发现匹配失败,此时已经读取了i+2个字符,但是退出内层循环,只是i++,再重复读取字符,即从i+1开始重复读取字符,而第i+1,i+2个字符已经被读取了,按道理不应该重复计算。并且状态2应该具有对历史匹配的记忆能力,即状态2表示了0->1->2的过程,表示“ab”已经匹配了。
如何利用这些已知匹配信息来提高计算速度呢?主要从两点入手:
1) 每次匹配失败,状态回归到最近一个等价状态,比如状态3的等价状态应该是1,因为1表示已匹配字符‘a’,而3表示已匹配‘aba’,当然不能说1与3完全等价,只有在接下来的输入不是b时,状态3可看作与1等价因为,此时表示”aba”匹配没有意义,最多只有最后1个“a”还有意义,因为0->1也是表示匹配“a”,所以可以看作3与1等价。
2) 主串不需要回退,即不再需要外层循环。
首先我们可以假设任意一个状态具有两个跳转函数,其中1个若读取匹配的字符,跳转到下一个状态即i++, j++,另外一个即空跳转(读取ε)到初始状态0,当然空跳转只是在不匹配的情况下才使用,即j=0,注意由于是空跳转,即表示不读取任何输入,此时i不变,这也是合理的,因为si!=tj,那么留着si用于接下来的匹配。
根据以上分析,我们可以得出下属FA:
分析:
1) 状态0时,若输入不是a,则0->0;这里不是空跳转,注意若在起始状态如果不断空跳转,那么程序中的表现即i不变,陷入死循环。
2) 对于任意状态k,若不匹配,则需要回退到前面的状态,在前面我们都是让其回归初始状态,为了提高效率,让其回归最近一个“等价”状态,那么如何计算最近1个等价状态呢?
定义“等价”状态函数next[k],设想我们先找k的前状态k-1的等价状态next[k-1],若t[next[k-1]] = t[k-1],即表示next[k-1]->next[k-1]+1与k-1->k的跳转条件相同,那么可定义next[k] = next[k-1]+1,形象来说,即k-1的等价状态next[k-1]表示0,…,next[k-1]-1个字符与k-next[k-1], … ,k-1个字符相同,那么如果第next[k-1] 个字符与k-1个字符相同,相当于0,…,next[k-1] = k-next[k-1],…,k 即表示next[k]=next[k-1]+1,但是这是一个递推的过程,即如果t[next[k-1]] = t[k-1],那么判断是否t[next[next[k-1]]]=t[k-1]
在此可以定义next函数为:
若k=0, 则next[k]=-1/*设其为-1,一方面可以在下述递推中作为终止条件,另外也可以在0状态时,避免空跳转*/
若k>0, 且 t[k-1]=t[next[k-1]], 则next[k]=next[k-1]+1,否则递推判断是否t[k-1]=t[next[next[k-1]]],…;否则next[k]=0
具体程序Java实现如下:
int index(char src[], char pattern[])
{
int[] next = getNext(pattern);
int i = 0, j = 0;
while(i < src.length && j < pattern.length){
if(j == -1){ //起始位置不匹配,继续往前读
i++;
j = 0;
}
else if(pattern[j] == src[i]){ //状态转移
i++;
j++;
}
else j = next[j]; //只改变状态j,i不变,即空跳转
}
if(j == pattern.length) return i - j;
else return -1;
}
int[] getNext(char pattern[])
{
int[] next = new int[pattern.length];
next[0] = -1;
for(int i = 1; i < next.length; i++){
next[i] = 0; //初始化为0
int j = i - 1;
while(next[j] >= 0){ //递推求解next,-1为终止条件,即j=0时
if(pattern[i - 1] == pattern[next[j]]){
next[i] = next[j] + 1;
break;
}
else j = next[j];
}
}
return next;
}