KMP算法
- 模式匹配:子串定位运算,即在主串中找出子串出现的位置。
- 在串匹配中,将主串 S 称为目标串,子串 T 称为模式串。
- 如果在主串 S 中能够找到子串 T,则称,匹配成功,返回第一个和子串 T 中第一个字符相等的字符在主串 S 中的序号;否则称为匹配失败,返回 0 。
串的存储结构
-
串的存储结构如下:
typedef struct { char *ch; // 若非空则按串长分配存储区,否则 ch 为 NULL int length; // 串长度 } HString;
#define maxstrlen 255 // 可在 255 以内定义最大串长。 typedef unsigned char SString[maxstrlen+1]; // 0 号单元存放串的长度。 // SString a 等价于 unsigned char a[maxstrlen+1]
#define CHUNKSIZE 80 // 可由用户定义的块大小 typedef struct Chunk { // 结点结构 char ch[CHUNKSIZE]; struct Chunk *next; } Chunk; typedef struct { // 串的链表结构 Chunk *head, *tail; // 串的头和尾指针 int curlen; // 串的当前长度 } LString;
- 在堆区和栈区存储时,默认将串的 0 号单元存储串的长度 length 。
朴素的模式匹配算法
-
从主串 S 的第 pos 个字符起和模式 T 的第一个字符比较之,若相同,则继续比较后续字符;否则从主串 S 的下一个字符起再重新和模式 T 的字符比较之。
int Index(SString S, SString T, int pos) { int i , j; i = pos; j = 1; while (i <= S[0] && j <= T[0]) { if (S[i] == T[j]) { ++ i; ++ j; // 继续比较后继字符 } else { i = i – j + 2; // 指针后退重新开始匹配 j = 1; } } if ( j >T[0]) return i -T[0]; // (此时j值已经越界)找到了,返回序号 else return 0; //(找不到,返回0) } // Index
主串 Si-j+1 Si-j+2 …… Si-1 Si 模式串 T1 T2 …… Tj-1 Tj - 从高位向低位看(从右向左),当主串第 i 位与模式串第 j 位出现失配时,需要退回到起始匹配位置并寻找其下一位重新匹配。
- 由上表可知,模拟串的起始位为 T1,T1 距 Tj j - 1 个元素,所以 Si 距离其起始位置也为 j - 1 个元素,所以其起始元素为 Si-j+1,下一位为 Si-j+2 。
- 从最坏情况来看,假设主串有 m 个字符,模式串有 n 个字符,则第一次比较 n 次,第二次比较 n 次,主串最后取到 m - n + 1 个字符,仍然比较 n 次。所以时间复杂度为 O(m*n),若能找到一种算法可以大幅度降低时间复杂度,则可提高运算效率,由此引出一种高效匹配算法——KMP算法。
KMP算法
-
引例:主串:a b a b c a b c a c b a b ,模式串:a b c a c
-
第一次匹配:
a b a b c a b c a a b c a c -
可知第一次匹配时,在第 3 位出现失配,而 T2 已经完成了比较,可知 S2 为 b,而 T1 为 a,所以不必从 S2 再次比较,可以直接将 S3 与 T1 进行比较。
-
第二次匹配:
a b a b c a b c a a b c a c -
可知第三次匹配时,在第 5 位出现失配,而 T4 已经完成了比较,可知 S6 为 a,而 T1 为 a,所以不必从 S6 再次比较,可以直接将 S7 与 T2 进行比较。
-
第三次匹配:
a b a b c a b c a c a b c a c
-
-
根据上例可知,不是每次失配都要从主串起始位置的下一位开始比较,可以按照一定规律跳过无效比较进而减少时间复杂度。
-
第一次失配时,主串比较了 a b (S1S2),第二次从模式串第 1 位开始比较,第二次失配时,主串比较了 a b c a (S3S6),第三次从模式串第 2 为开始比较。
-
通过多次试验观察可知,下一次起始比较位与主串前后相同位数有关,第一次 a b,无相同位数,所以下一次起始位为 0 + 1 = 1,第二次 a b c a,第一位与第四位相同,所以下一次起始位置为 1 + 1 = 2 。
-
由此可得,若主串比较 a b c a b,在第六位失配,前后相同位数为两位(a b),所以下一次起始位置为 2 + 1 = 3 。所以下一次比较的起始位置应为:前后相同位数 + 1 。但这只是我们肉眼观察得到的,计算机无法通过观察得到此规律,所以我们需要用计算机角度理解此规律。
-
在此处我们引入 next[] 数组,再此数组内存放失配位与重新比较位的关系,串的 0 位存放串的长度,所以无法在 0 位失配,所以数组的 0 位为 0,当第一位失配时,下一次比较仍需从第一位开始比较。next[] 数组中的数据与主串无关,仅与模式串有关。
-
以模式串 a b a c a b 为例,在 next[] 数组中,第 0 位为 0,第 1 位为 0,可列出以下表格:
模式串 a b 失配位 0 1 2 next 0 0 - 若模式串在第二位出现失配,则需从第一位重新开始比较,所以 T2 的 next 值为 1。
模式串 a b a 失配位 0 1 2 3 next 0 0 1 - 若模式串在第三位出现失配,则说明主串对应第三位不是 a ,则需查看 T3 的前一位(即T2)的 next 值,T2 的 next 值为 1,T1 与T2 不同,所以 T3 的 next 值为 1(T2 的 next 值)。
模式串 a b a c 失配位 0 1 2 3 4 next 0 0 1 1 - 若模式串的第四位出现失配,则说明主串对应第四位不是 c,则需查看 T4 的前一位的 next 值,T3 的 next 值为 1,T1 与 T3 的值相等(都为 a),所以将 T3 的 next 值加 1 填入 T4 中。
模式串 a b a c a 失配位 0 1 2 3 4 5 next 0 0 1 1 2 - 若模式串的第五位出现失配,则说明主串对应第五位不是 a,则需查看 T5 的前一位的 next 值,T4 的 next 值为 2,T2 与 T4 不相等,则需查看 T2 的 next 值,为 1,T1 与 T4 不相等,所以要从第一位重新开始比较,所以 T5 的 next 值为 1。
模式串 a b a c a b 失配位 0 1 2 3 4 5 6 next 0 0 1 1 2 1 - 若模式串的第六位出现失配,则说明主串对应第六位不是 b,则需查看 T6 的前一位的 next 值,T5 的 next 值为 1,T1 与 T5 相等,所以将 T5 的 next 值加 1 填入T6 中。
模式串 a b a c a b 失配位 0 1 2 3 4 5 6 next 0 0 1 1 2 1 2 -
根据上述分析我们完成了对 next 表的构建,当 next = 0 时说明对应的主串比较位应向后挪动一位,即起始位置应由 S1 变成 S2 ,而其他值就是当失配位失配后的重新开始比较的位置,例如当第六位失配后,只需从第二位重新开始比较即可。
-
引入 next[] 的意义就在于,定位主串初始比较位 i 指针的不必回退,而是通过指向模式串初始比较位的 j 指针的改变降低时间复杂度(此处指针与C语言指针的意义不同),参看 next 数组生成算法:
void get_next(SString T, int next[]) { i = 1; j = 0; next[1] = 0; while (i < T[0]) { // i小于模式串长度 if ( j == 0 || T[i] == T[j] ) { ++i; ++j; next[i] = j; // } else j = next[j]; // j指针回退 } }
-
对应的 KMP算法为:
int Index_KMP(SString S, SString T, int pos) { i = pos; j = 1; while (i <= S[0] && j <= T[0]) { if (j == 0 || S[i] == T[j]) { ++i; ++j; // 继续比较后继字符 } else j = next[j]; // 模式串向右移动 } if (j > T[0]) return i-T[0]; // 匹配成功 else return 0; }
-
通过上述 next[] 算法规则,当模式串为 a a a a b 时,会得出以下 next 数组:
模式串 a a a a b 失配位 1 2 3 4 5 next 0 1 2 3 4 - 当 T2 失配时,S2 已经确定不是 a 了,所以再次从第一位进行比较结果也是相同的,同理可得 T3、T4 也是相同的道理,由此可见 next 数组不能彻底地将时间复杂度降到最低,仍有优化的余地。
-
所以在 next[] 后我们再次引入 nexrval[] 来减少 next[] 的无效比较,再次引例:
模式串 a b a a b c a c 失配位 1 2 3 4 5 6 7 8 next 0 1 1 2 2 3 1 2 nextval 0 1 0 2 1 3 0 2 - 第一位的 nextval[] 必定为 0。
- 第二位的 next[] 值为 1,第二位与第一位不同(T2 与 T1),不算重复比较,所以从第一位(nextval[]值)重新开始比较。
- 第三位的 next[] 值为 1,但第三位与第一位相同,算重复比较,所以挪动主串后重新开始比较。
- 第四位的 next[] 值为 2,第四位与第二位不同,不算重复比较,所以从第二位重新开始比较。
- 第五位的 next[] 值为 2,但第五位与第二位相同,算重复比较,所以需要再次读取第二位的 next[] 值为 1,第五位与第一位不同,所以从第一位重新开始比较。
- 第六位的 next[] 值为 3,第六位与第三位不同,不算重复比较,所以从第三位重新开始比较。
- 第七位的 next[] 值为 1,但第七位与第一位相同,算重复比较,所以挪动主串后重新开始比较。
- 第八位的 next[] 值为 2,第八位与第二位不同,不算重复比较,所以从第二位重新开始比较。
-
由此可得 nextval[] 的生成算法为:
void get_nextval(SString T, int nextval[]) { int i,j; i = 1; j = 0; nextval[1] = 0; while (i < T[0]) { if ( j == 0 || T[i] == T[j] ) { ++i; ++j; (j就是当前下表的next值) if(T[i]!=T[j]) nextval[i] = j; else nextval[i] = nextval[j]; } else j = next[j]; } }
-
只需将上述 KMP算法中的 next[] 替换为 nextval[] 即可。