KMP算法學習筆記
KMP算法 從零開始
大部分來自他人博客,蒟蒻只是總結學習
- 引言
字符串匹配。給你兩個字符串,尋找其中一個字符串是否包含另一個字符串,如果包含,返回包含的起始位置.
char *str = "bacbababadababacambabacaddababacasdsd";
char *ptr = "ababaca";
暴力解法
如果當前字符匹配成功(即S[i] == P[j]),則i++,j++,繼續匹配下一個字符;
如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j 被置為0。
來看看時間復雜度:最壞情況下為O(n*m)
所以有沒有一種改進的算法
改進方法
可以實現復雜度為O(m+n),為何簡化了時間復雜度:
KMP算法主要是取消了指針的回溯,充分利用了目標字符串ptr的性質(比如裏面部分字符串的重復性,即使不存在重復字段,在比較時,實現最大的移動量)。每趟匹配過程中出現字符比較不等時,不回溯主指針i,利用已得到的“部分匹配”結果將模式向右滑動盡可能遠的一段距離,繼續進行比較。
具體概念上的我不想深究,從代碼開始理解
---代碼
- KmpSearch函數
- 假設現在文本串S匹配到 i 位置,模式串P匹配到 j 位置
- 如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++,繼續匹配下一個字符;
- 如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]。此舉意味著失配時,模式串P相對於文本串S向右移動了j - next [j] 位。
換言之,當匹配失敗時,模式串向右移動的位數為:失配字符所在位置 - 失配字符對應的next 值,即移動的實際位數為:j - next[j],且此值大於等於1。
- 假設現在文本串S匹配到 i 位置,模式串P匹配到 j 位置
所以next 數組各值的含義:代表當前字符之前的字符串中,有多大長度的相同前綴後綴。例如如果next [j] = k,代表j 之前的字符串中有最大長度為k 的相同前綴後綴。
此也意味著在某個字符失配時,該字符對應的next 值會告訴你下一步匹配中,模式串應該跳到哪個位置(跳到next [j] 的位置)。如果next [j] 等於0或-1,則跳到模式串的開頭字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某個字符,而不是跳到開頭,且具體跳過了k 個字符。
int KmpSearch(char* s, char* p)
{
int i = 0;
int j = 0;
int sLen = strlen(s);
int pLen = strlen(p);
while (i < sLen && j < pLen)
{
//①如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
//②如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]
//next[j]即為j所對應的next值
j = next[j];
}
}
if (j == pLen)
return i - j;
else
return -1;
}
- getnext()函數
- (1) next[0] = -1;
- (2) 設next[j] = k,則next[j+1] = ?
令j=j+1,k=k+1;- 若pk=pj,則有“p1…pk-1pk”=“pj-k+1…pj-1pj” ,
next[j]=k; - 若pk+1≠pj+1,可把求next值問題看成是一個模式匹配問題,整個模式串既是主串,又是子串。
即使得k=next[k],回溯求得最長前綴等於最長後綴的下標
- 若pk=pj,則有“p1…pk-1pk”=“pj-k+1…pj-1pj” ,
j | 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
---|---|
模式串 | 0 a b c a a b b c a b c a a b d a |
next[j] | -1 0 0 0 1 1 2 0 0 1 2 3 4 5 6 0 1 |
void getnext(int*next,char*ctr){
next[0]=-1;
int j=0,k=-1,len=strlen(ctr);
while(j<len){
if(k==-1||ctr[j]==ctr[k]){
j++;
k++;
next[j]=k;
}
else k=next[k];
}
}
完整代碼
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+7;
int a[maxn],b[maxn];
int nxt[10005],n,m;
void getnext(){
int i=0,j=-1;
nxt[0]=-1;
while(i<m){
if(j==-1||b[i]==b[j]){
i++,j++;
if(b[i]==b[j])nxt[i]=nxt[j];
else nxt[i]=j;
}
else j=nxt[j];
}
}
int kmp(){
int i=0,j=0;
getnext();
while(i<n){
if(a[i]==b[j]||j==-1)i++,j++;
else j=nxt[j];
if(j==m)return i-j+1;
}
return -1;
}
int main(){
int t;
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)scanf("%d",&a[i]);
for(int i=0;i<m;i++)scanf("%d",&b[i]);
if(n<m)printf("-1\n");
else printf("%d\n",kmp());
}
return 0;
}
練習
牛客三 E
題目:給你一個字符串S,你要對字符串S的每一位i將前i位的字符串移動到尾部形成一個新的字符串,如果形成的字符串相同則歸為一類Li。現在讓你將Li類按照字典序排序,並讓你輸出每一類的數量和每一類中字符串對應的下標i
很多人用哈希,正解是KMP的next數組的運用.通過觀察,題目問的就是求字符串的循環節,而next數組就是前後綴的運用。然後有個結論:如果對於一個長度為L的字符串,如果L%(L-next[L])==0則代表它具有循環節,且循環節的長度為L-next[L]。
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+7;
char b[maxn];
int net[maxn],len;
template<class T>
void read(T &res)
{
res = 0;
char c = getchar();
T f = 1;
while(c < '0' || c > '9')
{
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9')
{
res = res * 10 + c - '0';
c = getchar();
}
res *= f;
}
template<class T>
void out(T x)
{
if(x < 0)
{
putchar('-');
x = -x;
}
if(x >= 10)
{
out(x / 10);
}
putchar('0' + x % 10);
}
void get_next(char*ctr)
{
net[0]=-1;
int j=0,k=-1;
len=strlen(ctr);
while(j<len)
{
if(k==-1||ctr[j]==ctr[k])
{
j++;
k++;
net[j]=k;
}
else k=net[k];
}
}
int main()
{
scanf("%s",b);
get_next(b);
int tmp=len-net[len];
if(len%tmp==0&&tmp!=len)
{
out(tmp);
puts(" ");
for(int i=0; i<tmp; i++)
{
out(len/tmp);
printf(" ");
for(int j=i;j<len;j+=tmp){
out(j);
//
printf(" ");
}
printf("\n");
}
}
else{
out(len);
// puts(" ");
printf("\n");
for(int i=0;i<len;i++){
out(1);
printf(" ");
out(i);
printf("\n");
}
}
//
return 0;
}
KMP算法學習筆記