之前只有代碼,最近簡單寫了一點文檔: google code 上的連接(老是最新)正則表達式
本文只關係到 有窮狀態自動機 ,本文也不講具體的算法,只講一些基本概念,以及用本軟件的自動機 能作什麼 , 怎麼用算法
也有其餘一些開源軟件實現了本軟件的部分功能,但整體上,不管是從功能仍是性能(運行速度,內存用量)上考慮,到目前爲止,我能找到的其它全部同類開源軟件都徹底沒法與本軟件競爭!歡迎你們一塊兒交流!api
關於自動機的形式化定義,能夠參考 wikipedia:數組
字典,有時候也叫關聯數組,能夠認爲就是一個 map<string, Value>,這是最簡單直接的表達,在 C++ 標準庫中,map是用 RBTree 實現的,固然,也能夠用 hash_map(或稱爲 unordered_map)。這些字典在標準庫中都有,不是特別追求cpu和內存效率的話,能夠直接拿來時使用。svn
可是,要知道,對於通常應用,將字典文件(假定是文本文件)加載到 map/hash_map 以後,內存佔用量要比字典文件大兩三倍。當數據源很大時,是不可接受的,雖然在如今這年代,幾G可能都算不上很大,可是,若是再乘以3,可能就是十幾二十G了,姑且不論數據加載產生的載入延遲(可能得幾十分鐘甚至一兩個小時)。函數
用 DFA 存儲字典,在不少專門的領域中是一個標準作法,例如不少分詞庫都用 DoubleArray Trie DFA 存儲詞庫,語音識別軟件通常也用 DFA 來存儲語音。性能
用作字典的 DFA 是無環DFA (ADFA, Acyclic DFA),ADFA 的狀態轉移圖是 DAG(有向無環圖)。Trie 是一種最簡單的 ADFA,同時也是(全部ADFA等價類中)最大的 ADFA。DoubleArray Trie 雖然廣爲人知,但相比 MinADFA,內存消耗要大得多。測試
ADFA 可接受的語言是有限集合,從喬姆斯基語言分類的角度,是4型語言(General的NFA和DFA是3型語言)。固然,有限,只是有限而已,但這個集合可能很是大,一個很小的ADFA也能表達很是大的字符串集合,用正則表達式舉例: [a-z]{9} ,這個正則表達式對應的DFA包含10個狀態,除終止狀態外,其餘每一個狀態都有26個轉移(圖的邊),這個語言集合的大小是 269 = 5,429,503,678,976:從 aaaaaaaaa 一直到zzzzzzzzz。想象一下,用 HashMap 或者 TreeMap,或者 DoubleArray Trie 來表達該集合的話,將是多麼恐怖!優化
目前,要編譯 febird 中的自動機庫,須要 C++11 編譯器,推薦 gcc4.7 以上版本 $ svn checkout http://febird.googlecode.com/svn/trunk/ febird-read-only $ cd febird-read-only $ make $ $ cd tools/automata/ $ make $ ll rls/*.exe # 後面會講到詳細用法 -rwxrwxrwx 1 root root 27520456 8月 17 14:34 rls/adfa_build.exe* -rwxrwxrwx 1 root root 12265256 8月 17 14:34 rls/adfa_unzip.exe* -rwxrwxrwx 1 root root 16994384 8月 17 14:34 rls/dawg_build.exe* $ $ cd netbeans-cygwin/automata/ $ make $ ll rls/*/*.exe # 這兩個 exe 用來測試自動機生成的 (key,val) 二進制數據文件 -rwxrwxr-x 1 user user 9050159 2013-08-08 16:26 rls/forward_match/on_key_value.exe -rwxrwxr-x 1 user user 8994311 2013-08-08 16:26 rls/forward_match/on_suffix_of.exe
傳統上,ADFA 只能用做 set<Key> ,也就是字符串的集合。可是,本軟件能夠把 ADFA 用做 map<Key, Value>,經過兩種方式能夠達到這個目標:ui
本軟件實現了兩種 DFA,一種爲運行速度優化,另外一種爲內存用量優化,前者通常比後者快4~6倍,後者通常比前者節省內存30~40%,具體使用哪種,由使用者作權衡決策。
不一樣的數據,DFA有不一樣的壓縮率。 對於典型的應用,爲內存優化的DFA,壓縮率通常在3倍到20之間,相比RBTree/HashMap的膨脹3倍,內存節省就有9倍到60倍!同時仍然能夠保持極高的查詢速度(keylen=16字節,QPS 在 40萬到60萬之間),爲速度優化的版本,QPS 有 250萬。下面是幾個性能數據(map指map1,爲了對齊,在一些數字前面補了0):
size(bytes) | gzip | DFA(small+slow) | DFA(big+fast) | KeyLen | QPS(big+fast) | DFA Build time | |
File1(Query) | 226,433,393 | 96,293,588 | map:101,125,415 set:073,122,182 |
170,139,298 | 016.7 | 24,000,000 | 47'seconds |
File2(URL) | 485,968,345 | 25,094,568 | map:13,990,737 set:10,850,052 |
035,548,376 | 109.2 | 00,900,000 | 16'seconds |
URL文件的冗餘比較大,雖然文件尺寸大一倍多,但最終的dfa文件卻要小得多,建立dfa用的時間也少得多!dfa文件的加載速度很是快,至關於整個文件讀取,若是文件已在緩衝區,則至關於memcpy (如今已支持mmap,因此連memcpy也省了, 2014-01-12)。
警告: 若是key中包含有隨機串,例如 guid、md5 等,會大大增長自動機的內存用量!不要自做聰明把天然的串轉化成 md5 等 hash 串再插入自動機!
本軟件包含幾個程序,用來從文本文件生成自動機,生成的自動機能夠用C++接口訪問,這樣,就將自動機的存儲與業務邏輯徹底分離。
輸入文件的格式是: key \t value, \t 是 key, value 的分隔符, \t 也能夠是其它字符,只要該分隔符不在 key 中出現便可,value 中則能夠包含分隔符。該程序本質上無論每行的的內容是什麼,只是忠實地將每行文本加入自動機。分隔符的做用體如今後面將要提到的 api: DFA_Interface::match_key 中。||
options | arguments | comments |
-o | 輸出文件:爲 尺寸 優化的自動機 | 匹配速度較慢,尺寸較小,因此是小寫o |
-O | 輸出文件:爲 速度 優化的自動機 | 匹配速度很快,尺寸較大,因此是大寫O |
-l | 狀態字節:爲 尺寸 優化的自動機,可取值 4,5,6,7 | 自動機的每一個狀態佔幾個字節,越大的數字表示自動機能支持的最大狀態數也越大, 通常5就能夠知足大多數需求了 |
-b | 狀態字節:爲 速度 優化的自動機,可取值 4,5,6,7 | |
-t | 無參數 | 輸出文本,僅用於測試 |
-c | 無參數 | 檢查自動機正確性,僅用於測試 |
生成擴展的 DFA,能夠計算 key 的 index 號(字典序號),對應 map 的第一種實現方式(map1),使用方法同 adfa_build.exe ,輸入文件的每行是一個 Key。
解壓 dfa_binary_file,按字典序將建立自動計時的輸入文件 input_text_file 的每行寫到標準輸出 stdout ,能夠接受基本 dfa(由 adfa_build.exe 生成的)文件 和擴展dfa(由 dawg_build.exe 生成的)文件
打印全部前綴爲 P n 的行 (adfa_build.exe 或 dawg_build.exe 輸入文本的行) 的後綴
打印匹配全部 text n 的前綴的 Key (adfa_build.exe 或 dawg_build.exe 輸入文本的行) 的 value, 用於測試 map 實現方法2 (Key Value 之間加分隔符)
本軟件使用了 C++11 中的新特性,因此必須使用支持 C++11 的編譯器,目前測試過的編譯器有 gcc4.7 和 clang3.1。不過爲了兼容,我提供了C++98 的接口,一旦編譯出了靜態庫/動態庫,C++11 就再也不是必需的了。
#include <febird/automata/dfa_interface.hpp>
頭文件 febird/automata/dfa_interface.hpp 中主要包含如下 class:
這個類是最主要的 DFA 接口,對於應用程序,老是從 DFA_Interface::load_from(file) 加載一個自動機(adfa_build.exe 或 dawg_build.exe 生成的自動機文件),而後調用各類查找方法/成員函數。
這個類用來實現 map1,DAWG 的全稱是 Directed Acyclic Word Graph,能夠在 ADFA 的基礎上,在匹配的同時,計算一個字符串在 ADFA 中的字典序號(若是存在的話),同時,也能夠從字典序號計算出相應的字符串,時間複雜度都是O(strlen(word))。
對應於svn代碼目錄: febird-read-only/netbeans-cygwin/automata/samples
爲了保證輸入效率,咱們須要有一個從 詞條拼音 到 詞條漢字 的映射表,好比,拼音序列 ZiDongJi 對應的詞條是 自動機 , 自凍雞 ;從而,邏輯上講,這是一個 map<string,list<string> >。
假定咱們有一個漢語詞表,該詞表的詞條超過千萬,每一個詞條多是一句話(好比名言警句),而且,由於漢語中存在多音字,從而,包含多個多音字的詞條均可能有不少種發音,這個數目在最壞狀況下是指數級的,第一個字有 X1 個讀音,第二個字有 X2 個讀音,...,n 個字的詞條就有 X1*X2*X3*...*Xn 種讀音。另外,除了多音字,還有簡拼,對於短詞來講,簡拼的重碼率很高,不實用,可是對於長詞/句,簡拼的重碼率就很低了。
若是咱們用 HashMap/std::unordered_map 或 TreeMap/std::map 保存這個註音詞典,對於普通無多音字的詞條,無任何問題。一旦有多音字, X1*X2*X3*...*Xn 多是一個很是大的數字,幾十,幾百,幾千,幾千萬都有可能,這徹底是不可接受的!一個折衷的辦法就是僅選擇機率最大的拼音,但很惋惜,有些狀況下這個拼音多是錯的!
用自動機解決該問題,最簡單的方法就是用 string kv = X1*X2*X3*...*Xn + '\t' + 漢字詞條 來逐個插入,動態 MinADFA 算法能夠保證內存用量不會組合爆炸,可是,除了內存,還有時間,如此逐個展開,時間複雜度也是指數的!
這個問題我想了好久,終於有一天,想出了一個完美的解決辦法:
這個方法很是完美!雖然 NFA 轉化 DFA 最差狀況下是 NSpace(比NP還難) 的,可是在這裏,能夠證實,這個轉化是線性的:O(n)。