https://leetcode-cn.com/problems/stream-of-characters/
这题用的AC自动机,之前在工作中用过,再来练练手总结一下,这里记录一下自己在实现过程中比较纠结的几个点。
AC自动机可以实现多模匹配,就是初始化给定一批特定字符串(模式)集合,然后判断输入的字符流中是否包含该集合中的模式串。
1.Trie树
要实现AC自动机需要先理解Trie树。
建立Trie树时会有一个空节点Root作为根,其余每个节点有一个指向父节点的指针Father,一个大小为字符集大小的孩子指针集合Next,Next[i]==Null表示对应的孩子不存在,以及一个标记当前节点是否是某个模式串的结束标记end;
AC自动机针对Trie树做了优化来提高匹配速度,在每个节点中增加了一个后缀指针Tail
2.后缀指针(Tail)
普通Trie树(不涉及各种优化)的构建还是比较简单的,构建完Trie树之后,就得计算每个节点的“后缀指针”。
百度浏览了一下AC自动机的相关介绍,可以知道传统上大家都叫“失配(Fail)指针”,基本每篇文章里面说的都是Fail指针,Fail指针的意思就是当前位置匹配失败后需要跳转过去的地方。这种说法只表达了匹配失败后要跳转,但是没有表达出为什么要跳转,跳转到哪里去。实际本质上是要跳转到当前匹配串的后缀串,所以我觉得称为“后缀(Tail)指针”可能更易于理解一点。
即:对于要查找的字符串D,设从根匹配到当前节点的串为S1,S1继续向下匹配失败,这说明所有与S1有公共前缀的模式串都匹配失败了,这时需要换到另一个与S1没有公共前缀的串进行匹配。
这里切换的时候就需要一定技巧了,类似于KMP算法的思想,就是对于已经比较过的字符串要避免重复比较。
即,如果新模式串M的一个前缀S2是当前串S1的后缀,那么切换后不需要从M头部开始比较,因为前缀S2已经在S1的匹配过程中比较过了,所以直接从S2后面继续比较即可。
如图所示,由模式串ABCD,BCD构成的Trie树,假设输入的字符流为ABCDE,则左边匹配到字符C,即串S1后,发现继续匹配G与D不同,匹配失败,这时我们需要换另一个模式进行匹配,但是可以看到BCD的前缀BC是S1的后缀,以及比较过了,所以我们直接跳到C'继续匹配,并命中模式BCD。所以这里,C的Tail指针指向C'。
理解了Tail指针的含义,我们就可以在初始化建立Trie树之后,计算好每个节点的Tail指针,从而可以在匹配的时候减少不必要的重复比较,提高匹配速度。
计算所有节点Tail指针
Root的所有孩子节点的Tail指针指向Root;
对于其余每个值为字符'C'的节点Node,其Tail指针指向父节点的Tail指针对应节点的子节点中值也为'C'的节点,若该节点不存在,则指向【父节点】的【Tail指针对应节点】的【Tail指针对应节点】的子节点中值也为'C'的节点,一起类推,直至Root节点,即:
Node->Tail = Node->Father->Tail->(...Tail->)Next[C]
因为计算每个节点的Tail指针过程需要用到上层节点的Tail信息,所以需要按广度优先的顺序计算。
如图所示,节点A、B’的Tail指针直接指向Root(具体实现的时候将指向Root的都指向NULL,便于判断);
节点C的Tail指针(蓝色)由红色指针Father->Tail->Next[C]确定指向C’;
节点D的Tail指针(黄色), 由绿色指针Father->Tail指向Root,因为root没有值为D的孩子节点,所以D的Tail直接指向Root.
3.匹配
在建立好Trie树,计算完Tail指针之后,就可以进行字符流的匹配了。
匹配过程中使用currentNode指针表示当前在树上的匹配位置,大致有以下几种匹配状态:
(1)currentNode字符与字符流当前字符相等
(1.1)currentNode被标记为end,则匹配到一个模式串,返回成功;
(1.2)currentNode不是end,这时就是匹配失败了,需要通过Tail指针去匹配其他模式串。但是这种匹配失败是因为当前模式没有结束而导致的,后续继续输入的字符流可能使得当前模式串可以继续匹配成功,因此不能丢失当前位置,即currentNode保持不动,使用一个新的临时指针tmp,通过Tail去找有没有比当前模式短的后缀子串能够匹配上:
tmp=currentNode->Tail;
while(tmp && tmp->end==0){
tmp = tmp->tail;
}
如果通过Tail进行回溯过程中匹配到一个end,则表示有一个较短的模式命中,返回成功;
否则直至Root(NULL)也没有命中则表示没有匹配到模式串,返回失败;
if(tmp && tmp->end){
return true;
}
return false;
(2)currentNode字符与字符流当前字符不相等
当前模式匹配失败,并且无法继续匹配,通过Tail指针切换到另一个模式进行匹配:
currentNode = currentNode->tail;
大致原理就是如此,最后奉上具体代码:
#define ALPHABET_SIZE 26
typedef struct _AcNode {
struct _AcNode *father;
struct _AcNode *tail;
char value;
char end;
struct _AcNode *nexts[ALPHABET_SIZE];
} AcNode;
typedef struct {
AcNode *root[ALPHABET_SIZE];
AcNode *currentNode;
int nodeCount;
} StreamChecker;
//#define MAX_SIZE 2000000
//AcNode *bfsQueue[MAX_SIZE];
void getTail(StreamChecker* streamChecker){
AcNode **bfsQueue = (AcNode**)malloc(streamChecker->nodeCount*sizeof(AcNode*));
int bfsQueueCount = 0;
int bfsQueueEnd = 0;
int bfsQueueStart = 0;
AcNode **nexts = streamChecker->root;
int i = 0;
for(i=0; i<ALPHABET_SIZE; i++){
if(nexts[i]){
bfsQueueCount++;
bfsQueue[bfsQueueEnd] = nexts[i];
bfsQueueEnd++;
}
}
while(bfsQueueCount){
AcNode * node = bfsQueue[bfsQueueStart];
if(node->father==NULL){
node->tail = NULL;
}else{
AcNode * father = node->father;
while(node->tail==NULL && father && father->tail){
node->tail = father->tail->nexts[node->value-'a'];
father = father->tail;
}
if(node->tail==NULL){
node->tail = streamChecker->root[node->value-'a'];
}
}
bfsQueueStart++;
bfsQueueCount--;
nexts = node->nexts;
for(i=0; i<ALPHABET_SIZE; i++){
if(nexts[i]){
bfsQueueCount++;
bfsQueue[bfsQueueEnd] = nexts[i];
bfsQueueEnd++;
}
}
}
}
void trieTree(char ** words, int wordsSize, StreamChecker* streamChecker){
int i = 0;
for(i=0; i<ALPHABET_SIZE; i++){
streamChecker->root[i] = NULL;
}
streamChecker->currentNode = NULL;
streamChecker->nodeCount = 0;
for(i=0; i<wordsSize; i++){
AcNode *father = NULL;
AcNode *tail = NULL;
AcNode **nexts = streamChecker->root;
char *p = words[i];
while(*p){
streamChecker->nodeCount++;
AcNode **next = &(nexts[(*p)-'a']);
if(*next==NULL){
*next = (AcNode*)malloc(sizeof(AcNode));
(*next)->father = father;
(*next)->tail = NULL;
(*next)->end = 0;
(*next)->value = *p;
int j = 0;
for(; j<ALPHABET_SIZE; j++){
(*next)->nexts[j] = NULL;
}
}
father = *next;
nexts = (*next)->nexts;
p++;
}
father->end = 1;
}
}
StreamChecker* streamCheckerCreate(char ** words, int wordsSize) {
StreamChecker* streamChecker = (StreamChecker*)malloc(sizeof(StreamChecker));
trieTree(words, wordsSize, streamChecker);
getTail(streamChecker);
return streamChecker;
}
bool streamCheckerQuery(StreamChecker* obj, char letter) {
while(obj->currentNode){
AcNode **nodes = obj->currentNode->nexts;
if(nodes[letter-'a']){//equal
obj->currentNode = nodes[letter-'a'];
if(nodes[letter-'a']->end){//equal and end
return true;
}
else{//equal but not end
AcNode *tmp = obj->currentNode->tail;
while(tmp && tmp->end==0){
tmp = tmp->tail;
}
if(tmp && tmp->end){
return true;
}
return false;
}
}
//fail
obj->currentNode = obj->currentNode->tail;
}
AcNode **nodes = obj->root;
if(nodes[letter-'a']){//equal
obj->currentNode = nodes[letter-'a'];
if(nodes[letter-'a']->end){//equal and end
return true;
}else{//not end
return false;
}
}
return false;//no equal
}
void nodeFree(AcNode* node){
AcNode **nexts = node->nexts;
int i = 0;
for(i=0; i<ALPHABET_SIZE; i++){
if(nexts[i]){
nodeFree(nexts[i]);
}
}
free(node);
}
void streamCheckerFree(StreamChecker* obj) {
int i = 0;
for(i=0; i<ALPHABET_SIZE; i++){
if(obj->root[i]){
nodeFree(obj->root[i]);
}
}
free(obj);
}
/**
* Your StreamChecker struct will be instantiated and called as such:
* StreamChecker* obj = streamCheckerCreate(words, wordsSize);
* bool param_1 = streamCheckerQuery(obj, letter);
* streamCheckerFree(obj);
*/