1. 程式人生 > >多模式匹配演算法:AC自動機的C++實現

多模式匹配演算法: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"