KMP算法实现过程及含义理解
1. KMP算法的用处
在字符串的模式匹配中,朴素算法寻找子串首次出现位置会出现回溯过程,为了降低时间复杂度,引入KMP算法。KMP算法是通过移动模式串来跟主串进行匹配的,因此时间复杂度是线性的,没有回溯的过程。
2. KMP算法的实现
(1)先附上KMP主算法的代码:
int KMP(string s, string t, int pos, int next[]{
//s为主串,t为模式串,pos为开始查找位置,next数组保存模式串每个字符失配时应该重新开始的位置
//pos可以从0开始
int i = pos, j = 0;
int sLen = s.length(), tLen = t.length();
while(i < sLen && j < tLen){
if(j == -1 || next[i] == next[j]){
i++;
j++;
}else{
j = next[j];
}
}
if(j < tLen){//说明模式串并没有匹配成功
return -1;
}
//返回下标位置
return i - j;
}
KMP算法的查找程序不难理解,主要思想是:当模式串中的字符与主串的相同时,两个指针同时往后移动一位;当不匹配时,主串的指针不回溯,而是通过右移模式串,调整模式串的指针去跟主串进行比较。因此,知道模式串在某个位置失配时,应该怎么调整指针是KMP算法的核心所在。next数组就是用来存放模式串每个位置失配时指针应该调整到哪个位置的信息的。
(2)next数组的求法
- 首先给出一个例子:
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
字符串 | a | b | a | a | b | c |
next | -1 | 0 | 0 | 1 | 1 | 2 |
这里的思想是:在模式串中再进行模式匹配。初始化next[ 0 ] = -1作为标志。用指针 i 去遍历模式串,在t[ i ]位置时,我们去看前一个字符,即t[i - 1],看它是否等于以它的next值为下标的字符,即t[i - 1] == t[ next[i - 1] ]。如果相等或者next[ i - 1]为-1,那么next[ i ] = next[i - 1] + 1,指针往后;如果不等,我们用另外一个指针k去记录next[i - 1](当前与t[i - 1]比较的字符下标),再让k = next[ k ],也就是让t[i - 1]字符继续往前与t[ k ]比较,直到找到跟自己相等的字符或者到k为-1(到头)为止。其实就是比较出每个字符的结果,进而填写它后面一个字符的next值。
- 附上实现代码:
void getNext(string t, int next[]){
int len = t.length();
int i = 0, k = -1;
while(i < len - 1){//防止越界
if(k == -1 || t[i] == t[k]){
i++;
k++;
next[i] = k;
}else{
k = next[k];
}
}
}
3. next数组的理解
(1)最基本的含义就是:在模式串第i位字符失配时,模式串应该右移到什么位置(指针调整到什么位置),再继续与当前失配位置的主串继续匹配。
(2)next数组每一位的值表示在此之前重复的子串长度。
- 举个例子,这里我们把next数组扩大一位,同时修改一下求next数组的函数。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
字符串 | a | b | c | a | b | c | |
next | -1 | 0 | 0 | 0 | 1 | 2 | 3 |
void getNext(string t){
int len = t.length();
int *next = new int[len + 1];
int i = 0, k = -1;
while(i < len){
if(k == -1 || t[i] == t[k]){
i++;
k++;
next[i] = k;
}else{
k = next[k];
}
}
}
那么next[ 6 ]在这里代表的是什么意思呢? next [ 6 ] = 3说明在此之前已经有长度为3的子串跟模式串长度为3的前缀完全相等了。也就是从下标为3开始的“abc”与从下标为0开始的“abc”(即前缀)相等。
- 明白这个含义用处是非常大的,将next数组扩大一位,可以用来解决求字符串最大重复子串、子串循环等问题。
- 进一步讨论:实际上,next[ i ]在表示当前位置前面重复的最长子串时有三种情况:
(1)next[ i ] = 0,说明该位置之前没有与前缀相等的子串,即截止当前位置的前一个字符所形成的字符串中没有重复的子串。例子如下:
下标 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
字符串 | a | b | c | d | |
next | -1 | 0 | 0 | 0 | 0 |
next[4]=0表示在此之前的字符串无重复子串。
(2)next[ i ] = s.length() - 1,说明整个字符串为同一个字符组成。例子如下:
下标 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
字符串 | a | a | a | a | |
next | -1 | 0 | 1 | 2 | 3 |
next[4]=3说明字符串由单个字符构成。
(3)next[ i ] >= s.length() / 2,说明在当前位置之前有重复子串,并且next值的计算出现重叠。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
字符串 | a | b | c | a | b | c | a | b | c | |
next | -1 | 0 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
首先普及一个概念:最小循环节,是一个字符串中出现重复的最短的子串,计算方法是将next数组扩大一位,min_circle = len - next[ len ]。
在本例中min_circle = “abc”,长度为3。而next[9]=6,说明在求的时候计算重复子串出现了重叠部分,多算进去了一些。另外,如果字符串总长度len % min_circle == 0,则说明整个字符串是由最小循环节重复构成的;min_circle == len,则说明字符串没有重复部分。