字串的定位操作通常称为串的的匹配模式
朴素的模式匹配算法
假设我们要匹配下面的主串S=“goodgoogle”,中,找到T="google"这个字串位置,我们按朴素模式匹配算法需要下面几步
1.从主串第一个位置开始,S与T的前三个都匹配成功,第四个匹配失败
S:g o o d g o o g l e
| | | !
T:g o o g l e
2.从主串第二个位置开始,重新开始匹配
S:g o o d g o o g l e
!
T: g o o g l e
3.继续上面过程
S: g o o d g o o g l e
!
T: g o o g l e
4.匹配失败,继续匹配
S: g o o d g o o g l e
!
T: g o o g g l e
5.匹配失败,继续匹配
S: g o o d g o o g l e
!
T: g o o g l e
6.匹配成功。
S:g o o d g o o g l e
| | | | | |
T: g o o g l e
上面就是最普通的匹配算法
预备工作
#define Max 1000
typedef char String[Max];
代码如下
都是从下标为零的位置开始的
//朴素的模式匹配算法
int index(String S,String T,int pos)
{
int i = pos-1;
int j = 0;
int len1 = strlen(S);
int len2 = strlen(T);
while (i < len1 && j <len2)
{
if (S[i] == T[j])
{
++j;
++i;
}
else
{
i = i - j + 1; //匹配失败,回溯到首次匹配位置的下一位
j = 0;
}
}
if (j >= len2) //若匹配成功,则返回匹配到的位置下标
return i - len2;
else
return 0; //若匹配失败,则返回 0
}
KMP模式匹配算法
朴素的模式匹配算法的匹配算法虽然很容易理解,但效率极其低下,特别是当出现部分匹配的情况时,需要不断的回溯
所以聪明的前辈们想出了KMP模式匹配算法
我们先看看下面递推过程
S=“abcdeabcdef”,T=“abcdef”
进行匹配
S:a b c d e a b c d e f
| | | | | !
T:a b c d e f
在匹配到 f 时匹配失败,按朴素模式匹配算法的话,下一步,因该又从第二位开始匹配
但我们知道
T(1) !=T(2) !=T(3) !=T(4) !=T(5) !=T(6)
并且
T(1)=S(1),T(2)=S(2),T(3)=S(3),T(4)=S(4)
可以推出,在S(6)之前是没有可以和T串匹配的字符串的,所以可以直接跳到S(6)位置开始匹配
如下:
S:a b c d e a b c d e f
| | | | | |
T: a b c d e f
匹配成功
再看下面例子:
S =“abcababca”,T=“abcabx”
先从第一个位置开始匹配
S:a b c a b a b c a b x
| | | | | !
T:a b c a b x
T(1)!=T(2)!=T(3)
因为
T(1)=S(1),T(2)=S(2),T(3)=S(3),T(4)=S(4),T(5)=S(5)
但
T(1)=T(4),T(2)=T(5)
所以我们只能保证,S(4)前的是没有和T串匹配的
所以这次匹配失败后
下一次可以直接跳到S(4)位置开始匹配
如下:
S:a b c a b a b c a b x
| | !
T: a b c a b x
继续采用上面方法,下一次可以直接从S(6)位置开始匹配
S:a b c a b a b c a b x
| | | | | |
T: a b c a b x
匹配成功
通过上面方法进行字符串匹配的效率明显高于朴素模式匹配算法
next数组的推导
通过规律我们发现,匹配的关键是T串的相似度,在匹配前我们需要对T串进行研究
所以引出了next数组,来存放当前字符前的串的前后缀相似度
1.前缀: 包含首位字母但不包含末位字符的子串
2.后缀: 包含末位字符但不包含首位字符的子串
3.next数组的定义:当主串与模式串的某一位字符不匹配时,模式串要回退的位置
4.next [j]:其值 = 第j位字符前面 j-1位组成的子串的前后缀重合字符数 + 1
eg1:
j | 1 2 3 4 5 6 7 8 9 |
---|---|
模式串T | a b a b a a a b a |
next [ j ] | 0 1 1 2 3 4 2 2 3 |
当 j=1 时,next[1]=0;
当 j=2 时, 前面就是有只有一个字符 a 所以,next[2] = 1;
当 j=3 时, 前面子串中没有重合前后缀 所以,next[3] = 1;
当 j=4 时,前面字串中 a 重合,所以next[4]=2;
当 j=5 时,前面字串中 ab 重合,所以next[5]=3;
当 j=6 时,前面字串中 aba 重合,所以next[6]=4;
当 j=7 时,前面字串中 a 重合,所以next[7]=2;
当 j=8 时,前面字串中 a 重合,所以next[8]=2;
当 j=9 时,前面字串中 ab 重合,所以next[9]=3;
eg2:
j | 1 2 3 4 5 6 7 8 9 |
---|---|
模式串T | a a a a a a a a b |
next [ j ] | 0 1 2 3 4 5 6 7 8 |
当 j=1 时,next[1]=0;
当 j=2 时, 前面就是有只有一个字符 a 所以,next[2] = 1;
当 j=3 时, 前面字串中 a 重合,所以next[3]=2;
当 j=4 时,前面字串中 aa重合,所以next[4]=3;
当 j=5 时,前面字串中 aaa重合,所以next[5]=4;
当 j=6 时,前面字串中 aaaa重合,所以next[6]=5;
当 j=7 时,前面字串中 aaaaa重合,所以next[7]=6;
当 j=8 时,前面字串中 aaaaaa重合,所以next[8]=7;
当 j=9 时,前面字串中 aaaaaaa重合,所以next[9]=8;
代码:
都是从下标为零的位置开始的(上面推导过程是从一开始的)
void get_next(String T, int* next)
{
int i, k;
i = 1, k = 0;
next[0] = 0;
next[1] = 0;
int len = strlen(T);
while (i < len-1)
{
if (k == 0 || T[i] == T[k])
{
next[++i] = ++k;
}
else
{
k = next[k];
}
}
}
KMP算法代码
都是从下标为零的位置开始的
int index_KMP(String S, String T, int pos)
{
int i = pos;
int j = 0;
int next[Max];
get_next(T, next);
int len1 = strlen(S);
int len2 = strlen(T);
while (i < len1 && j < len2)
{
if (j == 0 || S[i] == T[j])
{
++i;
++j;
}
else
{
j = next[j];
}
}
if (j >= len2)
return i - len2;
else
return 0;
}
KMP模式匹配算法的改进
上面所讲的KMP算法只有在出现模式串与主串之间存在许多“部分匹配”的情况下,才能体现出他的优势
如下面
S =“aaabcde”,T =“aaaaax”
1.第一次匹配
S:a a a a b c d e q
| | | | !
T:a a a a a x
2.第二次匹配
S:a a a a b c d e q
| | | !
T: a a a a a x
3.第三次匹配
S:a a a a b c d e q
| | !
T: a a a a a x
4.第四次匹配
S:a a a a b c d e q
| !
T: a a a a a x
5.第五次匹配
S:a a a a b c d e q
!
T: a a a a a x
匹配失败
由这个例子可看出,并没有的到什么优化
而模式串前面几个字符都是a,那么2,3,4,5那四次匹配显得非常多余,没有必要进行
我们直接可以拿首位的next [1]的值去代替与它相等的字符对应的后续next数组值
nextval数组的推导
j | 1 2 3 4 5 6 7 8 9 |
---|---|
模式串T | a a a a a a a a b |
next [ j ] | 0 1 2 3 4 5 6 7 8 |
nextval [ j ] | 0 0 0 0 0 0 0 0 8 |
代码:
void get_nextval(String T, int* nextval)
{
int i, k;
i = 1, k = 0;
nextval[0] = 0;
nextval[1] = 0;
int len = strlen(T);
while (i < len-1)
{
if (k == 0 || T[i] == T[k])
{
i++;
k++;
if (T[i] != T[k])
{
nextval[i]=k;
}
else
{
nextval[i] = nextval[k];
}
}
else
{
k = nextval[k];
}
}
}