转载自:https://blog.youkuaiyun.com/weixin_41698717/article/details/107863356
参考博客: 从头到尾彻底理解KMP(2014年8月22日版)
1 串匹配概述
字符串匹配有多种形式,包括模式检测( pattern detection),模式定位(pattern location),模式计数(pattern counting),模式枚举(pattern enumration)。
2 蛮力算法
蛮力算法是最直接最直觉的方法,可以作为改进的基础。
2.1 蛮力算法版本A
//brute-force-A.h
#include <cstring>
int matchA( char* P, char* T ){//串匹配算法(Brute-force-1)
size_t n = strlen( T ), i = 0;
size_t m = strlen( P ), j = 0;
while ( j < m && i < n )
if( T[i] == P[j] )//匹配则转到下一个字符
{ ++i; ++j; }
else{//不匹配则文本串回退,模式串复位
i -= j - 1;
j = 0;
}
return i - j;
}
2.2 蛮力算法版本B
//brute-force-B.h
#include <cstring>
int matchB( char* P, char* T ){//串匹配算法(Brute-force-2)
size_t n = strlen( T ), i = 0;
size_t m = strlen( P ), j ;
for( i = 0; i < n - m + 1; ++i ){//文本串从第i个字符开始逐个比对
for( j = 0; j < m; ++j){
if( T[i + j] != P[j] ) break;//失配则右移再做一轮比对
}
if( j >= m ) break; //找到匹配子串
}
return i;
}
2.3 蛮力算法测试
蛮力算法的测试程序如下,可见匹配成功时,版本A与版本B都会返回模式串在文本串中的位置。串匹配失败时,版本1返回大于n-m,版本2返回n-m+1。
/*
* test program on string matching
* author@Ripples
* 20200807
*/
#include "brute-force-A.h"
#include "brute-force-B.h"
#include <iostream>
using namespace std;
int main(){
//初始化模式串与文本串
char P[] = "structure";
char Y[] = "alo";
char T[] = "data structure and algorithm";
//测试brute-force-1
cout << matchA( Y, T ) << endl;//28 返回大于n-m表示失败
cout << matchA( P, T) << endl;//5 P在T中的位置
//测试brute-force-2
cout << matchB( Y, T ) << endl;//26 返回n-m+1 表示匹配失败
cout << matchB( P, T) << endl;//5 P在T中的位置
return 0;
}
3 KMP算法
3.1 KMP主算法
KMP算法可以视为从蛮力算法的版本A改进而来,KMP算法的思路可以总结如下:
(1)T[ i ] == P[ j ] ,则++i, ++j ,继续匹配;
(2)T[ i ] != P[ j ],i不变,j = next[ j ]。
其中,next表示KMP算法赖以高效实现的核心。KMP算法主算法如下所示
//match_kmp.h
#include <cstring>
int matchKMP( char* P, char* T ){//串匹配算法(Brute-force-1)
int* next = buildNext( P );
int n = strlen( T ), i = 0;
int m = strlen( P ), j = 0;
while ( j < m && i < n )
if( 0 > j || T[i] == P[j] )//匹配则转到下一个字符
{ ++i; ++j; }
else{//不匹配则文本串回退,模式串复位
j = next[ j ];
}
delete [] next;
return i - j;
}
3.2 构造next表
next表各值的含义是:当前字符以前的字符串中,有多大长度相同前缀与后缀。
求解next表只与模式串有关,具体求解方式如下:
(1)P[ k ] == P[ j ](其中k = next[ j ]),next[ j + 1 ] = next[ j ] + 1
(2)P[ k ] != P[ j ](其中k = next[ j ]),next[ j + 1 ] = next[ k ] + 1= next[ next[ j ] ] + 1
next表构造算法优化
不允许P[ next[ j ] ] == P[ j ],以此避免重复比较以及失配的字符。next表构造算法如下
int* buildNext( char* P ){
size_t m = strlen( P ), j = 0;
int* N = new int[ m ];
int t = N[0] = -1;
while( j < m-1 ){
if( 0 > t || P[j] == P[t]){//匹配
++j; ++t;//j保持不变,t失配后向前移动
N[j] = ( P[j] != P[t] ? t :N[t] );//N[j] =t
}else{//失配
t = N[t];
}
}
return N;
}
“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组
void GetNext(char* p,int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p[j] == p[k])
{
++k;
++j;
next[j] = k;
}
else
{
k = next[k];
}
}
}
int KmpSearch(char* s, char* p)
{
int i = 0;
int j = 0;
int sLen = strlen(s);
int pLen = strlen(p);
while (i < sLen && j < pLen)
{
//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
//next[j]即为j所对应的next值
j = next[j];
}
}
if (j == pLen)
return i - j;
else
return -1;
}
4 BM算法
4.1 BM主算法
BM算法与KMP算法类似。不同的是,BM算法从后往前匹配,充分使用以往的比对信息使得模式串往后移动尽可能远的距离。
BM算法分别用了坏字符策略与好后缀策略,对应bc表与gs表。通过这两种策略,完成了对以往比对信息的充分利用。
#include <cstring>
int matchBM( char* P, char* T ){
int* bc = buildBC( P ); int* gs = buildGS( P );//构造bc表与gs表
size_t i = 0;
while( strlen( T ) >= i + strlen( P )){
int j = strlen( P ) - 1;
while( P[j] == T[i+j])
if( 0 > --j) break;
if( 0 > j )
break;
else
i += (( gs[j] > ( j - bc[ T[i+j]])) ? gs[j] : ( j - bc[ T[i+j]]));//选择两者中的更大者
}
delete [] gs; delete [] bc;
return i;
}
4.2 坏字符策略与bc表
坏字符策略:某个字符不匹配,模式串向右移动 坏字符位置 - 坏字符在模式串中出现的最右边位置。其中,坏字符表示的是文本串中不匹配的字符。
//建立bc表
int* buildBC ( char* P ) { //构造Bad Charactor Shift表:O(m + 256)
int* bc = new int[256]; //BC表,与字符表等长
for ( size_t j = 0; j < 256; j ++ ) bc[j] = -1; //初始化:首先假设所有字符均未在P中出现
for ( size_t m = strlen ( P ), j = 0; j < m; j ++ ) //自左向右扫描模式串P
bc[ P[j] ] = j; //将字符P[j]的BC项更新为j(单调递增)——画家算法
return bc;
}
4.3 好后缀策略与gs表
好后缀策略:字符失配时,后移位数等于 好后缀位置 - 好后缀上一次出现位置。
gs表的建立依托于ss表的建立,建表程序如下所示。
//建立ss表
int* buildSS ( char* P ) { //构造最大匹配后缀长度表:O(m)
int m = strlen ( P ); int* ss = new int[m]; //Suffix Size表
ss[m - 1] = m; //对最后一个字符而言,与之匹配的最长后缀就是整个P串
// 以下,从倒数第二个字符起自右向左扫描P,依次计算出ss[]其余各项
for ( int lo = m - 1, hi = m - 1, j = lo - 1; j >= 0; j -- )
if ( ( lo < j ) && ( ss[m - hi + j - 1] <= j - lo ) ) //情况一
ss[j] = ss[m - hi + j - 1]; //直接利用此前已计算出的ss[]
else { //情况二
hi = j; lo = ( lo < hi ) ? lo : hi;
while ( ( 0 <= lo ) && ( P[lo] == P[m - hi + lo - 1] ) ) //二重循环?
lo--; //逐个对比处于(lo, hi]前端的字符
ss[j] = hi - lo;
}
return ss;
}
//建立gs表
int* buildGS ( char* P ) { //构造好后缀位移量表:O(m)
int* ss = buildSS ( P ); //Suffix Size table
size_t m = strlen ( P ); int* gs = new int[m]; //Good Suffix shift table
for ( size_t j = 0; j < m; j ++ ) gs[j] = m; //初始化
for ( size_t i = 0, j = m - 1; j < UINT_MAX; j -- ) //逆向逐一扫描各字符P[j]
if ( j + 1 == ss[j] ) //若P[0, j] = P[m - j - 1, m),则
while ( i < m - j - 1 ) //对于P[m - j - 1]左侧的每个字符P[i]而言(二重循环?)
gs[i++] = m - j - 1; //m - j - 1都是gs[i]的一种选择
for ( size_t j = 0; j < m - 1; j ++ ) //画家算法:正向扫描P[]各字符,gs[j]不断递减,直至最小
gs[m - ss[j] - 1] = m - j - 1; //m - j - 1必是其gs[m - ss[j] - 1]值的一种选择
delete [] ss; return gs;
}
4.4 BM算法测试
以下是BM算法的测试程序,可以看出BM完成了串匹配的任务。
/*
* test program on string matching
* author@Ripples
* 20200807
*/
#include "match_bm.h"
#include <iostream>
using namespace std;
int main(){
//初始化模式串与文本串
char P[] = "structure";
char T[] = "data structure and algorithm";
cout << matchBM( P, T ) << endl;//5
return 0;
}
5 Karp-Rabin算法
Karp-Rabin算法遵循“万物皆数”的中心思想,将字符串转换为散列码,称其为“指纹”。“判断模式串P是否与文本串T匹配”这一问题可以转换为“判断T中是否有某个子串与模式串P拥有相同的指纹”。
Karp-Rabin算法具体的流程如下:
通过散列找到指纹匹配的字符串;
对于模式串与找到的字符串逐个比对,比对成功则返回。
#include<iostream>
#include<string>
using namespace std;
string a;
int al;
string b;
int bl;
long long d = 1073741824;
long long calcuStr(string tmp) {
long long rst = 0;
for (int i = 0; i < tmp.length(); i++) {
rst += tmp[i] - 95 + i;//将字符串转为对应的数字,再加上他的位数,能有效减少散列冲突
}
return rst % d;//(书上写的是将字符串转换为27进制,这样得到的数过大,不方便运算)
}
long long upDate(long long rst, char a, char b) {
return rst - a + b;//更新a字符串匹配的那一段的hash值
}
bool match() {
int hashA = calcuStr(a.substr(0, bl));
int hashB = calcuStr(b);
for (int i = 1; i < al - bl + 1; i++) {
if (hashA == hashB) {//如果二者哈希值相等,再比较一下是不是真的匹配了
bool isEqual = true;
for (int j = 0; j < bl; j++) {
if (a[j + i - 1] != b[j]) {
isEqual = false;
break;
}
}
if (isEqual) return true;
}
else {
hashA = upDate(hashA, a[i - 1], a[i + bl - 1]);//未匹配,则后移
}
}
return false;
}
int main() {
while (cin >> a >> b) {
al = a.length();
bl = b.length();
cout << match() << endl;
}
}
6 Sunday算法
Sunday算法在匹配失败时关注文本串中参加匹配的最末位字符的下一个字符,这个算法的效率很高。
该字符未在模式串中出现过,移动位数 = 匹配串长度 + 1
否则,移动位数 = 模式串最右端的该字符到尾端的距离 + 1
代码链接:https://blog.youkuaiyun.com/cj1064789374/article/details/85037119
#include<bits/stdc++.h>
using namespace std;
void SundayMatch(const char *src, const char *patt){//传入源串(长)与模板串(短)
int slen = strlen(src);//得源串长
int plen = strlen(patt);//得模板串长
int shift[256];//位移标记
for(int i=0;i<256;++i)shift[i]=plen+1;//初始化全部都是模板串长+1,对于一些模板串中没有的字符就是偏移整个plen
for(int i=0;i<plen;++i)shift[(unsigned char)(patt[i])]=plen-i;//当前字符串距离最右端的距离
//遍历模板串的每一位,记录他到串末的步距,由于是左扫到右,可以保证每个字符是后出现的那个到最右端距离
int limit=slen-plen;//最后至少还剩下patt长度的未匹配字符段
for(int i=0;i<=limit;i+=shift[src[i+plen]]){//i是母串当前扫到的左界
int j=0;
while(j<plen){//然后以I为左界逐位判
if(src[i+j]!=patt[j])break;//如果不等就跳出,每次跳出后,左界i各移shift[src[i+plen]],
//即母串中i+plen的那个字符到模板串末步长
j++;//一直相同就不断加
}
if (j==plen)cout<<"The index is: " << i << endl;//万一匹配到相等退出表示找到,就读出此时左界即可
}
}
int main(){
char s[1003],p[1003];
cin>>s>>p;
SundayMatch(s,p);
return 0;
}