之前只有代码,最近简单写了一点文档: 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)。