串,KMP算法

文章目录

  • 模式匹配
  • 暴力算法
  • KMP算法
    • next数组
    • KMP算法
    • nextval数组

逻辑结构:线性结构
存储结构:定长顺序存储(char),堆分配存储(new,malloc),块链存储

1.定长顺序存储
静态数组,栈分配内存(结构体,函数及其内部变量都是栈分配的),所以只能固定大小。
可以用strlen(s)计算串长,sizeof(s)计算数组大小。
输入过长,系统会自动截断,并在末尾添加'\0'

char s[] = "this is a string"
int len = strlen(s)  //得到字符串长

2.堆分配存储
仍然是一组地址连续的存储单元,在堆上申请。
使用 new/deletemalloc/free进行申请/释放。
可用strlen()计算串长,不可用sizeof()计算数组大小(仅适用于静态数组)。
赋值/输入时注意溢出,不会像静态数组那样截断。

char* s = new char[10];
cin >> s;
cout << strlen(s) << endl;

3.块链存储
使用链表来存储字符串,具体实现时,一个结点可以放一个/多个字符。


模式匹配

模式匹配:子串(模式串)的定位操作。


暴力算法

直接对主串上的每一个字符,都与子串进行比较,复杂度O(mn)
i = 2 \quad \quad \quad i=2 i=2
↓ \quad \quad \quad \downarrow
a b a b c a\quad b\quad a\quad b\quad c ababc
a b c a\quad b\quad c abc
↑ \quad \quad \quad \uparrow
j = 2 \quad \quad \quad j=2 j=2

1. i i i重置为1, j j j重置为0
     i = 1 \quad \; \ i=1  i=1
     ↓ \quad \; \ \downarrow  
a b a b c a\quad b\quad a\quad b\quad c ababc
     a b c \quad \; \ a\quad b\quad c  abc
     ↑ \quad \; \ \uparrow  
     j = 0 \quad \; \ j=0  j=0

i = 1 \quad \quad \quad i=1 i=1
↓ \quad \quad \quad\downarrow
a b a b c a\quad b\quad a\quad b\quad c ababc
a b c \quad \quad \quad a\quad b\quad c abc
↑ \quad \quad \quad \uparrow
j = 0 \quad \quad \quad j=0 j=0

i = 1 \quad \quad \quad \quad\quad\quad i=1 i=1
↓ \quad \quad \quad \quad\quad\quad\downarrow
a b a b c a\quad b\quad a\quad b\quad c ababc
a b c \quad \quad \quad a\quad b\quad c abc
↑ \quad \quad \quad\quad\quad\quad\uparrow
j = 0 \quad \quad \quad \quad\quad\quad j=0 j=0
匹配成功。

int BruteForce(char* s, char* t) {
    int p = 0, i = 0, j = 0;   //p是主串上的外指针,ij是俩内指针
    while (i < strlen(s) && j < strlen(t)) {
        if (s[i] == t[j]) {
            i++; j++;
        }
        else {
            p++;   //左移一位     
            i = p;  //重置
            j = 0;  //重置
        }
    }
    if (j == strlen(t))
        return i - strlen(t);
    else
        return -1;
}

KMP算法

相比于暴力算法,KMP算法做了些改进。
比如说模式串:
a   b   a   b a\ b\ a\ {\color{red}b} a b a b ,主串 a   b   a   a   a   b a\ b\ a\ {\color{red}a}\ a\ b a b a a a b 时, i = 3 , j = 3 i=3,j=3 i=3j=3
此时我们直接让j=2,i不变(模式串向左滑动)重新比较
a   b   a   b a\ {\color{red}b} \ a\ b a b a b a   b   a   a   a   b a\ b\ a\ {\color{red}a}\ a\ b a b a a a b,此时 i = 3 , j = 1 i=3,j=1 i=3j=1,继续比较
在这之中,我们需要找到第j个字符前面的,相同的最长后缀与最长前缀。
后缀为:a、ba
前缀为:a、ab
a与a相吻合,我们之间越过匹配的a,从 j = 1 j=1 j=1开始即可。

KMP算法中,主串指针 i i i,不会回退,而模式串的指针 j j j,在匹配失败时,会移到相应位置(next[j]),跳过重复判断的地方,从相同前后缀的地方继续判断。

i = 2 \quad \quad \quad i=2 i=2
↓ \quad \quad \quad \downarrow
a b a b c a b c a c b a b a\quad b\quad a\quad b\quad c\quad a\quad b\quad c\quad a\quad c\quad b\quad a\quad b ababcabcacbab
a b c a c a\quad b\quad c\quad a\quad c abcac
↑ \quad \quad \quad \uparrow
j = 2 \quad \quad \quad j=2 j=2

没有相同前缀后缀,直接令 j = 0 j=0 j=0 i i i不变,重新比较

i = 2 \quad \quad \quad i=2 i=2
↓ \quad \quad \quad \downarrow
a b a b c a b c a c b a b a\quad b\quad a\quad b\quad c\quad a\quad b\quad c\quad a\quad c\quad b\quad a\quad b ababcabcacbab
a b c a c \quad \quad \quad a\quad b\quad c\quad a\quad c abcac
↑ \quad \quad \quad \uparrow
j = 0 \quad \quad \quad j=0 j=0

i = 6 \quad\quad\quad\quad\quad\quad\quad \quad \quad i=6 i=6
↓ \quad\quad\quad\quad\quad\quad\quad \quad \quad \downarrow
a b a b c a b c a c b a b a\quad b\quad a\quad b\quad c\quad a\quad b\quad c\quad a\quad c\quad b\quad a\quad b ababcabcacbab
a b c a c \quad \quad \quad a\quad b\quad c\quad a\quad c abcac
↑ \quad\quad\quad\quad\quad\quad\quad \quad \quad \uparrow
j = 4 \quad\quad\quad\quad\quad\quad\quad \quad \quad j=4 j=4

此时我们找后缀和前缀,只有 a a a可以,我们直接令 j = 2 j=2 j=2即可

i = 6 \quad\quad\quad\quad\quad\quad\quad \quad \quad i=6 i=6
↓ \quad\quad\quad\quad\quad\quad\quad \quad \quad \downarrow
a b a b c a b c a c b a b a\quad b\quad a\quad b\quad c\quad a\quad b\quad c\quad a\quad c\quad b\quad a\quad b ababcabcacbab
   a b c a c \quad \quad \quad \quad \quad \quad \quad \ \ a\quad b\quad c\quad a\quad c   abcac
↑ \quad\quad\quad\quad\quad\quad\quad \quad \quad \uparrow
j = 1 \quad\quad\quad\quad\quad\quad\quad \quad \quad j=1 j=1

  i = 9 \quad\quad\quad\quad\quad\quad\quad \quad \quad\quad \quad \quad \quad \ i=9  i=9
  ↓ \quad\quad\quad\quad\quad\quad\quad \quad \quad \quad \quad \quad \quad \ \downarrow  
a b a b c a b c a c b a b a\quad b\quad a\quad b\quad c\quad a\quad b\quad c\quad a\quad c\quad b\quad a\quad b ababcabcacbab
   a b c a c \quad \quad \quad \quad \quad \quad \quad \ \ a\quad b\quad c\quad a\quad c   abcac
   ↑ \quad\quad \quad \quad \quad \ \ \quad\quad\quad\quad\quad\quad \quad \quad \uparrow   
   j = 4 \quad\quad\quad \quad \quad \quad \ \ \quad\quad\quad\quad\quad \quad \quad j=4   j=4
匹配成功。

对于模式串中每个字符匹配错误时,指针 j j j的跳转明显只与模式串内部有关,我们可以预先计算出,每个字符匹配错误时指针跳转的位置next[j]


next数组

动态规划法:
1.边界条件:
我们规定,next[0] = -1,当遇到next值为-1时,i++,j++(指针均后移一位)。
显然,next[1] = 0,第二个字符不匹配,就让第一个字符匹配看看。

2.递推关系:
若已知next[i] = k,则 S i S_i Si 后面有 k k k个字符 S i − k . . . S i − 2 S i − 1 S_{i-k}...S_{i-2}S_{i-1} Sik...Si2Si1,与 S 0 S 1 . . . S k − 1 S_0S_1...S_{k-1} S0S1...Sk1相同。
S 0 , S 1 , . . , S k − 1 ⏟ k , S n e x t [ i ] , . . . . . . , S i − k , . . , S i − 2 , S i − 1 ⏟ k , S i , . . . \underbrace{{\color{blue}S_0,S_1,..,S_{k-1}}}_{k},{\color{red}S_{next[i]}},......,\underbrace{{\color{blue}S_{i-k},..,S_{i-2},S_{i-1}}}_{k},{\color{red}S_{i}},... k S0,S1,..,Sk1,Snext[i],......,k Sik,..,Si2,Si1,Si,... ↕ \quad \quad \quad \quad \quad\quad \quad \quad \quad \quad\quad \quad \quad \quad \quad\quad \quad \quad \quad \quad\quad \quad \quad \quad\updownarrow
第 二 次 比 较 , 模 式 串 右 移 S 0 , . . , S k − 2 , S k − 1 ⏟ k , S n e x t [ i ] , . . . . . . 第二次比较,模式串右移 \quad \quad \quad \quad \underbrace{{\color{blue}S_0,..,S_{k-2},S_{k-1}}}_{k},{\color{red}S_{next[i]}},...... k S0,..,Sk2,Sk1,Snext[i],......
(蓝色表必然相同,红色 S i S_i Si是匹配失败的地方)
显然,对于计算next数组,其实也是模式匹配,我们需要找到相应的前缀后缀相匹配。但是我们需要记录下匹配的串的后一个字符的下标,作为next数组的值。

对于next[i+1]
(1) 若 S i = S n e x t [ i ] S_i=S_{next[i]} Si=Snext[i] S i = S k S_i=S_k Si=Sk,那么 S i − k . . . S i − 1 S i = S 0 S 1 . . . S k S_{i-k}...S_{i-1}S_i=S_0S_1...S_k Sik...Si1Si=S0S1...Sk
此时next[i+1] = next[i]+1 = k+1;前缀后缀相同的有k+1个,可以直接将指针移到第k+2个字符 S k + 1 S_{k+1} Sk+1上,继续比较。
S 0 , S 1 , . . , S n e x t [ i ] ⏟ k + 1 , S n e x t [ i ] + 1 , . . . . . . , S i − k , . . , S i − 1 , S i ⏟ k + 1 , S i + 1 \underbrace{{\color{blue}S_0,S_1,..,{\color{orange}S_{next[i]}}}}_{k+1},{\color{red}S_{next[i]+1}},......,\underbrace{{\color{blue}S_{i-k},..,S_{i-1},{\color{orange}S_{i}}}}_{k+1},{\color{red}S_{i+1}} k+1 S0,S1,..,Snext[i],Snext[i]+1,......,k+1 Sik,..,Si1,Si,Si+1
(橙色是判断后相同,此时前缀后缀+1,并且是最长的了)

(2) 若 S i ≠ S n e x t [ i ] S_i\not=S_{next[i]} Si=Snext[i]
我们判断是否 S i = S n e x t [ n e x t [ i ] ] S_i=S_{next[next[i]]} Si=Snext[next[i]]
因为 S n e x t [ i ] S_{next[i]} Snext[i] S n e x t [ n e x t [ i ] ] S_{next[next[i]]} Snext[next[i]]next[next[i]]个前缀后缀相同。
next[next[i]] = m,那么 S 0 , S 1 , . . , S m − 1 = S k − m , . . , S k − 1 S_0,S_1,..,S_{m-1}=S_{k-m},..,S_{k-1} S0,S1,..,Sm1=Skm,..,Sk1
S 0 , S 1 , . . , S m − 1 ⏟ m , S n e x t [ n e x t [ i ] ] , . . . . . . , S k − m , . . , S k − 1 ⏟ m , S n e x t [ i ] \underbrace{{\color{blue}S_0,S_1,..,S_{m-1}}}_{m},{\color{red}S_{next[next[i]]}},......,\underbrace{{\color{blue}S_{k-m},..,S_{k-1}}}_{m},{\color{red}S_{next[i]}} m S0,S1,..,Sm1,Snext[next[i]],......,m Skm,..,Sk1,Snext[i]

S 0 , S 1 , . . , S m − 1 = S k − m , . . , S k − 1 = S i − m + 1 . . . S i − 1 S i S_0,S_1,..,S_{m-1}=S_{k-m},..,S_{k-1}=S_{i-m+1}...S_{i-1}S_{i} S0,S1,..,Sm1=Skm,..,Sk1=Sim+1...Si1Si
如果 S i = S n e x t [ n e x t [ i ] ] S_i=S_{next[next[i]]} Si=Snext[next[i]],那么next[i+1] = next[next[i]]+1 = m+1

S 0 , . . , S m − 1 , S n e x t [ n e x t [ i ] ] ⏟ m + 1 , . . . . . . , S k − m , . . , S k − 1 ⏟ m , S n e x t [ i ] , . . . . . . , S i − m , . . , S i − 1 , S i ⏟ m + 1 , S i + 1 \underbrace{{\color{blue}S_0,..,S_{m-1}},{\color{orange}S_{next[next[i]]}}}_{m+1},......,\underbrace{{\color{blue}S_{k-m},..,S_{k-1}}}_{m},{\color{red}S_{next[i]}},......,\underbrace{{\color{blue}S_{i-m},..,S_{i-1},{\color{orange}S_{i}}}}_{m+1},{\color{red}S_{i+1}} m+1 S0,..,Sm1,Snext[next[i]],......,m Skm,..,Sk1,Snext[i],......,m+1 Sim,..,Si1,Si,Si+1
(前缀后缀由 n e x t [ i ] + 1 → n e x t [ n e x t [ i ] ] + 1 next[i]+1\rightarrow next[next[i]]+1 next[i]+1next[next[i]]+1减少了)

虽然我们的相同前缀后缀减小了,但是依旧找到了,如果依旧没有,就寻找 S n e x t [ n e x t [ n e x t [ i ] ] ] S_{next[next[next[i]]]} Snext[next[next[i]]]…,直到 S 0 S_0 S0
这种重置错误字符对应位置的方法,也相当于使用了模式匹配算法,真就一个字非常的吊 dio …。如果没找到,就将j重置为next[j],继续寻找。这也是为什么get_next()和kmp()主体几乎一样,因为get_next()也是用的模式匹配的思想。

正常情况,串不长,我们的 j j j只需要1~3次就差不多了,很容易由next[k]=0、1,然后就结束了判断。

void get_next(char* t, int *next) {   //动态规划法
    next[0] = -1;
    int i = 0;       //i是比较位置的前一个字符S_{i-1}
    int j = next[i];      //j=next[i]
    while (i < strlen(t)-1) {
        if (j == -1 || t[i] == t[j]) {  //若第i-1个和第j个相同,那么就 next[i+1]=j+1
            i++; j++;
            next[i] = j;
        }
        else
            j = next[j];  //Si与Snext[i]不同,则j=next[j]=next[next[i]]
    }					 //其实也相当模式匹配失败,重置了j
}

上述为书上的解法,利用动态规划的思想,进行求解,可以说是非常精简,我看了好久还是懵的 我尝试去写了写,感觉真不错。


KMP算法

有了next数组,我们就不需要每次失败去重置 i , j i,j i,j指针,我们只需要重置 j=next[j]即可

int kmp(char* s, char* t, int* next) {
    int i = 0, j = 0;       //i为主串指针,j为模式串指针
    int n = strlen(s), m = strlen(t);   //n为主串长,m为模式串长

    while (i < n && j < m) {
        if (j == -1 || s[i] == t[j]) {
            i++; j++;
        }
        else {
            j = next[j];    //重置j为next[j]
        }
    }
    if (j == m)
        return i - m;
    else
        return -1;
}

nextval数组

对于:
     i = 3 \quad \quad \quad \quad \; \ i=3  i=3
     ↓ \quad \quad \quad \quad \; \ \downarrow  
a a a b a a a a b a\quad a\quad a\quad {\color{red}b} \quad a\quad a\quad a\quad a\quad b\quad aaabaaaab
a a a a b a\quad a\quad a\quad {\color{red}a}\quad b aaaab
     ↑ \quad \quad \quad \quad \; \ \uparrow  
     j = 3 \quad \quad \quad \quad \; \ j=3  j=3

如果是next数组,显然,下一步会去匹配 j = 2 j=2 j=2.

     i = 3 \quad \quad \quad \quad \; \ i=3  i=3
     ↓ \quad \quad \quad \quad \; \ \downarrow  
a a a b a a a a b a\quad a\quad a\quad {\color{red}b}\quad a\quad a\quad a\quad a\quad b\quad aaabaaaab
     a a a a b \quad \; \ a\quad a\quad {\color{red}a}\quad a\quad b \qquad \qquad  aaaab此时 S n e x t [ 3 ] = S 3 = a , 必 然 失 败 S_{next[3]}=S_3=a,必然失败 Snext[3]=S3=a
     ↑ \quad \quad \quad \quad \; \ \uparrow  
     j = 2 \quad \quad \quad \quad \; \ j=2  j=2

接着匹配 j = 1 j=1 j=1, j = 0 j=0 j=0,这些都会失败。

     i = 3 \quad \quad \quad \quad \; \ i=3  i=3
     ↓ \quad \quad \quad \quad \; \ \downarrow  
a a a b a a a a b a\quad a\quad a\quad {\color{red}b}\quad a\quad a\quad a\quad a\quad b\quad aaabaaaab
a a a a b \quad \quad \quad a\quad {\color{red}a}\quad a\quad a\quad b \qquad \qquad aaaab此时 S n e x t [ 2 ] = S 2 = a , 必 然 失 败 S_{next[2]}=S_2=a,必然失败 Snext[2]=S2=a
     ↑ \quad \quad \quad \quad \; \ \uparrow  
     j = 1 \quad \quad \quad \quad \; \ j=1  j=1

     i = 3 \quad \quad \quad \quad \; \ i=3  i=3
     ↓ \quad \quad \quad \quad \; \ \downarrow  
a a a b a a a a b a\quad a\quad a\quad {\color{red}b}\quad a\quad a\quad a\quad a\quad b\quad aaabaaaab
     a a a a b \quad \quad \quad \quad \; \ {\color{red}a}\quad a\quad a\quad a\quad b \qquad \qquad  aaaab此时 S n e x t [ 1 ] = S 1 = a , 必 然 失 败 S_{next[1]}=S_1=a,必然失败 Snext[1]=S1=a
     ↑ \quad \quad \quad \quad \; \ \uparrow  
     j = 0 \quad \quad \quad \quad \; \ j=0  j=0

低效的原因:出现了 p j = p n e x t [ j ] p_j=p_{next[j]} pj=pnext[j]

我们求next数组保证 p n e x t [ j ] p_{next[j]} pnext[j] 的前缀和 p j p_j pj 的后缀是相同的,但是没有考虑过 p n e x t [ j ] p_{next[j]} pnext[j] 的值的影响,如果 p j = p n e x t [ j ] p_j=p_{next[j]} pj=pnext[j],我们下次将 j j j移到 n e x t [ j ] next[j] next[j]位置,只会重蹈覆辙再次失败!
所以我们在求next的时候,还需要考虑 p n e x t [ j ] p_{next[j]} pnext[j] 的值。

p n e x t [ j ] = p j p_{next[j]}=p_j pnext[j]=pj
则将next[j]修正为next[next[j]],继续递归寻找前缀后缀相同,但是对应 s i s_i si 的字符不同的字符。

void get_nextval(char* s, int* nextval) {
    nextval[0] = -1;
    int i = 0, j = -1;
    while (i < strlen(s) - 1) {
        if (j == -1 || s[i] == s[j]) {
            i++; j++;
            if (s[i] != s[j])        //第i+1个字符与next[i]+1个字符不同
                nextval[i] = j;
            else
                nextval[i] = nextval[j];    //递归寻找
        }
        else
            j = nextval[j];
    }
}

nextval数组很有可能出现多个-1,也就是递归最后得到 S i = S 0 S_i=S_0 Si=S0,直接跳过比较。加快了比较过程。

     i = 3 \quad \quad \quad \quad \; \ i=3  i=3
     ↓ \quad \quad \quad \quad \; \ \downarrow  
a a a b a a a a b a\quad a\quad a\quad {\color{red}b} \quad a\quad a\quad a\quad a\quad b\quad aaabaaaab
a a a a b a\quad a\quad a\quad {\color{red}a}\quad b aaaab
     ↑ \quad \quad \quad \quad \; \ \uparrow  
     j = 3 \quad \quad \quad \quad \; \ j=3  j=3

递归到第一个a,此时 n e x t v a l [ 3 ] = n e x t v a l [ 0 ] = − 1 nextval[3]=nextval[0]=-1 nextval[3]=nextval[0]=1

i = 4 \quad \quad \quad \quad \quad \quad i=4 i=4
↓ \quad \quad \quad \quad \quad \quad \downarrow
a a a b a a a a b a\quad a\quad a\quad b \quad {\color{blue}a}\quad a\quad a\quad a\quad b\quad aaabaaaab
a a a a b \quad \quad \quad \quad \quad \quad {\color{blue}a}\quad a\quad a\quad a\quad b aaaab
↑ \quad \quad \quad \quad \quad \quad \uparrow
j = 0 \quad \quad \quad \quad \quad \quad j=0 j=0

统考真题
只有两个选择题,考了KMP算法,我们只需要会口算即可(找前缀后缀的模式匹配),将前缀的后一个字符下标记为next[i]即可。
nextval没考,算法题也没考,说实话用动态规划写这玩意挺难的,或许是我太菜了,写不出来,逻辑很nb。

KMP通俗解释:
S i S_i Si匹配失败,观察其前缀,再从 S 0 S_0 S0开始,看看有没有相匹配的前缀。
要是找到,那前缀的后一个字符的下标就是next[i]
因为可以直接从前缀后一个字符开始和 S i S_i Si比较,毕竟他们的前面是一样的。

如果聪明点,可以优化一下得到nextval数组,就是求的时候,再看看,前缀后一个字符 S n e x t [ i ] S_{next[i]} Snext[i]是否等于 S i S_i Si,等于就没必要比了,必然失配,直接向下递归,next[i]=next[j];

你可能感兴趣的:(算法,链表,字符串,指针,c语言)