数据结构——KMP算法
KMP算法
BF算法因主串指针频繁回溯,在最坏情况下效率较低。KMP算法通过预处理模式串,利用其前缀后缀的重叠特性,避免主串指针回溯,大幅提升匹配效率,是字符串模式匹配的经典优化算法。
1. KMP算法的核心思想
KMP算法的核心是消除主串指针的回溯。当模式串与主串某位置匹配失败时,不是将主串指针回退到“上一次起始位置的下一个”,而是通过分析模式串的“前缀-后缀重叠信息”,直接将模式串指针跳转到合适的位置,继续与主串当前位置的字符比较。
这里的“前缀-后缀重叠”指:模式串中某子串的“前缀”(从第一个字符开始的子串)和“后缀”(以最后一个字符结尾的子串)存在相等的部分。例如,模式串“abab”中,子串“aba”的前缀“a”和后缀“a”相等,长度为1;子串“ab”的前缀“a”和后缀“b”不相等,长度为0。这种重叠信息可用于匹配失败时的指针跳转,避免主串指针重复比较。
2. next数组的定义与计算
为了实现指针的智能跳转,KMP算法引入next数组:对于模式串的第jjj个字符(从0开始),next[j]next[j]next[j]表示“模式串前jjj个字符组成的子串的最长相等前缀后缀长度”。
(1)next数组的定义示例
以模式串T=T =T=“ababc”为例,计算每个位置的next[j]next[j]next[j],结果如下表:
| 模式串字符位置 jjj | 0(‘a’) | 1(‘b’) | 2(‘a’) | 3(‘b’) | 4(‘c’) |
|---|---|---|---|---|---|
| next[j]next[j]next[j] | 0 | 0 | 1 | 2 | 0 |
- j=0j=0j=0:子串只有一个字符,无前后缀,next[0]=0next[0]=0next[0]=0;
- j=1j=1j=1:子串“ab”,前缀“a”和后缀“b”不相等,next[1]=0next[1]=0next[1]=0;
- j=2j=2j=2:子串“aba”,前缀“a”和后缀“a”相等,长度1,next[2]=1next[2]=1next[2]=1;
- j=3j=3j=3:子串“abab”,前缀“ab”和后缀“ab”相等,长度2,next[3]=2next[3]=2next[3]=2;
- j=4j=4j=4:子串“ababc”,无相等的前缀后缀,next[4]=0next[4]=0next[4]=0。
(2)next数组的计算方法
计算nextnextnext数组可通过动态规划实现,核心逻辑是“利用已计算的nextnextnext值推导当前next[j]next[j]next[j]”:
- 初始化next[0]=0next[0] = 0next[0]=0,定义指针i=0i=0i=0(前缀指针)、j=1j=1j=1(后缀指针);
- 若T[i]==T[j]T[i] == T[j]T[i]==T[j]:说明前缀后缀可延长,next[j]=i+1next[j] = i+1next[j]=i+1,i++i++i++,j++j++j++;
- 若T[i]!=T[j]T[i] != T[j]T[i]!=T[j]:若i>0i > 0i>0,则i=next[i−1]i = next[i-1]i=next[i−1](回退到前一个位置的最长前缀后缀长度);否则next[j]=0next[j] = 0next[j]=0,j++j++j++。
以下是计算nextnextnext数组的C语言代码(嵌入KMP算法中):
// 计算模式串t的next数组
void getNext(char t[], int next[], int m) {int i = 0, j = 1;next[0] = 0;while (j < m) {if (t[i] == t[j]) {next[j] = i + 1;i++;j++;} else {if (i > 0) i = next[i - 1];else {next[j] = 0;j++;}}}
}
3. KMP算法的匹配过程
KMP的匹配过程基于nextnextnext数组,主串指针iii始终不回溯,模式串指针jjj根据next[j]next[j]next[j]跳转,步骤如下:
- 初始化i=0i=0i=0(主串指针)、j=0j=0j=0(模式串指针);
- 比较S[i]S[i]S[i]和T[j]T[j]T[j]:
- 若相等:i++i++i++,j++j++j++,继续比较下一个字符;
- 若不相等:若j>0j > 0j>0,则j=next[j−1]j = next[j-1]j=next[j−1](根据next数组跳转);否则i++i++i++(主串指针后移,模式串从头开始);
- 重复上述步骤,直到j==mj == mj==m(匹配成功,返回i−mi - mi−m)或i==ni == ni==n(匹配失败,返回-1)。
以“主串S=S =S=‘ababcabcacbab’,模式串T=T =T=‘abcac’(nextnextnext数组为[0,0,1,0,2])”为例:
- 初始i=0,j=0i=0, j=0i=0,j=0:S[0]=′a′==T[0]=′a′S[0]='a' == T[0]='a'S[0]=′a′==T[0]=′a′,i=1,j=1i=1, j=1i=1,j=1;
- S[1]=′b′==T[1]=′b′S[1]='b' == T[1]='b'S[1]=′b′==T[1]=′b′,i=2,j=2i=2, j=2i=2,j=2;
- S[2]=′a′!=T[2]=′c′S[2]='a' != T[2]='c'S[2]=′a′!=T[2]=′c′,j=next[1]=0j = next[1] = 0j=next[1]=0;
- S[2]=′a′==T[0]=′a′S[2]='a' == T[0]='a'S[2]=′a′==T[0]=′a′,i=3,j=1i=3, j=1i=3,j=1;
- S[3]=′b′==T[1]=′b′S[3]='b' == T[1]='b'S[3]=′b′==T[1]=′b′,i=4,j=2i=4, j=2i=4,j=2;
- S[4]=′c′==T[2]=′c′S[4]='c' == T[2]='c'S[4]=′c′==T[2]=′c′,i=5,j=3i=5, j=3i=5,j=3;
- S[5]=′a′!=T[3]=′a′S[5]='a' != T[3]='a'S[5]=′a′!=T[3]=′a′? 纠正,模式串“abcac”的第3个字符是‘a’,S[5]=′a′==T[3]=′a′S[5]='a' == T[3]='a'S[5]=′a′==T[3]=′a′,i=6,j=4i=6, j=4i=6,j=4;
- S[6]=′c′==T[4]=′c′S[6]='c' == T[4]='c'S[6]=′c′==T[4]=′c′,j=5==m=5j=5 == m=5j=5==m=5,匹配成功,返回i−m=6−5=1i - m = 6 - 5 = 1i−m=6−5=1(实际起始位置需根据示例调整,核心是跳转逻辑)。
4. KMP算法的代码实现
以下是KMP算法的完整C语言实现,包含nextnextnext数组计算和匹配过程:
// KMP算法:s为主串,t为模式串,n为主串长度,m为模式串长度
int KMP(char s[], char t[], int n, int m) {int next[m];getNext(t, next, m); // 计算next数组int i = 0, j = 0;while (i < n && j < m) {if (s[i] == t[j]) { // 字符相等,继续i++;j++;} else {if (j > 0) j = next[j - 1]; // 模式串指针跳转else i++; // 主串指针后移}}return j == m ? i - m : -1; // 匹配成功返回起始位置,否则返回-1
}// 计算next数组(辅助函数)
void getNext(char t[], int next[], int m) {int i = 0, j = 1;next[0] = 0;while (j < m) {if (t[i] == t[j]) {next[j] = i + 1;i++;j++;} else {if (i > 0) i = next[i - 1];else {next[j] = 0;j++;}}}
}
5. KMP算法的性能分析
KMP算法的时间复杂度由两部分组成:计算nextnextnext数组的时间O(m)O(m)O(m)(mmm为模式串长度),以及匹配过程的时间O(n)O(n)O(n)(nnn为主串长度)。因此,整体时间复杂度为O(n+m)O(n + m)O(n+m),远优于BF算法的最坏情况O(n×m)O(n \times m)O(n×m)。
例如,当主串长度n=106n=10^6n=106,模式串长度m=103m=10^3m=103时,KMP算法只需约106+10310^6 + 10^3106+103次操作,而BF算法最坏可能需要10910^9109次操作,效率差距显著。
综上,KMP算法通过预处理模式串得到nextnextnext数组,消除了主串指针的回溯,实现了线性时间复杂度的字符串模式匹配。其核心是对“前缀-后缀重叠信息”的利用,这一思想不仅解决了字符串匹配问题,也为其他领域的算法优化提供了启发。理解KMP的nextnextnext数组计算和匹配逻辑,是掌握高级字符串处理技术的关键。
