一、算法介绍
KMP算法,全称Knuth-Morris-Pratt算法,是一种线性时间复杂度的字符串匹配算法。该算法由D.E.Knuth、J.H.Morris和V.R.Pratt提出,因此也称为克努特—莫里斯—普拉特操作。它主要用于在一个较长的字符串(称为主串或目标串)中查找一个较短的字符串(称为子串或模式串)的位置。
核心思想
KMP算法的核心思想是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数,以达到快速匹配的目的。
与朴素的字符串匹配算法(即每次匹配失败时,主串和模式串的指针都会回溯)不同,KMP算法在匹配失败时,主串的指针不回溯,仅通过移动模式串的指针,并借助一个预处理得到的数组(称为next数组或部分匹配表)来决定下一个匹配的位置,从而提高了匹配效率。
next数组
- 定义:next数组是一个与模式串等长的数组,用于存储模式串中每个位置之前的子串的最长相同前后缀长度。具体地,对于模式串中的每个位置i(从1开始计数),next[i]表示以i结尾的前缀子串的最长相同前后缀长度。
- 构建方法:遍历模式串,对于每个位置i,计算以i结尾的前缀子串的最长相同前后缀长度。这通常通过比较前缀子串的后缀和之前已经计算过的前缀子串的前缀来实现。
- 作用:在匹配过程中,当模式串的某个位置与主串不匹配时,可以根据next数组直接跳转到模式串的下一个可能匹配的位置,而无需像朴素算法那样回溯主串和模式串的指针。
算法步骤
- 预处理:构建模式串的next数组。
- 匹配过程:
- 初始化两个指针i和j,分别指向主串和模式串的起始位置。
- 遍历主串,对于每个位置i:
- 如果text[i]等于pattern[j],则i和j同时向右移动一格。
- 如果text[i]不等于pattern[j],则根据next数组将j回退到下一个可能匹配的位置(即j=next[j-1],如果j>0;否则i向右移动一格,j保持为0)。
- 如果j等于模式串的长度,则说明匹配成功,返回匹配起始位置(即i-j)。
- 如果遍历完主串仍未找到匹配,则返回-1表示未找到。
时间复杂度和空间复杂度
- 时间复杂度:KMP算法的时间复杂度为O(m+n),其中m是主串的长度,n是模式串的长度。这是因为KMP算法只需遍历主串和模式串各一次,并且在发生不匹配时能够利用next数组进行高效的跳转,从而避免了不必要的比较操作。(朴素模式匹配算法的时间复杂度:O(m*n))
- 空间复杂度:KMP算法需要额外的空间来存储next数组,其空间复杂度为O(n)。
应用场景
KMP算法在实际应用中具有广泛的用途,特别是在文本搜索、生物信息学、网络爬虫等领域。例如,在文本编辑器中实现查找功能、在DNA序列分析中查找特定的基因序列等场景都可以使用KMP算法来提高效率。
综上所述,KMP算法是一种高效且实用的字符串匹配算法,它通过预处理模式串和构建next数组来优化匹配过程,减少了不必要的比较次数,从而提高了匹配效率。
二、算法原理
2.1 核心思想
【天勤考研】KMP算法易懂版_哔哩哔哩_bilibili
人类语言:当出现不匹配项时,直接移动将模式串已匹配子串的公共前缀移动到公共后缀的位置,目标串指针不回溯,继续向后比较。
注意:这里是最长公共前后缀,且长度小于已匹配子串的长度(必须是两个不同位置的串,否则没法移动)。
机器语言:当出现不匹配项时,假设匹配子串的公共前后缀的长度为n,就从模式串的n+1号位开始继续比较。
因此我们需要对模式串进行预处理,找到每个位置之前的公共前后缀的长度+1(移动后的比较位置),这就是next数组,next是下一个比较位置的意思。
使用next数组:
如果模式串的n号位不匹配,对应next数组的下标,模式串指针就跳转到next数组n号位标记的下标处与主串当前位继续比较
- 1号位不匹配,主串指针i++
- n号位不匹配(n>1),模式串指针j = next[j](如果下标从0开始不需要+1)
注意:这里的数组下标是从1开始的,在实际代码中,下标从0开始,next[j] = j位置之前的公共前后缀的长度(不需要+1)
2.2 如何求next数组?
KMP算法之求next数组代码讲解_哔哩哔哩_bilibili
- 如果模式串下标从0开始,next[0]初始化为-1,next[j] = j位置之前的公共前后缀的长度;
- 如果模式串下标从1开始,next[1]初始化为0,next[j] = j位置之前的公共前后缀的长度 + 1;
提示:next首位置的初始值并无实际意义,只是一个标记值,表示首位置与主串下一位比较。
-
首先要知道求next[j]要看的是j位置之前的最长公共前后缀,与j位置的值无关
-
类似于动态规划算法,我们可以利用之前位置的next值求当前位置
-
令k = next[j-1],如果str[k] == str[j-1],那么next[j] = k+1
-
但是如果不匹配,就需要再找公共前后缀的公共前后缀了,比较最前缀和最后缀的末尾值
-
即令k = next[k],如果str[k] == str[j-1],那么next[j] = k+1
-
如果还不匹配,就重复上述步骤继续递推,直到k==0(首位置next越界)则递推结束。此时的next[j] = 1(首位置),表示前j-1位的前后缀中没有一位是重合的是最不理想的情况,如果j位置不匹配则模式串回溯到1号位与主串当前位置比较。
三、代码实现
完整代码:mystring · 52Hertz_Echo/CPP_Practice - 码云 - 开源中国 (gitee.com)
MYstring::Find_KMP
size_t Find_KMP(const Mystring &substr, size_t pos = 0) const{// 预处理模式串,得到对应的next数组int m = _size;int n = substr.size();int next[n];substr.GetNext(next);// Debug:打印next数组进行检查// for (auto e : next)// cout << e << " ";// cout << endl;// 利用next数组进行KMP模式匹配int i = pos, j = 0;while (i < m && j < n){if (j == -1 || _str[i] == substr[j]) //首位置不匹配 || 当前位置匹配{++j;++i;}else{j = next[j];}}if (j == n)return i - n;elsereturn -1;}
Mystring::GetNext
void GetNext(int next[]) const{next[0] = -1; int k = -1; // k = next[i-1]int i = 1;while (i < _size){if (k == -1 || _str[i - 1] == _str[k]){next[i] = k + 1;++k; // k = next[i-1]++i;}else{k = next[k];}}}
Mystring::Find_BF
size_t Find_BF(const Mystring &substr, size_t pos = 0) const{size_t n = substr.size();for (size_t i = pos; i < _size; ++i){if (_str[i] == substr[0]){size_t j = 1;while(j < n && _str[i + j] == substr[j]){++j;}if (j == n){return i;}}}return -1;}