- 将原串的指针移动至本次「发起点」的下一个位置(b 字符处);匹配串的指针移动至起始位置。
- 尝试匹配,发现对不上,原串的指针会一直往后移动,直到能够与匹配串对上位置。
当使用朴素匹配法时, 当字符串与模式串中的字符不匹配时,
则字符串中的指针调整至下一个发起点;匹配串中的指针调整至起始位置, 两个指针位置上的字符重新匹配;
Knuth-Morris-Pratt 算法的核心为前缀函数,记作 π(i),其定义如下:
对于长度为 m 的字符串 s,其前缀函数π(i)(0≤i<m) 表示 s 的子串 s[0:i] 的最长的相等的真前缀与真后缀的长度。
特别地,如果不存在符合条件的前后缀,那么 π(i)=0。其中真前缀与真后缀的定义为不等于自身的的前缀与后缀。
我们举个例子说明:字符串 aabaaabaabaaab 的前缀函数值依次为 0,1,0,1,2,2,30,1,0,1,2,2,3。
π(0)=0,因为 aa 没有真前缀和真后缀,根据规定为 0(可以发现对于任意字符串 π(0)=0 必定成立);
π(1)=1,因为 aaaa 最长的一对相等的真前后缀为 aa,长度为 11;
π(2)=0,因为 aabaab 没有对应真前缀和真后缀,根据规定为 00;
π(3)=1,因为 aabaaaba 最长的一对相等的真前后缀为 aa,长度为 11;
π(4)=2,因为 aabaaaabaa 最长的一对相等的真前后缀为 aaaa,长度为 22;
π(5)=2,因为 aabaaaaabaaa 最长的一对相等的真前后缀为 aaaa,长度为 22;
π(6)=3,因为 aabaaabaabaaab 最长的一对相等的真前后缀为 aabaab,长度为 33。
有了前缀函数,我们就可以快速地计算出模式串在主串中的每一次出现。
1.KMP
前缀串:指一串字符中,以第一个字符为开头的所有连续子串,但不包括最后一个字符;
后缀串: 指一串字符中,以最后一个字符为结尾的所有连续子串, 但不包括第一个字符;
1.1 kmp 优势
KMP的出现使得:
当字符串与模式串中的字符不匹配时:
1. 首先在模式串中检查已经匹配成功的部分, 是否存在相同的前缀和后缀,
1.1 如果存在, 则模式串指针跳转到相同前缀的后一个位置开始匹配; 但是,如果跳转后的模式串指针位置上的元素 与 字符串中指针位置上的元素仍然不匹配时,(并且,如果此时模式串指针前面已经不存在相同的前缀和后缀了,如果存在,重复上面步骤) 则模式串指针 重新回到模式串的起始位置;
通过以上分析可知:
KMP 算法相比于朴素法的优势在于, 使用KMP时:
- 字符串的指针不会往前回退, 只会不断往后移动;
- 模式串中的指针,回退过程中, 总是先寻找当前位置的子串上是否存在相同的前缀串, 然后回退到相同前缀串的后一个位置, 直到没有相同前缀串后, 模式串的指针才回退到初始位置;
在模式串中, 指针回退到下一个匹配点的位置与字符串无关,
1.2 next数组
具体讲来:
对于模式串 abcabd 的字符 d 而言,由它发起的下一个匹配点跳转必然是字符 c 的位置。
因为字符 d 位置的相同「前缀」和「后缀」字符 ab 的下一位置就是字符 c。
由此可见,在模式串中由某个位置回退到前面的匹配位置, 这个过程是与字符串无关的,我们将这一个过程称为寻找next 点;
所以,第一步我们先生成next 数组, 数组中的每个位置上的数值,代表了相同位置上的模式串中, 当该位置上的元素与字符串上的元素不匹配时, 模式串的指针应该跳转到具有相同前缀串的后一个位置上;
1.2 next数组的生成 ,
生成最大相同前缀后缀表,即构造next 数组的过程;
生成next 数组的关键点:
1. 后缀串的起点指针,用来遍历是模式串的,始终是向后移动的;
2. 判断两个指针对应位置上的元素是否相等, 来对前缀串的终止指针,进行移动;
3. 将
j
j
j 此时的位置 赋值给
n
e
x
t
[
i
]
next[i]
next[i];
根据模式串构造出 next 数组的逻辑步骤:
- 初始化 next[0] = 0;
- 使用后缀串的起点指针 i i i遍历模式串;
- 根据两种情况,移动前缀串指针的位置;
- 处理前缀串 与后缀串 相同的情况, 移动 j j j;
- 处理前缀串 与后缀串 不同的情况, 回退 j j j;
- next[i]赋值: 后缀串起点指针遍历过程中, 将当前移动 j j j 的位置赋值给 next[i] ;
void getNext(int* next, const string& p){
//1. next[0] 规定初始值;
next[0] = 0;
// i 后缀串的起始位置, 不断后移,用来遍历模式串;
// j 前缀串的终止位置, 双向移动, 可回退,并且该数值存放到next[i] 中;
int j = 0;
for(int i = 1; i < p.size(); i++){// 2. 使用后缀串指针遍历 模式串;
// 两个指针对应位置上元素不同时, 跳转前缀串指针;
while( j - 1 > 0 && p[i] != p[j] ) j = next[j - 1];
// 两个指针对应位置上元素相同时, 前缀串指针正常后移;
if( p[i] == p[j] ) j++;
// 赋值 next[i]
next[i] = j;
}
}
1.3 模式串匹配字符串过程
开始模式串匹配字符串,
该过程中,调用上述的next[] 数组;
两个串匹配的关键点:
**1. 字符串的指针用来遍历字符串,始终是向后移动的, 模式串的指针根据情况移动;
2. 判断两个指针对应位置上的元素是否相等, 来对模式串的指针,进行移动;
**
具体步骤:
-
判断模式串 是否为空串; 若是, 返回 0;
-
新建一个 next 数组, 大小为模式串的长度;
-
调用 getNext 函数,生成 next 数组;
-
创建两个指针, i , j i, j i,j 遍历, i i i 遍历字符串, j j j 遍历模式串;
-
开始遍历字符串,该过程中, 字符串指针始终后移;
5.1: 当两个指针对应位置上的元素不同时, 模式串指针,跳转回退;
5.2 :当两个指针对应位置上的元素相同时, 模式串指针向后移动;
5.3: 当模式串到达终止位置时,此时字符串指针位置为 i i i,
返回此时字符串中,匹配模式串的起始位置, 即为 i − p . s i z e + 1 i - p.size + 1 i−p.size+1; -
当遍历字符串结束后, 之前如果没有return, 表明模式串指针没有走到模式串的结尾, 表明 字符串中 没有匹配模式串的 子串, 返回-1; 结束;
1.4 code
class Solution {
public:
void getNext(int* next, const string& p){
//1. next[0] 规定初始值;
next[0] = 0;
// i 后缀串的起始位置, 不断后移,用来遍历模式串;
// j 前缀串的终止位置, 双向移动, 可回退,并且该数值存放到next[i] 中;
int j = 0;
for(int i = 1; i < p.size(); i++){// 2. 使用后缀串指针遍历 模式串;
// 两个指针对应位置上元素不同时, 跳转前缀串指针;
while( j > 0 && p[i] != p[j] ) j = next[j - 1];
// 两个指针对应位置上元素相同时, 前缀串指针正常后移;
if( p[i] == p[j] ) j++;
// 赋值 next[i]
next[i] = j;
}
}
int strStr(string haystack, string needle){
// i 用做字符串指针,始终后移, j 用作模式串指针, 根据情况移动;
int i = 0, j = 0;
// 新建一个数组, 大小为模式串的大小;
int next[needle.size()];
// 2. 调用 生成 next 数组的函数;
getNext(next, needle);
//3. 开始遍历 字符串;
for(int i = 0; i < haystack.size(); i++){
// 当两个指针对应位置上元素不匹配时, 模式串指针回退;
while ( j > 0 && haystack[i] != needle[j]) j = next[j - 1];
// 当两个指针对应位置上元素相同时, 移动模式串指针;
if( haystack[i] == needle[j] ) j++;
// 当模式串指针, 到达模式串的结尾时, 返回字符串中的匹配起始位置;
if( j == needle.size()) return (i - needle.size() + 1);
}
// 如果之前没有返回, 表明模式串没有走到最后,匹配没有完成;
return -1;
}
};
2. 字符串中的重复子串判断
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例 1:
输入: “abab”
输出: True
解释: 可由子字符串 “ab” 重复两次构成。
示例 2:
输入: “aba”
输出: False
2.1 问题分析
-
直接对字符串自身, 构造next 数组(next数组长度 = 字符串长度);
如果该 next 数组的 最后一个位置上的 数值 不为0, 表明该 字符串中 存在最大相同前后缀;最大相同前后缀的长度 = 数组最后一个位置上的数值 = ;
该最大相同前后缀代表的是: 字符串中减掉第一个周期重复串后的长度;
所以 第一个周期的重复串长度 = 字符串长度 - 最大相同前后缀;
- 如果最大相同前后缀长度不为0 , 并且 字符串 能够被 第一周期重复串整除,表明该字符串可以由 重复串 构成;
2.2 code
class Solution {
public:
void getNext(int* next, const string& p) {
// i, 后缀串的起点指针,方向,单向后移; j 前缀穿的终止指针, 方向是双向的;
int j = 0;
next[0] = 0;
// 开始遍历模式串,生成next 数组;
for (int i = 1; i < p.size(); i++) {
// 循环, 当两指针对应位置上元素不同时, 前缀串指针 回退;
while (p[i] != p[j] && j > 0) j = next[j - 1];
// 条件, 如果两个指针对应位置上元素相同时, 前缀穿指针后移;
if (p[i] == p[j]) j++;
// 将 j 的位置赋值给 next[i]
next[i] = j;
}
}
bool repeatedSubstringPattern(string s) {
// 准备next 数组
int next[s.size()];
getNext(next, s);
// 最大相同前缀后缀长度 = next 数组中最后一个位置上数值;
// 第一个重复周期的子串长度 = 字符串长度 - 最大相同前后缀长度;
int maxLR = next[s.size() - 1];
int period1 = s.size() - maxLR;
// 如果最大相同前后缀长度不为0 ,且字符串长度能够被周期串长度 整除, 表明该字符串可以有 周期串 构成;
if ( maxLR != 0 && s.size()% period1 == 0) return true;
else return false;
}
};