有窮自動機ios
定義一個狀態的集合Q,而一個狀態q經過一個轉移函數δ則可轉移到另一個狀態q', 而自動有一個初始狀態,還會有一個接受狀態,到達了接受狀態則說明該一系列的輸入時符號該自動機所肯定的模式。在編譯原理的課上,接觸了有窮自動機,而正則表達式和有窮自動自動機是等價的,程序設計語言的詞法則能夠用正則表達式來定義。可見,有窮自動機用來作字符匹配那是再合適不過了。
一個簡單的有窮自動機
如圖所示,0爲起始狀態,1爲接受狀態(結束狀態),箭頭表示狀態轉移。能夠略微地推導一下,aba,aaa,abba都能符合該模式,固然對於這個比較簡單的例子,咱們能夠發現只要是包含a的字符串都能符合該有窮自動機。
固然要編程實現一個專門匹配某個字符串的有窮自動機,還得對模式字符串進行分析。咱們的有窮狀態有若干個狀態,從狀態0到狀態n。若是將狀態比做是一條大路的話,那麼就有各類小路在各類狀態中轉換,例如《算法導論》裏面有以下的插圖:
能夠這樣看,當輸入後狀態轉移向右,則愈來愈接近接受狀態,然而有些轉移向後,則前面的工做功虧一簣,須要展轉再回去(這就比如辛辛苦苦三十年,一朝貶到老家前)。這就是狀態轉換的真諦,永遠在這裏面徘徊轉換,在這個肯定性有窮自動機裏面,只有一條出路,那就是7。咋一看好像和字符串匹配沒有多大關係,實則字符串的匹配也是一些狀態,當與當前的字符不匹配時就退回到某個狀態再作打算,繼續讓後面的字符串作匹配,當一路向前匹配的時候,則將達到接受狀態。
一、 後綴函數σ
σ(x) 是x的後綴P的最長前綴的長度。定義式爲σ(x) = max{k:Pk 是 x的後綴,長度爲k}, 例如P = ab, 則σ(ccab) = 2,而(cdca) = 1,如圖所示。
上面是x,而下面則是P。可見是用P的前綴去作x的後綴,所得的最長的匹配長度即爲k。而變遷函數說明有多少個匹配了則說明到了狀態幾。也就是說 δ(q,a) = σ(Pq a) ,表示在狀態q的時候輸入字符a,那麼即等於字符串Pq(已經有q位匹配)加上一個a計算獲得的最長前綴長度。(數學符號一行,賽過千言萬語啊!)一切的模型仍在於一一字符比對當中,用P的前綴去作x的後綴。終態函數φ(Ti) = σ(Ti),即後綴函數的值就是Ti做爲輸入的終止狀態值。
下面是程序,分爲兩部分,一部分從頭至尾掃描字符串,另一部分是計算變遷轉移表的函數,狀態變遷以二維數組的形式存儲。先輸入兩個字符,而後輸入字符集,以$結束,而後能夠求出結果,編程細節上可能還有漏洞。
#include <string>
#include <vector>
#include <iostream>
using namespace std;
void cinContextAndParten(string &context, string &parten);
bool IsRearFix(string x, string y);
void ComputeTransition(string parten, vector<char> charset, int *tranTable[]);
void DFAMatch(string context, int *tranTable[], string parten, vector<char> charset);
int _tmain(int argc, _TCHAR* argv[])
{
string context; // 內容
string parten; // 要匹配的模式
char ch;
cinContextAndParten(context, parten); // 輸入兩個字符串
cout << "請輸入字符集:" << endl;
vector<char> charset;
while( cin>>ch && ch != '$')
{
charset.push_back(ch); // 放入字符集中
}
int charsetLen = charset.end() - charset.begin(); // 字符集大小
int partenLen = parten.length(); // 用於構建轉移函數
int **tranTable = new int*[partenLen+1]; // 這個就是狀態轉換表包括一個狀態
for(int i=0; i<partenLen+1; i++)
{
tranTable[i] = new int[charsetLen];
}
// 先計算出狀態轉換表
ComputeTransition(parten, charset, tranTable);
// 而後進行字符串匹配
DFAMatch(context, tranTable, parten, charset);
system("pause");
return 0;
}
// 輸入兩個字符串
void cinContextAndParten(string &context, string &parten)
{
std::cout << "please input context : \n";
std::cin >> context;
std::cout << "please input parten : \n";
std::cin >> parten;
}
//---------------------------------- 有窮自動機--------------------------------------------//
// 傳入參數目標字符串,轉移函數信息,模式
void DFAMatch(string context, int *tranTable[], string parten, vector<char> charset)
{
int n = context.length(); // 獲取context長度
int m = parten.length();
if( n < m) // 先檢查長短
return;
int q = 0; // 轉移狀態
int k = 0; // k 字符context[i] 在字符集中下標
for(int i= 0; i<n; i++)
{
vector<char>::iterator it; // 迭代器
for(it = charset.begin(); it != charset.end(); it++)
{
if( *it == context[i])
{
k = it - charset.begin(); // 下標
break;
}
}
q = tranTable[q][k]; // 從狀態轉移表中進行查找下一個狀態
if (q == m) // 爲接受狀態
cout << i - m+1<< endl; // 輸出匹配位置
}
}
// 根據當前模式和字母表,計算出狀態轉換表
void ComputeTransition(string parten, vector<char> charset, int *tranTable[])
{
int m = parten.length();
for(int q=0; q<m+1; q++) // m+1個狀態
{
vector<char>::iterator pch;
for(pch = charset.begin(); pch!= charset.end(); pch++)
{
int k = min(m+1, q+2);
string tmp = parten.substr(0,q); // 字符串鏈接
tmp.push_back(*pch); // 往尾部添加一個字符
do
{
k--; // 此處顯示出了KMP的玄妙,KMP將減小一些沒必要要的檢測
}while(! IsRearFix( parten.substr(0,k), tmp)); // 判斷Pk是不是Pqa 的後綴
// 給狀態轉移表賦值
tranTable[q][pch-charset.begin()] = k; // 得到狀態轉移函數q 遇到字符ch 到k
}
}
}web