多模式匹配演算法:AC自動機的C++實現
AC自動機(Aho-Corasick automaton)是用來處理多模式匹配問題的。
基本可認為是TrieTree+KMP。其中KMP是一種單模式匹配演算法。
AC自動機的構造要點是失敗指標的設定,用於匹配失敗時跳轉到另一節點繼續匹配。同時在匹配的過程中也用來檢索其他“同尾”的模式。
失敗指標的設定:
用BFS。
對於每個節點,我們可以這樣處理:設這個節點上的字母為C,沿著他父親的失敗指標走,直到走到一個節點,他的兒子中也有字母為C的節點。然後把當前節點的失敗指標指向那個字目也為C的兒子。如果一直走到了root都沒找到,那就把失敗指標指向root。
最開始,我們把root加入佇列(root的失敗指標顯然指向自己),這以後我們每處理一個點,就把它的所有兒子加入佇列,直到全部設定完畢。
要點1:root的孩子的那一層比較特殊,若按照上述演算法,它們的失敗指標會指向自己,這會在匹配的過程中導致死迴圈。顯然root的子節點的失敗指標應指向root,我們應對這一層單獨處理。
要點2:沿著父節點的失敗指標走到root之後並不是立即將子節點的失敗指標設定為root,而是在root的子節點中找尋字母為C的節點,將它設定為失敗指標。若沒有才設定為root。這樣不會丟失模式只有一個字母的情況。
匹配過程:
一開始,Trie中有一個指標t1指向root,待匹配串中有一個指標t2指向串頭。
接下來的操作和KMP很相似:
若:t2指向的字母,是Trie樹中,t1指向的節點的兒子,那麼
①t2+1,t1改為那個兒子的編號
②如果t1所在的點可以順著失敗指標走到一個綠色點(指TrieTree中單詞結尾字母對應的節點),那麼以那個綠點結尾的單詞就算出現過了。
否則:t1順這當前節點的失敗指標向上找,直到t2是t1的一個兒子,或者t1指向根。如果t1路過了一個綠色的點,那麼以這個點結尾的單詞就算出現過了。
c++實現:
//TrieTreeNode.h #pragma once #include<iostream> using namespace std; template<class T> class TrieTreeNode { public: TrieTreeNode(int MaxBranch)//用於構造根節點 { MaxBranchNum = MaxBranch; ChildNodes = new TrieTreeNode<T>*[MaxBranchNum]; for (int i = 0; i < MaxBranchNum; i++) ChildNodes[i] = NULL; word = NULL; wordlen = 0; FailedPointer = NULL; Freq = 0; ID = -1; } public: int MaxBranchNum;//最大分支數; char* word;//單詞字串的指標 int wordlen; TrieTreeNode<T> **ChildNodes; int Freq;//詞頻統計 int ID;//構建TrieTree樹時的插入順序,可用來記錄字串第一次出現的位置 TrieTreeNode<T> *FailedPointer; };
//TrieTree.h
#pragma once
#include<iostream>
#include"TrieTreeNode.h"
#include<queue>
using namespace std;
template<class T>
class TrieTree
{
//Insert時為節點代表的單詞word分配記憶體,Delete時只修改Freq而不刪除word,Search時以Freq的數值作為判斷依據,而不是根據word是否為NULL
public:
TrieTree(const int size);
~TrieTree(){ Destroy(root); };
void Insert(const T* str);//插入單詞str
void Insert(const T* str, const int num);//插入單詞str,帶有編號資訊
int Search(const T* str);//查詢單詞str,返回出現次數
bool Delete(const T* str);//刪除單詞str
void PrintALL();//列印trie樹中所有節點對應的單詞
void PrintPre(const T* str);//列印以str為字首的單詞
void SetFailedPointer();//設定匹配失效時的跳轉指標
int MatchKMP(char* str);//返回str中出現在該TrieTree中的單詞個數
private:
void Print(const TrieTreeNode<T>* p);
void Destroy(TrieTreeNode<T>* p);//由解構函式呼叫,釋放以p為根節點的樹的空間
private:
TrieTreeNode<T>* root;
int MaxBranchNum;//最大分支數
};
template<class T>
void TrieTree<T>::Destroy(TrieTreeNode<T>* p)
{
if (!p)
return;
for (int i = 0; i < MaxBranchNum; i++)
Destroy(p->ChildNodes[i]);
if (!p->word)
{
delete[] p->word;//只是釋放了char陣列word的空間,指標word本身的空間未釋放,由後續的delete p釋放
p->word = NULL;
}
delete p;//釋放節點空間
p = NULL;//節點指標置為空
//以上的置NULL的兩句無太大意義,但是:程式設計習慣
}
template<class T>
bool TrieTree<T>::Delete(const T* str)
{
TrieTreeNode<T>* p = root;
if (!str)
return false;
for (int i = 0; str[i]; i++)
{
int index = str[i] - 'a';
if (p->ChildNodes[index])
p = p->ChildNodes[index];
else return false;
}
p->Freq = 0;
p->ID = -1;
return true;
}
template<class T>
void TrieTree<T>::PrintPre(const T* str)
{
TrieTreeNode<T>* p = root;
if (!str)
return;
for (int i = 0; str[i]; i++)
{
int index = str[i] - 'a';
if (p->ChildNodes[index])
p = p->ChildNodes[index];
else return;
}
cout << "以" << str << "為字首的單詞有:" << endl;
Print(p);
}
template<class T>
int TrieTree<T>::Search(const T* str)
{
TrieTreeNode<T>* p = root;
if (!str)
return -1;
for (int i = 0; str[i]; i++)
{
int index = str[i] - 'a';
if (p->ChildNodes[index])
p = p->ChildNodes[index];
else return 0;
}
return p->Freq;
}
template<class T>
TrieTree<T>::TrieTree(const int size)
{
MaxBranchNum = size;
root = new TrieTreeNode<T>(MaxBranchNum);//根節點不儲存字元
root->FailedPointer = root;//設定失配指標
}
template<class T>
void TrieTree<T>::Insert(const T* str)
{
TrieTreeNode<T>* p = root;
int i;
for (i = 0; str[i]; i++)
{
if (str[i]<'a' || str[i]>'z')
{
cout << "格式錯誤!" << endl;
return;
}
int index = str[i] - 'a';//下溯的分支編號
if (!p->ChildNodes[index])
p->ChildNodes[index] = new TrieTreeNode<T>(MaxBranchNum);
p = p->ChildNodes[index];
}
if (!p->word)//該詞以前沒有出現過
{
p->word = new char[strlen(str) + 1];
strcpy_s(p->word, strlen(str) + 1, str);
p->wordlen = i;//設定單詞長度
}
p->Freq++;
}
template<class T>
void TrieTree<T>::Insert(const T* str, const int num)
{
TrieTreeNode<T>* p = root;
int i;
for (i = 0; str[i]; i++)
{
if (str[i]<'a' || str[i]>'z')
{
cout << "格式錯誤!" << endl;
return;
}
int index = str[i] - 'a';//下溯的分支編號
if (!p->ChildNodes[index])
p->ChildNodes[index] = new TrieTreeNode<T>(MaxBranchNum);
p = p->ChildNodes[index];
}
if (!p->word)//該詞以前沒有出現過
{
p->word = new char[strlen(str) + 1];
strcpy_s(p->word, strlen(str) + 1, str);
p->wordlen = i;
}
p->Freq++;
if (num < p->ID || p->ID == -1)//取最小的num作為當前節點代表的單詞的ID
p->ID = num;
}
template<class T>
void TrieTree<T>::PrintALL()
{
Print(root);
}
template<class T>
void TrieTree<T>::Print(const TrieTreeNode<T>* p)
{
if (p == NULL)
return;
if (p->Freq > 0)
{
cout << "單詞:" << p->word << " 頻數:" << p->Freq;
if (p->ID >= 0)
cout << " ID:" << p->ID;
cout << endl;
}
for (int i = 0; i < MaxBranchNum; i++)
{
if (p->ChildNodes[i])
{
Print(p->ChildNodes[i]);
}
}
}
template<class T>
int TrieTree<T>::MatchKMP(char* str)
{
int count = 0;//str中出現的TrieTree中的單詞個數
char* p = str;//str中指標
TrieTreeNode<T>* node = root;//TrieTree的節點指標
while (*p)
{
if (node->ChildNodes[*p - 'a'])//當前字元匹配成功
{
TrieTreeNode<T>* temp = node->ChildNodes[*p - 'a']->FailedPointer;
while (temp != root)//在匹配的情況下,仍然沿FailedPointer搜尋,可檢索出所有模式。
{
if (temp->Freq > 0)
{
count++;
//cout << "temp->wordlen:" << temp->wordlen << endl;
cout << (int)(p - str) - temp->wordlen + 1 << " " << temp->word << endl;//列印已匹配的模式的資訊
}
temp = temp->FailedPointer;
}
node = node->ChildNodes[*p - 'a'];
p++;
if (node->Freq > 0)
{
count++;
//cout << "node->wordlen:" << node->wordlen << endl;
cout << (int)(p - str) - node->wordlen << " " << node->word << endl;//列印已匹配的模式的資訊
}
}
else//失配,跳轉
{
if (node == root)
p++;
else
node = node->FailedPointer;
}
}
return count;
}
template<class T>
void TrieTree<T>::SetFailedPointer()
{
queue<TrieTreeNode<T>*> q;
q.push(root);
while (!q.empty())
{
TrieTreeNode<T>* father = q.front();//父節點
q.pop();
for (int i = 0; i < MaxBranchNum; i++)//對每一個子節點設定FailedPointer
{
if (father->ChildNodes[i])
{
TrieTreeNode<T>* child = father->ChildNodes[i];
q.push(child);
TrieTreeNode<T>* candidate = father->FailedPointer;//從father->FailedPointer開始遊走的指標
while (true)
{
if (father == root)
{
candidate = root;
break;
}
if (candidate->ChildNodes[i])//有與child代表的字母相同的子節點
{
candidate = candidate->ChildNodes[i];
break;
}
else
{
if (candidate == root)
break;
candidate = candidate->FailedPointer;//以上兩句順序不能交換,因為在root仍可以做一次匹配
}
}
child->FailedPointer = candidate;
}
}
}
}
//main.cpp
#pragma once
#include<iostream>
#include<fstream>
#include"TrieTree.h"
using namespace std;
void test(TrieTree<char>* t)
{
char* charbuffer = new char[50];
char* cb = charbuffer;
fstream fin("d:\\words.txt");
if (!fin){
cout << "File open error!\n";
return;
}
char c;
int num = 0;
while ((c = fin.get()) != EOF)
{
if (c >= '0'&&c <= '9')
num = num * 10 + c - '0';
if (c >= 'a'&&c <= 'z')
*cb++ = c;
if (c == '\n')
{
*cb = NULL;
t->Insert(charbuffer, num);
cb = charbuffer;
num = 0;
}
}
fin.close();
}
void main()
{
TrieTree<char>* t = new TrieTree<char>(26);
char* c1 = "she";
char* c2 = "shee";
char* c3 = "he";
char* c4 = "e";
char* s = "shee";//要匹配的串
t->Insert(c1);
t->Insert(c2);
t->Insert(c3);
t->Insert(c4);
//test(t);
t->SetFailedPointer();
t->PrintALL();
cout << endl << "匹配結果為:" << endl;
int result = t->MatchKMP(s);
cout << "共匹配" << result << "處模式串" << endl;
system("pause");
}
執行結果:
對"shee"進行匹配
模式串為:"shee" "she" "he" "e"