赞
踩
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度为O(m+n) 。
目录
在暴力做法中,一旦发现模式串与主串中字符匹配失败,i指针就回退到位置2,j指针回退到位置1,然后再做比较,这种做法时间复杂度为O(m*n),缺点在于浪费了前面已经匹配的信息。
让i指针原地不动,j指针回跳到恰当的位置再做比较,恰当的位置即从前面已经匹配的信息当中,再找出仍然匹配的一段。具体实现就是通过next()函数来实现。函数本身包括了模式串的局部匹配信息。稍后讲解如何构造next函数。
在此便可以体会到KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
第一轮匹配失败后,
从S串中去掉一个字符往后看,不可行,因为ab和aa不匹配;
从S串中去掉两个字符往后看,不可行,因为b和a不匹配;
从S串中去掉三个字符往后看,发现后面的aabaa与模式串中的前五个字符aabaa相等,这样j指针只需要回跳到位置6,再和主串进行比较即可,这是方案一;
还有另一种方案,继续采用从主串中依次删去字符的方法,从S串中去掉六个字符往后看(去掉四个和五个匹配失败),发现后面的aa和模式串中的前两个字符aa相等,这样j指针需要回跳到位置3,再与主串进行比较。
比较这两种方案,很明显方案一更好,因为用贪心的思想考虑,方案一中找到的相等的匹配段长度更长,越长起始位置就越靠左,如果这个位置后面部分恰好等于模式串,那这就是一组可行解。即使这一位置后面部分与模式串不匹配,那再采用从主串中依次删去字符的方法,在从S串中去掉四个和五个字符匹配失败后,去掉六个字符(即方案二),发现后面的aa和模式串中的前两个字符aa相等,这样j指针需要回跳到位置3,再与主串进行比较。这样的做法就能保证不漏解。
通过比较我们发现,这样的结论:
1.取最长的相等前后缀,可以保证不漏解。
2.通过模式串前后缀的自我匹配的长度,计算next函数,给j指针打一张表,匹配失败时就跳到next[j]的位置继续匹配。
1.前缀和后缀:例如:p = "abcab",这里考虑的前缀和后缀都是真前缀/真后缀(前后缀长度不能等于模式串自身长度),其前缀为从首字符开始不包含尾字符的子串,即"a","ab","abc","abca",后缀为从尾字符开始不包含首字符的子串,即"b","ab","cab","bcab"
2.在找最长相等前后缀时,完全可以把主串抛开,因为在匹配失败前,主串和模式串相等,那么如果能在主串中找到一段和模式串中前缀相等的匹配段,则一定也能找到与模式串前缀相等的模式串后缀。因此可以通过模式串前后缀的自我匹配的长度,来计算next函数,给j指针打一张表,当主串与模式串在任何位置匹配失败时,j指针都能回调到next[j]的位置继续匹配。
next函数:next[i]表示模式串P[1,i]中相等前后缀的最长长度。
暴力模拟一遍:
右侧数字为需要枚举的次数,加和算得暴力解法的时间复杂度为O(n²),时间复杂度太高还需要再进行优化。
仔细观察,发现规律;
1.next函数值如果增加,那么它只能在上一步的基础上至多增加1;
2.next函数值如果减少,例:next[8] = 5,当增加一个字符至next[9]时,发现匹配失败,那就可以从前面匹配成功的五个字符中,再找出一段相等前后缀,aabaa中最长相等前后缀为aa,因此可以利用前面的next[5] = 2来计算next[9]。
这样无论是增加还是减少都能找到规律,那么就可以进行优化。
- ne[1] = 0;
- for (int i = 2, j = 0; i <= n; i ++ )//n为模式串长度
- {
- while (j && p[i] != p[j + 1]) j = ne[j];//预判发现匹配失败,j就回跳到能匹配的位置,如果找不到能匹配的位置,j就回跳到0,退出while循环
- if (p[i] == p[j + 1]) j ++;//匹配成功,j指针后移一位,指向匹配前缀的末尾
- ne[i] = j;
- }
双指针:i 扫描模式串, j扫描前缀。
初始化,ne[1] = 0, i = 2, j = 0;
每轮for循环,i向后走一步。
1.若p[i] != p[j + 1],j 就回跳到能匹配的位置,如果找不到能匹配的位置,j就回跳到0,退出while循环。
2.若p[i] == p[j + 1],让 j + 1,j指针后移一位,指向匹配前缀的末尾。
3.next[i] 等于 j 的值。
*j反映 j 指针回跳的情况。
不带*反映i指针和j指针每轮循环后最后移动到的位置。
利用p[i] 与p[j + 1]作比较,就是做一个预判,如果匹配,j指针就向右移动一位(进可攻),如果不匹配,就回跳到能匹配的位置(退可守),如果找不到能匹配的位置(退无可退),j 回跳到0,并退出while循环。
回跳:退而求其次,类似于套娃。
用以往匹配失败的信息,构造next()函数,尽量减少j指针回退的步数,从而减少模式串与主串的匹配次数。
主要考虑while循环次数,即j指针走了多少步。j指针所走的总步数就决定了总的执行次数。每轮for,j至多+1,那么j一共向右至多走n步,往左跳的步数加起来最多不会超过n步,否则j变为负数,故j的总步数不会超过2n。例:模式串为aaa.....ab(长度为n),i扫描模式串,j扫描前缀,i = 2, j= 1,i = 3, j = 2,....i指向n, j指向倒数第三个字符a,j != 0, p[i] != p[j + 1],j = ne[j],进入while循环从倒数第三个位置一步一步往回跳,这样计算j的总步数也不会超过2n.因此时间复杂度为O(n).
此过程与求next函数过程很相似,可以结合记忆。
- for (int i = 1, j = 0; i <= m; i ++)//m为主串长度
- {
- while (j && s[i] != p[j + 1]) j = ne[j];
- if (s[i] == p[j + 1]) j ++;
- if (j == n) printf("%d\n", i - n + 1);
- }
双指针:i 扫描主串, j扫描模式串。
初始化,i = 1 , j = 0;
每轮for循环,i向后走一步。
1.若s[i] != p[j + 1],j 就回跳到能匹配的位置,如果找不到能匹配的位置,j就回跳到0,退出while循环。
2.若s[i] == p[j + 1],让 j + 1,j指针后移一位。
3.若匹配成功,输出匹配位置。//如果要输出所有匹配位置,此处还需要再回跳,即加一行 j = ne[j];
*j反映 j 指针回跳的情况。
不带*反映i指针和j指针每轮循环后最后移动到的位置。
主要考虑while循环次数,即j指针走了多少步。j指针所走的总步数就决定了总的执行次数。每轮for,j至多+1,那么j一共向右至多走m步,往左跳的步数加起来最多不会超过m步,否则j变为负数,故j的总步数最多为2m。例:主串为aaa.....a(长度为n),模式串为ab,i扫描主串,j扫描模式串,i = 1, j = 1; i = 2, j = 0;i = 3, j = 1; i = 4, j = 0,.....j指针随着i指针向前走一步,再往后跳一步,循环往复,所以时间复杂度为O(m).
1.next[i] 表示模式串p[1, i]中相等前后缀的长度。
2.双指针 i指针不回退, j指针来回跑。
3.时间复杂度为:O(m +n)。
给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串 P 在模式串 S 中多次作为子串出现。
求出模板串 P 在模式串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1≤N≤1e5
1≤M≤1e6
输入样例:
- 3
- aba
- 5
- ababa
输出样例:
0 2
答案代码:
- #include <iostream>
- #include <stdio.h>
-
- using namespace std;
-
- const int N = 1e5 + 10, M = 1e6 + 10;
-
- int n, m;
- int ne[N];
- char p[N], s[M];
-
-
- //时间复杂度为O(m + n)
- int main()
- {
- cin >> n >> p + 1 >> m >> s + 1;//这样输入可以使字符串下标从1开始,便于执行操作
-
- //求next数组过程
- for (int i = 2, j = 0; i <= n; i ++ )//next[1] = 0;直接从2开始
- {
- while (j && p[i] != p[j + 1]) j = ne[j];
- if (p[i] == p[j + 1]) j ++;
- ne[i] = j;
- }
-
- //kmp匹配过程
- for (int i = 1, j = 0; i <= m; i ++ )
- {
- while (j && s[i] != p[j + 1]) j = ne[j];
- if (s[i] == p[j + 1]) j ++;
- if (j == n)
- {
- printf("%d ", i - n);//模板中是i - n + 1,但此题下标从0开始,故i - n + 1 - 1 = i - n
- j = ne[j];//求出模板串 P 在模式串 S中所以出现的位置的起始下标,找到一个位置后退回去再找一个
- }
- }
-
- return 0;
- }
1.b站--董晓算法 182 KMP 算法_哔哩哔哩_bilibili
2. AcWing算法基础课
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。