字符匹配问题。
因为项目需要,在字符串中查找是否有某段特定的字符出现,想起前段时间看的KMP算法,于是总结一下以免忘记了。
例如:字符串s: BDABCDABACD. 模式串p: ABA,因为s中有一小段字符串与模式串ABA相同,在s的第6位(初始为0),否则查找失败。
给s和p分别设置一个指针i,j = 0
一般的匹配过程是:一开始
BDABCDABACD
ABA
发现s[0]和p[0]不匹配。则将p向右移动一位,即i++,j不变。
BDABCDABACD
ABA
发现此时还是不匹配,则i++,j不变。
BDABCDABACD
ABA
此时i=2,j=0,发现s[2] = p[0] = A,则i++,j++,变成了i=3,j=1。发现s[3] = p[1] = B仍然匹配,则i++,j++,变成了i=4,j=2。但是此时的s[4] = C 就不等于 p[2]=A了。这时j重置为0,i=i-(j-1)=3.(因为之前发现第一次匹配是在i=2的情况,但是发现不匹配,所以i就要达到先前i++的效果,于是就从i=2变到了i=3了)
重复以上过程,发现在i=6的时候又出现了第一次匹配,即s[6] = p[0] = A。就是下面这种状态
BDABCDABACD
ABA
那么此时执行i++,j++ ,i就变成了7,j变成了1,发现s[7] = j[1] = B, 则再执行i++,j++,发现s[8] = j[2]=B,此时j到头了,于是就匹配了。返回i-j,就代表文本串开始匹配的位置。
这个算法通过遍历整个字符串找匹配,时间复杂度为O(m*n)。(假设m为|s|,n为|p|)比较慢,在扫描s的时候有些是元素是可以直接跳过的,比如在i=2,j=0的时候第一次匹配,到了i=4,j=2的时候适配了,此时i=i-(j-1)=3,回到了指向B的位置,去尝试和p[j=0]去匹配,但是此时肯定会发生失配,因为s[3]=p[1],但是p[0]不等于p[1],所以此时根本不用比就知道了,此步是trivial的。于是就有了下面的KMP算法。
KMP算法是由Knuth, Morris, Pratt这三位大牛于1977年发表的,其中第一位作者就是The art of computer programming的作者,现代计算机鼻祖。
思路:
当匹配到i,j位置的时候,如果j=-1或者匹配成功,s[i]=p[j]的话,i++, j++。
如果失配,即s[i]!=p[j],此时另i不变,j=next[j]。这样做的效果是,模式串p相对于文本串s向右移动了j-next[j]位。
What the hell is next array??
next数组,代表当前字符之前的字符串中(不包括当前字符),有多大长度的相同前缀后缀。例如,next[j] = k,说明在j之前的字符串中有最大长度为k的相同前缀后缀。
本来当失配的时候j回溯到0的,但是现在j不用了,只需要回到next[j]=k的位置就好啦,因为前面的字符串已经匹配了,但是注意这时i是不动的,从头到位都是递增的状态。
然后再拿s[i]去和p[j]尝试匹配。
什么是相同前缀后缀
对于一个p = p_0, p_1, p_2,…,p_j的模式串,如果有p_0,p_1,…p_k = p_j-k, p_j-k-1,…,p_j,则此时next[j]=k+1.
例如模式串ABA,各个字串是
前缀 后缀 最大公共元素长度
A 空 空 0
AB A B 0
ABA A,AB A,BA 1
所以模式串各个字符的前缀后缀的公共元素的最大长度表
字符 A B A
0 0 1
因为我们看的是失配的字符之前的字符串的最大相同前缀后缀,所以干脆定义一个next数组,next数组等于最大长度表右移1位,然后令首位为-1,另next[j]=k即可。
所以:
字符
j 0 1 2
字符 A B A
next -1 0 0
private static void getNext(String p, int[] next){
int plen = p.length();
next[0] = -1;
int k = -1;
int j = 0;
while(j<plen-1){
//p[k]表示前缀,p[j]表示后缀
if(k==-1 || p.charAt(j) == p.charAt(k)){
++k;
++j;
next[j] = k;
}
else{
k = next[k];
}
}
}
private static int kmpSearch(String s, String p, int[] next){
int i = 0;
int j = 0;
int slen = s.length();
int plen = p.length();
while(i<slen && j<plen){
if(j==-1 || s.charAt(i)==p.charAt(j)){
i++;
j++;
}
else{
j = next[j];
}
}
if(j==plen) return i-j;
else return -1;
}
时间复杂度,扫描s需要i=0 to m,所以O(m),扫描p需要j=0 to n,所以O(n),所以总的running time = 0(m+n)