字符串模式匹配有以下几种算法:
- BF算法
- KMP算法
- Sunday算法
- AC自动机(多模匹配)
BF算法:
我们常用的暴力算法,时间复杂度 O(n2) O ( n 2 ) 。
代码演示:
int BF(const char *text, const char *pattern) {
int len1 = strlen(text);
int len2 = strlen(pattern);
for (int i = 0; i < len1 - len2 + 1; ++i) {
int flag = 1;
for (int j = 0; pattern[j]; ++j) {
if (text[i + j] == pattern[j]) continue;
flag = 0;
}
if (flag) return 1;
}
return 0;
}
KMP算法:
基于BF算法的优化,他根据字符串出现前缀与后缀相同的情况进行优化
假设这里SA、SB、TA、TB都相同,我们在这个位置失配,如在BF算法,我们将TA与SA之后的第一个字符进行匹配,这样效率显然很低,其实我们发现既然TB等于TA,那我们下一轮可以使用TA的下一个字符串与SB之后的第一个字符匹配即可。
我们这里要根据模式串计算一个next数组存储模式串中每个字符对应的前缀中字符位置,如若没有则标记为-1。
KMP算法的主要优点在于母串指针不会回溯。
代码演示:
int KMP(const char *text, const char *pattern) {
int len_1 = strlen(text);
int len_2 = strlen(pattern);
int *next = (int *)malloc(sizeof(int) * len_2);
next[0] = -1;
int matrix = -1;
for (int i = 1; pattern[i]; ++i) {
while (matrix != -1 && pattern[matrix + 1] != pattern[i]) {
matrix = next[matrix];
}
if (pattern[matrix + 1] == pattern[i]) {
matrix = matrix + 1;
}
next[i] = matrix;
}
matrix = -1;
for (int i = 0; text[i]; ++i) {
while (matrix != -1 && pattern[matrix + 1] != text[i]) {
matrix = next[matrix];
}
if (pattern[matrix + 1] == text[i]) {matrix++;}
if (pattern[matrix + 1] == 0) return 1;
}
return 0;
}
Sunday算法:
当我们在这个地方发生失配时,Sunday算法主要采用的是对齐法,然后模式串指针跳跃到这个字符最后出现的位置。
在这里我们可以使用一个数组预处理模式串中每个字符最后出现的位置,那么我们不妨直接计算出遇到每个字符母串应该跳跃的位置。 如果是模式串中没有出现过的字符,那么我们母串的指针应该向前跳跃多少步? 答案必然是 模式串长度 + 1
我们已经明确直到 t t 不在模式串中,所以我们的母串指针一定要跳到 t t 之后。
如果当前字符是模式串第一个字符,那么我们母串的指针应该向前跳跃多少步? 答案必然是 模式串长度。
根据数学归纳法,我们可以推论实质上跳跃步数为 len−i l e n − i 。
代码演示:
int sunday(const char *text, const char *pattern) {
int len = strlen(pattern), len2 = strlen(text);
int ind[127] = {0};
for (int i = 0; i < 127; i++) ind[i] = len + 1;
for (int i = 0; pattern[i]; i++) ind[pattern[i]] = len - i;
for (int i = 0; i <= len2 - len;) {
int j = 0;
for (; j < len; j++) {
if (pattern[j] != text[i + j]) break;
}
if (j == len) return 1;
i += ind[text[i + len]];
}
return 0;
}
AC自动机算法:
本质:字典树 + KMP
数据结构用于数据表示
字典树暴力匹配其实也较为优秀,大型企业主要采用的还是字典树而非AC自动机,由于模式串增加后,重构fail指针较为麻烦。
添加fail指针的字典树叫做AC自动机(NFA)。单边关系转化
AC自动机采用的是KMP的思想:母串指针不回溯。
在编程实现的过程中需要进行层次遍历。采用队列的数据结构,父节点为子节点分配fail指针。
代码演示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BASE 26
#define MAX_N 100000
#define BL 'a'
typedef struct Node {
char *str;
int flag;
struct Node* next[BASE];
struct Node* fail;
} Node;
Node* getNode() {
return (Node*)calloc(1, sizeof(Node));
}
void clear(Node *node) {
if (node == NULL) return;
for (int i = 0; i < BASE; ++i) {
if (node->next[i] == NULL) continue;
clear(node->next[i]);
}
if (node->flag) free(node->str);
free(node);
return;
}
Node* insert(Node *root, const char* str) {
if (root == NULL) root = getNode();
Node* p = root;
for (int i = 0; str[i]; ++i) {
if (p->next[str[i] - BL] == NULL) p->next[str[i] - BL] = getNode();
p = p->next[str[i] - BL];
}
p->flag = 1;
p->str = strdup(str);
return root;
}
void build_ac(Node* root) {
if (root == NULL) return;
Node **queue = (Node**)malloc(sizeof(Node*) * MAX_N);
int head = 0, tail = 0;
queue[tail++] = root;
while (head < tail) {
Node* node = queue[head++];
for (int i = 0; i < BASE; ++i) {
if (node->next[i] == NULL) continue;
Node *p = node->fail;
while (p && p->next[i] == NULL) {
p = p->fail;
}
if (p == NULL) {
node->next[i]->fail = root;
}else {
node->next[i]->fail = p->next[i];
}
queue[tail++] = node->next[i];
}
}
free(queue);
return;
}
void search_ac(Node *root, char *text) {
Node* p = root;
for (int i = 0; text[i]; ++i) {
while (p && p->next[text[i] - BL] == NULL) p = p->fail;
if (p == NULL) p = root;
else p = p->next[text[i] - BL];
Node *q = p;
while (q) {
if (q->flag) printf("%s\n", q->str);
q = q->fail;
}
}
return;
}
int main() {
Node* root = NULL;
root = insert(root, "say");
root = insert(root, "she");
root = insert(root, "shr");
root = insert(root, "he");
root = insert(root, "her");
build_ac(root);
search_ac(root, "sasherhs");
clear(root);
system("pause");
return 0;
}