模式匹配
模式匹配
子串的定位操作通常称为串的模式匹配,本小节主要介绍简单的模式匹配算法、KMP算法、KMP算法的进一步优化。
简单的模式匹配算法
简单的模式匹配算法,通过暴力搜索。
int Index(string s, string t)
{
int i = 0, j = 0;
while(i<s.length() && j < t.length())
{
if(s.at(i) == t.at(j))
{
i++;
j++;
}
else
{
i = i - j +1;
j = 0;
}
}
if (j >= t.length())
return i - j;
return -1;
};
KMP算法
在暴力匹配中,每趟匹配失败都是模式向后移一位再从头比较,这种频繁的重复使模式串不断地自我比较,因此其效率较低。
从分析模式本身的结构来看,若已匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么此时已匹配相等的前缀序列的该后缀及其后面展开区域有可能匹配。
PM表
字符串的前缀、后缀和部分匹配值
- 前缀:除最后一个字符以外,字符串的所有头部子串。
- 后缀:除第一个字符以外,字符串的所有尾部子串。
- 部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。
以'ababa'为例五,'ababa'的部分匹配值为00123:
- 'a'的前缀和后缀为空集,最长相等前后缀长度为0;
- 'ab'的前缀为,后缀为,,最长相等前后缀长度为0;
- 'aba'的前缀为,后缀为,,最长相等前后缀长度为1;
- 'abab'的前缀为,后缀为,,最长相等前后缀长度为2;
- 'ababa'的前缀为,后缀为,,最长相等前后缀长度为3;
'ababa'对应的PM(Partial Match)(部分匹配值)表格如下:
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
PM | 0 | 0 | 0 | 1 | 0 |
使用PM表进行字符串匹配:
- 第一趟匹配过程
主串 | a | b | a | b | c | a | b | c | a | c | b | a | b |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | c |
由于'c'和'a'不匹配,前面的2个字符'ab'是匹配的,通过查表可知,最后一个匹配字符'b'对应的部分匹配值为0,因此按照下述公式,计算子串需要向后移动的位数:
因此,故将子串向右移动位。
- 第二趟匹配过程
主串 | a | b | a | b | c | a | b | c | a | c | b | a | b |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | c | a | c |
'c'和'b'不匹配,查看最后一个字符'a'的部分匹配值为,因此,将子串右移位。
- 第三趟匹配过程
主串 | a | b | a | b | c | a | b | c | a | c | b | a | b |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | c | a | c |
全部匹配成功,由于匹配过程中,主串没有回退,因此时间复杂度为.
KMP算法原理
如下图,当'c'与'b'不匹配时,此时匹配的前缀为'abca',此时'abca'的最长公共元素为'a',因此可以将子串直接移动位,即位。
主串 | a | b | a | b | c | a | b | c | a | c | b | a | b |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
子串 | a | b | c | a | c | ||||||||
子串 | a | b | c | a | c | ||||||||
子串 | a | b | c | a | c | ||||||||
子串 | a | b | c | a | c |
对公式解释:
部分匹配值为最长公共匹配前后缀,因此用已经匹配的字符数-对应的部分匹配值,可以使最长的前缀挪到相应的后缀上。
对算法的改进
在使用部分匹配,每当失败时,就要去搜索他前面一个元素的匹配值,使用起来不方便,因此将PM表向右移动一位,此时,只需找匹配失败元素对应的值即可。
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | -1 | 0 | 0 | 0 | 1 |
第一个元素右移缺失后使用来填充,因为第一个元素就匹配失败的话,只需将子串向后移动一位,不需要计算移动位数。
最后一个元素在右移过程中溢出,由于最后一个元素没有后继元素需要使用他的部分匹配值,因此可以舍去。
此时推导出的子串移动位数(Move)公式为:
相当于将子串的指针回退到
next数组
的含义: 在子串的第个字符与主串发生失配时,跳到子串 位置重新与主串当前位置进行比较。
公式如下:
使用计算机对数组求解:
- 由公式有.
- 若,则:
2.1 若,则表明在模式串中不可能出现满足上述条件,此时.
2.2 若,则表明在模式串中,此时将滑动到,即令,若,则以此类推,直到找到更小的使,满足条件,则.
- 当不存2.中的满足上述条件时,即不存在长度更短的相等前缀后缀时,令
KMP算法流程举例
设S='aabaabaabaac',P='aabaac',求P的及KMP匹配过程。
使用手工法求数组,步骤如下:
- 求PM表
j | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
P | a | a | b | a | a | c |
PM | 0 | 1 | 0 | 1 | 2 | 0 |
- 将PM表向右移动一位
j | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
P | a | a | b | a | a | c |
PM Moved | -1 | 0 | 1 | 0 | 1 | 2 |
- 将PM Moved所有元素加1
j | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
P | a | a | b | a | a | c |
next | 0 | 1 | 2 | 1 | 2 | 3 |
KMP匹配的过程如下:
匹配过程中进行的单个字符之间的比较次数为6+4+4
- i=6,j=6时匹配失败,将P位于的数组移到j处。
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
S | a | a | b | a | a | b | a | a | b | a | a | c |
P | a | a | b | a | a | c | ||||||
next | 0 | 1 | 2 | 1 | 2 | 3 |
- i=9,j=6时匹配失败,将P位于的数组移到j处。
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
S | a | a | b | a | a | b | a | a | b | a | a | c |
P | a | a | b | a | a | c | ||||||
next | 0 | 1 | 2 | 1 | 2 | 3 |
- 匹配成功。
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
S | a | a | b | a | a | b | a | a | b | a | a | c |
P | a | a | b | a | a | c | ||||||
next | 0 | 1 | 2 | 1 | 2 | 3 |
KMP的代码实现
采用先求解移位后的PM表,再对所有元素+1的方式实现。
vector<int> get_next(string P) {
vector<int> next(P.length(), 0);
next[0] = -1;
int j = 0, k = -1;
while (j < P.length()) {
if (k == -1 || P.at(j) == P.at(k)) {
k++;
j++;
next[j] = k;
} else
k = next[k];
}
for (int i = 0; i < P.length(); i++)
next.at(i)++;
return next;
}
KMP算法的改进-nextval数组
KMP缺陷
前面KMP定义的数组在某些情况下存在缺陷,如下表所示。
主串 | a | a | a | b | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
模式 | a | a | a | a | b | ||||
j | 1 | 2 | 3 | 4 | 5 | ||||
next | 0 | 1 | 2 | 3 | 4 | ||||
nextval | 0 | 0 | 0 | 0 | 4 |
使用KMP匹配的过程如下:
- 第一次匹配
主串 | a | a | a | b | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
模式 | a | a | a | a | |||||
j | 1 | 2 | 3 | 4 | |||||
next | 0 | 1 | 2 | 3 |
- 第二次匹配
主串 | a | a | a | b | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
模式 | a | a | a | ||||||
j | 1 | 2 | 3 | ||||||
next | 0 | 1 | 2 |
- 第三次匹配
主串 | a | a | a | b | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
模式 | a | a | |||||||
j | 1 | 2 | |||||||
next | 0 | 1 |
- 第四次匹配
主串 | a | a | a | b | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
模式 | a | ||||||||
j | 1 | ||||||||
next | 0 |
- 第五次匹配
主串 | a | a | a | b | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
模式 | a | a | a | a | b | ||||
j | 1 | 2 | 3 | 4 | 5 | ||||
next | 0 | 1 | 2 | 3 | 4 |
可以发现,该算法效率低下的主要的原因在于 , 上述例子中、、, 因此后三次使用相同的字符和匹配,因此无意义,一定失配。
KMP改进
出现上述问题后,遇到的处理方法:
若出现上述情况,则将修改为,直至两者不再相等。