定义
由一个或多个字符组成的有限序列
存储结构
定长顺序存储
#define Length 255
struct sstring {
int length;
char ch[Length];
};
此时使用结尾的 '\0' 或长度 length 标记结束位
块链存储
模式匹配
Brute-Force 算法(暴力算法)
令主串为 s, 需要匹配的字符串称为“模式” t
步骤
以主串 "BBC ABCDAB ABCDABCDABDE" 和模式 "ABCDABD" 为例, 下标从 1 开始
- 比较第一个字符
-
发现不匹配, 则模式后移一位继续匹配
表现为 i(2) = i(1) - j(1) + 2, 即继续比较 s[2] 与 t[1]
-
s[2] 与 t[1] 也不匹配, 继续后移一位
- 以此类推, 到 D 发现不匹配
- 模式再后移一位, 模式从头开始重新匹配
表现为 i(6) = i(11) - j(7) + 2, 即继续比较 s[6] 与 t[1]
- 完全匹配后返回第一个匹配字符下标
表现为 index(12) = i(19) - t.length(7)
i=19 因为最后一次匹配(i=18)时相等, i++
代码实现
#include<stdio.h>
#define Length 255
struct sstring {
int length;
char ch[Length];
};
int find(sstring s, sstring t, int pos = 1) {
int i = pos; // 默认从 1 开始查找
int j = 1;
int count = 0; // 比较次数
// 下标不能超过两个字符串
while (i <= s.length && j <= t.length) {
count++;
// 如果字符相等, 则继续下一个字符
if (s.ch[i] == t.ch[j]) {
i++;
j++;
}
else {
// 遇到不匹配的字符
// 模式后移一位
i = i - j + 2;
// 模式重新从 1 开始匹配
j = 1;
}
};
printf("BF 算法比较次数: %d\n", count); // 26
// j > t.length 说明模式完全匹配
if (j > t.length) {
// 返回模式第一个匹配的字符位置
return i - t.length;
}
else {
// 匹配不成功返回 -1
return -1;
}
}
int main() {
// 前面一个 - 占位, 不起作用, 字符长度不包括 -
sstring s = {23, "-BBC ABCDAB ABCDABDCABDE"}
, t = {7, "-ABCDABD" };
int index = find(s, t);
printf("第一个匹配字符下标: %d", index);
}
复杂度分析
假设主串 s 长 n, 模式串 t 长 m
可能出现匹配的位置
最好的情况下, 如果在第 i 个位置匹配成功, 则前面 i-1 个位置都只比较一次就跳到下一个位置, 总共比较了 次
平均比较次数为
最坏的情况下, 如果第 i 个位置匹配成功, 则前面 i-1 个位置都比较 m 次(即每次都只是最后一个位置不一样), 总共比较了 次
平均比较次数:
存在问题
前面都是匹配的, 突然遇到一个不匹配的, 如果再后移一位去比较, 之前匹配的字符就完全错位, 肯定是不会匹配的
KMP 算法
前缀和后缀
前缀: 除了最后一个字符外的字符子串集合
后缀: 除了第一个字符外的字符子串集合
以字符串 'ababa' 为例
a没有前缀和后缀, 最大相等前后缀长度为 0ab的前缀为 {a}, 后缀为 {b}, 最大相等前后缀长度为 0aba的前缀为 {a, ab}, 后缀为 {a, ba}, 相等的前后缀为 {a}, 最大相等前后缀长度为 1abab的前缀为 {a, ab, aba}, 后缀为 {b, ab, bab}, 相等的前后缀为 {ab}, 因此最大相等前后缀长度为 2ababa的前缀为 {a, ab, aba, abab}, 后缀为 {a, ba, aba, baba}, 相等的前后缀为 {a, aba}, 最大相等前后缀长度为 3
PM 表
根据前后缀最大长度可以得出部分匹配表(PM 表)
最前面的
-1是方便当j=1时出现不匹配时能够访问PM(j-1), 为什么是-1与代码有关(后面说明)
这个表有什么用?
如上图, 当第 5 个不匹配时, 找到最后一个匹配字符(b)对应的部分匹配表值(2), 也就是说已经匹配了的字符串是 "abab", 其从左起, 有一个子字符串 "ab", 其在右边能找到一个一模一样的子字符串 "ab", 只需要将整体移动到后边, 就可以继续比较, 好处是 i 不需要“回退”了
ab是最长相等前后缀, 所以两个ab之间不会存在更长的重复子字符串
那么这个移动距离是多少呢?
实际使用时, 不是把字符移动, 而是改变下标
j
即j = j - ((j-1) - PM[j-1])=1+PM(j-1)
其中j-1是已经匹配的字符数
当
j=1时出现不匹配(即t[1]≠s[1]), 则移动后下标j = 1 + (-1) = 0
这时就需要在代码中判断当j=0时直接i++, j++, 这样j又变成1, 但是i变大了, 相当于t[1]与s[2]继续进行比较
next 表
使用部分匹配表求位移, 需要知道最后一个匹配字符的表值(也就是PM[j-1]), 如果把部分匹配表值右移一位, 就可以直接使用当前下标 j 获取表值(也就是Next[j])
部分匹配表最后一个 a 的 3 是不需要的, 因为能用到最后一个, 说明已经完全匹配了
这里还可以优化一下, 每次不匹配都需要 j=1+next[j] 有点麻烦, 如果 next 表在原来的基础上 +1 岂不美哉?
现在不匹配就可以直接 j=next[j] 了 🎉🎉🎉🎉
回到刚刚的例子:
next 表值的特殊意义:
当在 j 处遇到不匹配时, 跳转到 next[j] 处继续与主串比较
代码实现
例子与 BF 算法一致
#include<stdio.h>
#include<string>
#define Length 255
struct sstring {
int length;
char ch[Length];
};
// 手动生成 next 表
// 第一个 0 占位
int next[8] = { 0, 0, 1, 1, 1, 1, 2, 3 };
int find(sstring s, sstring t, int pos = 1) {
int i = pos;
int j = 1;
int count = 0;
while (i <= s.length && j <= t.length) {
if (j == 0) {
i++;
j++;
}
else if (s.ch[i] == t.ch[j]) {
count++;
i++;
j++;
}
else {
j = next[j];
}
};
printf("KMP 算法比较次数: %d\n", count); // 13
if (j > t.length) {
return i - t.length;
}
else {
return -1;
}
}
int main() {
sstring s = { 23, "-BBC ABCDAB ABCDABDCABDE" }
, t = { 7, "-ABCDABD" };
int index = find(s, t);
printf("第一个匹配字符下标: %d", index);
}
next 表代码实现
next[1]一定等于0- 当
next[j]=k时, 存在
为什么
1<k<j?k-1代表1~j-1子串最大相等前后缀长度, 因此一定存在k-1<j-1, 即k不会超过j, 并且 就是最长前缀
当 next[j]=k 时, next[j+1] 等于?
如果 t[k]=t[j] , 则有 , 因此 next[j+1] 等于 next[j]+1=k+1
比如
如果 t[k]≠t[j], 则将其看作一个迷你版模式匹配
因为 j'=next[k] < k, 所以 一定是向右滑动, 如果滑一次不匹配, 则 j''=next[next[k]], 直到最后 为止, 这时 next[j+1]=x+1
- 其他情况下都是
1
void buildNext(sstring t, int next[]) {
int j = 1, k = 0;
next[1] = 0; // 固定值
while (j < t.length) {
if (k == 0 || t.ch[j] == t.ch[k]) {
k++;
j++;
next[j] = k;
}
else {
k = next[k];
}
}
}
🌟过程分析
1. j=1, k=0, 结束后 j=2, k=1, next[2]=1, 代表 1 子串不存在相等前后缀(next[j]=k说明 1~j-1 子串存在 k-1 长度的相等前后缀)
- j=2, k=1, 此时 t[2]!=t[1], k=next[1]=0
- j=2, k=0, 结束后 j=3, k=1, next[3]=1, 说明
1~2子串不存在相等前后缀, 因为前面判断过t[2]!=t[1]
- j=3, k=1, t[3]==t[1], 结束后 j=4, k=2, next[4]=2, 说明
1~3子串有长度为 1 的相等前后缀(即 t[1] 和 t[3])
- j=4, k=2, t[4]==t[2], 结束后 j=5, k=3, next[5]=3, 因为此时
k>1了, 所以判断t[j]与t[k]是否相等, 相等则next[j+1]=next[j]+1=k+1
如果此时不相等呢, 比如
那就移到 1 号位(next[2]=1), 此时 j=4, k=1, 发现相等了, next[5]=k+1=2
复杂度分析
建立 next 表需要 , 匹配字符需要 , 总体需要
例题
BM 算法
坏字符(BC)
好后缀(GS)
步骤
- 首先对齐主串和模式串, 比较模式串的尾部, 发现不匹配, 则 C 被称为“坏字符”
尾部比较的好处是, 只要尾部不相同, 前面的都不用比了
- 坏字符 C 出现在模式串中, 因此将其与坏字符对齐, 继续比较尾部
那么位移是多少呢?
设坏字符在模式串中出现的位置(最后出现, 也就是最右边的)为 x ,出现坏字符的位置为 y , 则位移为 y-x , 如果模式串中没有坏字符, 则 x=0
注意, 本文字符下标都是从
1开始的, 如果从0开始, 则x=-1
为什么是最靠右的? 举个例子:
最靠右位移较小, 不会丢失匹配项
如果坏字符在左边没有出现, 却在好后缀里出现, 怎么办(也就是 x>y)? 比如:
那就往后移动一个单位(坏字符的原则是不倒退)
注意, 这是在左边没有的情况下才这样, 左边和好后缀都有, 根据左边来
- 由于空格不在模式串中, 整体移动到空格后边, 发现一个好后缀 D
- 一旦有好后缀, 则往前继续比较
- 比较完成
如果好后缀之后遇到坏字符怎么办?
比如:
如果根据坏字符规则, 由于 I 不在模式串中, 则移动 3-0=3 个单位, X 在模式串中, 移动 7-2=5 个单位, 最后也能匹配成功
🧐 能否借鉴 KMP 算法充分利用好后缀?
好后缀规则位移与坏字符类似, 即:
设好后缀在模式串中出现的位置(最后一个字符)为 x , 出现好后缀的位置(最后一个字符)为 y , 则位移为 y-x , 如果模式串中没有好后缀了, 则 x=0
注意, 对上面的例子而言, 好后缀可以是
MPLEPLELEE
只有MPLE可以是左边任意位置, 而非最长好后缀只能在开头
下图以 0 为起始
上面的例子不好说明好后缀的好处, 我们换个例子:
完全使用坏字符规则, 完成匹配需要 5 步
采用好后缀规则, 仅需要 4 步!
与 KMP 算法类似, 这样可以省略一些不必要的比较
📢当同时存在坏字符和好后缀时, 谁的位移最大取谁的
复杂度分析
对于坏字符规则:
最好情况下:
假设在第 i 个位置匹配成功, 则前 i - 1 次都只比较一次, 则平均情况:
最坏的情况下和 BF 算法一样, 复杂度
当加入了好后缀后, 最差的情况变成了与 KMP 一样, 即