逻辑结构:线性结构
存储结构:定长顺序存储(char),堆分配存储(new,malloc),块链存储
1.定长顺序存储
静态数组,栈分配内存(结构体,函数及其内部变量都是栈分配的),所以只能固定大小。
可以用strlen(s)
计算串长,sizeof(s)
计算数组大小。
输入过长,系统会自动截断,并在末尾添加'\0'
。
char s[] = "this is a string"
int len = strlen(s) //得到字符串长
2.堆分配存储
仍然是一组地址连续的存储单元,在堆上申请。
使用 new/delete
或malloc/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算法做了些改进。
比如说模式串:
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=3,j=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=3,j=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]
动态规划法:
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} Si−k...Si−2Si−1,与 S 0 S 1 . . . S k − 1 S_0S_1...S_{k-1} S0S1...Sk−1相同。
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,..,Sk−1,Snext[i],......,k Si−k,..,Si−2,Si−1,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,..,Sk−2,Sk−1,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 Si−k...Si−1Si=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 Si−k,..,Si−1,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,..,Sm−1=Sk−m,..,Sk−1
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,..,Sm−1,Snext[next[i]],......,m Sk−m,..,Sk−1,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,..,Sm−1=Sk−m,..,Sk−1=Si−m+1...Si−1Si
如果 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,..,Sm−1,Snext[next[i]],......,m Sk−m,..,Sk−1,Snext[i],......,m+1 Si−m,..,Si−1,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]+1→next[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
}
上述为书上的解法,利用动态规划的思想,进行求解,可以说是非常精简,我看了好久还是懵的 我尝试去写了写,感觉真不错。
有了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;
}
对于:
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];