问题引出
假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?
暴力匹配
如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
- 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
- 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
理清楚了暴力匹配算法的流程及内在的逻辑,咱们可以写出暴力匹配的代码,如下:
public function index($s, $p){
$sLen = strlen($s);
$pLen = strlen($p);
$i = 0;
$j = 0;
while($i < $sLen && $j < $pLen){
//如果相等则同时后移
if($s[$i] == $p[$j]){
$i++;
$j++;
} else {
//如果失配(即$s[i] != $p[j]),令i = i - (j - 1),j = 0
$i = $i - $j + 1;
$j = 0;
}
}
//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
if($j == $pLen){
return $i - $j;
} else {
return -1;
}
}
缺点
该方法会反复回溯主串,导致效率低下
KMP模式匹配
可以利用已经部分匹配这个有效信息,保持主串上的指针不回溯,通过修改子串的指针,让模式串尽量地移动到有效的位置。
next数组值推导
原理摘要:
1:j值的变化与主串没什么关系,关键就取决于p串的结构中是否有重复的问题
2:j值的多少取决于当前字符串之前的串的前后缀的相似度
公式:
1:当j=1时,为0
2:当此集合不为空时 Max{k|1<k<j,且’p1…pk-1’=’pj-k+1…pj-1’}
3:其他情况 1
代码实现
public function kmpSearch($s, $p){
$sLen = strlen($s);
$pLen = strlen($p);
$i = 0;
$j = 0;
$next = $this->getNext($p);
while($i < $sLen && $j < $pLen){
//如果相等或者j=-1则同时后移
if($s[$i] == $p[$j] || $j == 0){
$i++;
$j++;
} else {
//如果$j != -1,且当前字符匹配失败(即$s[i] != $p[j]),则令 i 不变,$j = $next[$j]
$j = $next[$j];
}
}
//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
if($j == $pLen){
return $i - $j;
} else {
return -1;
}
}
private function getNext($p){
$pLen = strlen($p);
$next = array();
$i = 1;
$j = 0;
$next[1] = 0;
while($i < $pLen){
if($j == 0 || $p[$i] == $p[$j]){ //$p[$i]表示后缀的单个字符、$p[$j]表示前缀的单个字符
++$i;
++$j;
$next[$i] = $j;
} else {
$j = $next[$j]; //若字符不相等 则j值回溯
}
}
return $next;
}
不足
比如我们的主串=“aaaabcde”,子串=“aaaaax”,其中next数组值分别为012345,在开始时,当i=5、j=5时,我们发现b与a不相等。那么其实中间的几步都是多余的判断。
由于子串的第二、三、四、五位置的字符都与首位的a相等,那么可以用首位next[1]的值去取代与它相等的字符后续next[j]的值,这是个很好的办法。
kmp优化
nextval数组值推导
它是在计算出next值的同时,如果a位字符与它next值指向的b位字符相等,则将a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next的值。
代码实现
private function getNextVal($p){
$pLen = strlen($p);
$nextVal = array();
$i = 1;
$j = 0;
$nextVal[1] = 0;
while($i < $pLen){
if($j == 0 || $p[$i] == $p[$j]){ //$p[$i]表示后缀的单个字符、$p[$j]表示前缀的单个字符
++$i;
++$j;
//若当前字符与前缀字符不同,则当前的j为nextval在i位置的值
//如果与前缀字符相同,则将前缀字符的nextval值赋值给nextval在i位置的值
if(isset($p[$i]) && $p[$i] != $p[$j]){
$nextVal[$i] = $j;
} else {
$nextVal[$i] = $nextVal[$j];
}
} else {
$j = $nextVal[$j]; //若字符不相等 则j值回溯
}
}
return $nextVal;
}
next数组推导实战
假如我们子串=“abcdex”
推导过程:
1:当j=1时,next[1]=0
2:当j=2时,j由1到j-1就只有串a,属于其他情况next[2]=1
3:当j=3时,j由1到j-1串是ab,显然a与b不相等,属于其他情况,next[3]=1
4:以后同理,所以最终的next[j]为011111
总结:如果前后缀一个字符相等,k值是2,两个字符相等k值是3,n个相等k值就是n+1
nextval数组推导实战
举例 我们的子串=“aaaaaaaab”,先得出next数组的值分别为012345678,接下来我们来看nextval
推导过程:
1:当j=1时,next[1]=0
2:当j=2时,next的值为1,第二个字符与第一个字符相等,所以nextval[2]=nextval[1]=0
3:同样的道理,其后都为0
4:当j=9时,next的值为8,第九个字符b与第八个字符a不相等,所以nextval[9]=8
扩展知识
1:BM算法
KMP的匹配是从模式串的开头开始匹配的,而1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法。该算法从模式串的尾部开始匹配,且拥有在最坏情况下O(N)的时间复杂度。在实践中,比KMP算法的实际效能高。
同时应用到了两种启发式规则,即坏字符规则 和好后缀规则 ,来决定向右跳跃的距离。基本思路就是从右往左进行字符匹配,遇到不匹配的字符后从坏字符表和好后缀表找一个最大的右移值,将模式串右移继续匹配。
2:Sunday算法
上文中,我们已经介绍了KMP算法和BM算法,这两个算法在最坏情况下均具有线性的查找时间。但实际上,KMP算法并不比最简单的c库函数strstr()快多少,而BM算法虽然通常比KMP算法快,但BM算法也还不是现有字符串查找算法中最快的算法,本文最后再介绍一种比BM算法更快的查找算法即Sunday算法。
Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很相似:
- 只不过Sunday算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。
- 如果该字符没有在模式串中出现则直接跳过,即移动位数 = 匹配串长度 + 1;
- 否则,其移动位数 = 模式串中最右端的该字符到末尾的距离+1。