KMP算法
暴力法
字符串的模式匹配,首先想到的应该是暴力法,如下代码
static int32_t violence_matching(const string &matching_str, const string &pattern) { int32_t m_len = matching_str.length(); //被匹配串的长度 int32_t p_len = pattern.length(); //模式串的长度 if (p_len == 0 || m_len == 0 || p_len > m_len) { //模式串或被匹配串的长度为空,或者模式串的长度大于被匹配串的长度,很定匹配不到 return -1; //返回-1表示没匹配到 } for (int32_t m_index = 0, p_index = 0; m_index < m_len;) { int32_t temp_index = m_index; while (matching_str[m_index] == pattern[p_index] && m_index < m_len && p_index < p_len) { ++m_index; ++p_index; } if (p_index) { //如果p_index等于模式串的长度,表示匹配到了 return temp_index; } p_index = 0; //否则,p_index置0,m_index加1,重新开始匹配 ++m_index; } return -1; }
很显然,暴力法时间复杂度为O(p_len*m_len)。
最长相同前后缀子串
先引入一个概念,最长相同前后缀子串,如字符串abcabc,其前缀子串为[a,ab,abc,abca,abcab],后缀子串为[c,bc,abc,cabc,bcabc],那么其最长相同前后缀子串就是abc。
优化思考
假设在暴力匹配算法中我们不回溯m_index,而只通过移动p_index的方式,并且每次移动的次数是常数次,跟p_len不相关,那么时间复杂度就变成了O(m_len + 计算移动方式的时间复杂度),而焦点也就变成了计算移动方式上。
我们观察如下匹配:

如果D不匹配,那么我们要怎么移动模式串达到m_index不回溯的目的,直观的想法是将“C”与当前的m_index对其,因为“C”和“D”前面都有“AB”,而“AB”是“ABCDAB”的最长相同前后缀子串,首先这种按最长相同前后缀子串移动是正确的,可以从两个方面说明其正确性:
(1)不会出现不匹配的现象,这是很显然的;
(2)不会漏掉匹配,假设有漏掉的匹配,那么就会出现比“AB”更长的相同前后缀子串,就与“最长”的定义冲突了
那么,我们的焦点又一次转移了,变成了计算模式串和其所有前缀子串的最长相同前后缀子串,以“ABCDABD”为例,即计算[A,AB,ABC,ABCD,ABCDA,ABCDAB,ABCDABD]的最长相同前后缀子串,用f[index]表示(即部分匹配表)。
计算f[index]
首先想到的还是暴力法,即穷举出所有的前后缀,然后计算最长相同前后缀子串,如下代码:
static uint32_t get_max_same_prefix_suffix(const string &str) { //取得str最长相同前后缀子串 if (str.length() == 0) { return 0; } vector<string> prefix; vector<string> suffix; for (uint32_t i = 1; i < str.length(); ++i) { //字符串的前缀 prefix.push_back(str.substr(0, i)); } for (uint32_t i = 1; i < str.length(); ++i) { //字符串的后缀 suffix.push_back(str.substr(i)); } uint32_t max_index = 0; for (auto p_it = prefix.begin(); p_it != prefix.end(); ++p_it) { //取得最大相同前后缀子串 for (auto s_it = suffix.begin(); s_it != suffix.end(); ++s_it) { if (*s_it == *p_it && (*s_it).length() > max_index) { max_index = (*s_it).length(); } } } return max_index; } static void get_f_index(const string &pattern, vector<uint32_t> &f_index) { //得到f_index for (uint32_t i = 1; i <= pattern.length(); ++i) { string temp_str = pattern.substr(0, i); f_index.push_back(get_max_same_prefix_suffix(temp_str)); } }
很明显,这种计算时间复杂度的方法是O(p_len2),复杂度太高。
优化计算f[index]
利用已经计算出来的f[index],这分两种情况(p为模式串):
(1)如果p[k] = p[f[k - 1]](这里的f[k - 1]其实是f[k-1] - 1 + 1,因为f[k - 1]是数量,对应index需要减1,而下一个字母又需要加1),那么f[k] = f[k - 1] + 1,
(2)如果p[k] != p[f[k - 1]], 那么我们需要寻找p[0]....p[k - 1]的第二长相同前后缀子串,其必然是最长相同前后缀子串的最长相同前后缀子串,即p[0]...p[f[f[k - 1] - 1] - 1],如果这时有p[k] = p[f[f[k - 1] - 1]],那么f[k] = f[f[k - 1] - 1] + 1,如果p[k] != p[f[f[k - 1] - 1]],依次类推
例如
对于模式串abcabdabcabc,当我们计算f[10]时,因为p[10] = p[f[9]],所以f[10] = f[9] + 1 = 5
当我们计算f[11]时,首先f[10] = 5, 因为p[11] != p[5], 而有p[11] = p[f[f10] - 1]] = p[2], 所以f[11] = f[f[10] - 1] + 1 = f[4] + 1 = 3
翻译成代码:
static void get_f_index(const string &pattern, vector<uint32_t> &f_index) { uint32_t p_len = pattern.length(); f_index.push_back(0); //f_index[0]为0 for (uint32_t i = 1, k = 0; i < p_len; ++i) { while (k > 0 && pattern[k] != pattern[i]) { k = f_index[k-1]; } if (pattern[k] == pattern[i]) { ++k; } f_index.push_back(k); } }
使用摊还分析来分析这里的时间复杂度,k的值只有在++k这里增加,只有在k = f_index[k - 1]这里减少,k++在整个for循环中最多执行p_len次,因为k > 0,所以k = f_index[k - 1]也最多执行p_len次,也就是说均摊到每次循环的时间复杂度为O(p_len)/p_len = O(1),那么整个计算的时间复杂度就是O(p_len)。
利用f[index]的匹配算法
有了f[index]我们就可以写出匹配算法了,如果不匹配时,可以移动p_index = f_index[p_index - 1],代码如下:
static int32_t findex_matching(const string &matching_str, const string &pattern) { int32_t m_len = matching_str.length(); int32_t p_len = pattern.length(); vector<uint32_t> f_index; get_f_index(pattern, f_index); if (p_len == 0 || m_len == 0 || p_len > m_len) { //模式串或被匹配串的长度为空,或者模式串的长度大于被匹配串的长度,很定匹配不到 return -1; //返回-1表示没匹配到 } for (int32_t m_index = 0, p_index = 0; m_index < m_len && p_index < p_len; ++m_index) { while (matching_str[m_index] != pattern[p_index] && p_index != 0) { p_index = f_index[p_index - 1]; //只需要移动p_index即可,这个下面有用:) } if (matching_str[m_index] == pattern[p_index]) { ++p_index; if (p_index == p_len) { //匹配完成 return m_index - p_index + 1; } } } return -1; }
匹配的时间复杂度同样可以用摊还分析计算出来,为O(m_len),所以完整匹配算法的时间复杂度为O(m_len + p_len),从正确性来说f_index已经足够了,但还可以继续优化。
转化f_index
f_index就是KMP中的部分匹配表,从代码中可以看出,p_index跳转等于f_index[p_index - 1],根据这个规律,我们可以构造一个临时跳转表temp_next,为啥叫临时跳转表,因为后面还有优化空间,优化后的才是最终跳转表。
temp_next[p_index] = f_index[p_index - 1],代码如下:
static void get_temp_next(const string &pattern, vector<uint32_t> &temp_next) { uint32_t p_len = pattern.length(); temp_next.push_back(0); //temp_next[0]为0 temp_next.push_back(0); //temp_next[1]为0 for (uint32_t i = 1, k = 0; i < p_len - 1; ++i) { //计算到p_len - 2就可以了 while (k > 0 && pattern[k] != pattern[i]) { k = temp_next[k-1]; } if (pattern[k] == pattern[i]) { ++k; } temp_next.push_back(k); } }
如果使用temp_next,那么跳转的时候就是p_index = temp_next[p_index]。
temp_next优化
在跳转时,如果p[p_index] = p[temp_next[p_index]]时,很显然这个跳转是无效的,因为不等于p[p_index]自然也就不等于p[temp_next[p_index]],这时我们通过temp_next进一步优化得到next,代码如下:
static void get_next_from_temp_next(const string &pattern, const vector<uint32_t> &temp_next, vector<uint32_t> &next) { uint32_t p_len = pattern.length(); for (uint32_t i = 0; i < temp_next.size(); ++i) { uint32_t next_elem = temp_next[i]; while (pattern[i] == pattern[next_elem] && next_elem != 0) { next_elem = temp_next[next_elem]; } next.push_back(next_elem); } }
参考文档
- 原版论文《Fast Pattern Matching In Strings》,有兴趣的可以啃啃
- KMP算法详解

浙公网安备 33010602011771号