1.多模匹配算法简介
多模式匹配在这里指的是在"一个字符串"中寻找"多个模式字符字串"的问题。一般来说,给出一个长字串和很多短模式字符串,如何最快最省的求出哪些模式字符串出现在长字串中是我们需要思考的(因为基本上大多数情况下是在给定的长字串中出现我们给的模式字串的其中几个)该算法的应用领域有很多,例如:2.1构造前缀树
比如我们现在有5个待搜索模式串:"uuidi"、"ui"、"idi"、"idk"、"di",建立如下图所示的前缀树:
每个节点中其实存在两个数组,一个是son[]数组,一个是fail[]数组,数组长度都是26。其中son[]数组存储的是该节点的孩子节点,若26个字母中对应的孩子存在,则对应的位置存储孩子的指针,否则为空。对于26个字母中不存在的那些孩子,程序肯定要跳转了,那么跳到哪里去呢?3.1中已经给出了如何跳转的描述,这里讲跳转的位置提前计算好,存入fail[]对应的位置就可以了。简单点,可以这么理解,son[]中存放的是当前节点的亲儿子,fail[]存放的是当前节点的干儿子。当亲儿子不存在时,就要找干儿子,这两个肯定儿子会存在一个(当然也只会存在一个)。程序运行时,优先寻找亲儿子,找不到的时候才会寻找干儿子,这样就能保证每次跳转都是有意义的。每跳转一次,要比较的字符串都会向前移动过一个字符。这样,要比较的字符串就不会回溯,提高了效率。
4.算法的时间复杂度分析
多模匹配算法中穷举匹配算法的时间最坏复杂度为O(m*n),其中n为目标字符串的长度,m为模式串的平均长度。类似Kmp的多模匹配算法的时间复杂度为O(n)。同时通过实验进行了验证,试验结果如下:
Trie树多模匹配时间
词典词长 |
建树时间(ms) |
穿线时间(ms) |
穷举匹配 |
KMP匹配 |
加速比 |
||
每字符跳转次数 |
匹配用时(ms) |
每字符跳转次数 |
匹配用时(ms) |
||||
5~10 |
241 |
461 |
4.75 |
216 |
1 |
130 |
1.66 |
15~20 |
670 |
1612 |
5.01 |
551 |
1 |
206 |
2.67 |
25~30 |
1068 |
2824 |
5.08 |
923 |
1 |
288 |
3.20 |
45~50 |
1863 |
5292 |
5.13 |
1632 |
1 |
443 |
3.68 |
65~70 |
2481 |
7477 |
5.15 |
2293 |
1 |
585 |
3.92 |
75~80 |
2963 |
9061 |
5.16 |
2771 |
1 |
673 |
4.11 |
其中,词典为随机生成的英语字母序列,规模为10万条。目标字符传为这10万个单词拼接起来的长字符串。从上图中可以可以看出:
(1)穷举法匹配平均每比较一个字符需要跳转5次左右,也就是平均时间复杂度在O(5*n)左右。词典规模越大时该系数越大,但是推测就在5左右,不会大很多。
(2)类似KMP的匹配算法确实做到了每一步跳转都是有效的。但是获得的加速比随着单词长度的增长会逐渐增大。
(3)根据图中的信息可以推测,对随机产生的序列穷举匹配法平均5次就可以后移目标串中的一个字符,理论上的O(m*n)的时间复杂度一般很难遇到。
(4)根据图中的信息可以推测,加速比最高应该在5左右。理论上加速比和穷举匹配中每字符的跳转次数应该非常接近。
5.源代码
如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const int MAX = 5000000;
const int Son_Num = 26; //每个孩子的最大分支个数
const int dict_max_len = 30; //词典中单词的最大长度
//---------------------Trie树的定义----------------------------
//节点定义
class Node{
public:
int flag; //节点的是否有单词出现的标记
int count; //单词出现的次数
string word; //到达该节点时出现的单词
vector word_set;//到达该节点时能够匹配上的单词
Node* failjump; //匹配失败时指针的跳转目标
Node* son[Son_Num]; //指向分支节点的指针,这里为26个
Node* fail[Son_Num]; //指向各个分支跳转节点的指针,26个
public:
Node() : flag(0),count(0),word(""){
failjump = NULL;
memset(son, NULL, sizeof(Node*) * Son_Num);
memset(fail, NULL, sizeof(Node*) * Son_Num);
}
};
//trie树的操作定义
class Trie{
private:
Node* pRoot;
private:
void print(Node* pRoot);
public:
Trie();
~Trie();
void insert(string str); //插入字符串
bool search(string str, int &count); //查询字符串
bool remove(string str); //删除字符串
void destory(Node* pRoot); //销毁Trie树
void printAll(); //打印Trie树
void failjump(); //穿线算法
void multi_match_1(string &str, int begin, int end, vector &result); //多模匹配(回溯)
void multi_match_2(string &str, int begin, int end, vector &result); //多模匹配(KMP)
};
//构造函数
Trie::Trie(){
pRoot = new Node();
}
//析构函数
Trie::~Trie(){
destory(pRoot);
}
//打印以root为根的Trie树
void Trie::print(Node* pRoot){
if(pRoot != NULL){
if(pRoot -> word != ""){
cout << pRoot -> count << " " << pRoot -> word << endl;
}
if((pRoot -> word_set).size()){
for(int i = 0; i < (pRoot -> word_set).size(); i++){
cout << "--" << (pRoot -> word_set)[i];
}
cout << endl;
}
for(int i = 0; i < Son_Num; i++){
print(pRoot -> son[i]);
}
}
}
//打印整棵树
void Trie::printAll(){
print(pRoot);
}
//插入新的单词
void Trie::insert(string str){
int index = 0;
Node* pNode = pRoot;
//不断向下搜索单词的字符是否出现
for(int i = 0; i < str.size(); i++){
index = str[i] - 'a';
//字符在规定的范围内时,才进行检查
if(index >= 0 && index < Son_Num){
//父节点是否有指针指向本字符
if(NULL == pNode -> son[index]){
pNode -> son[index] = new Node();
}
//指针向下传递
pNode = pNode -> son[index];
}else{
return;
}
}
//判断单词是否出现过
if(pNode -> flag == 1){
//单词已经出现过,计数器加1
pNode -> count++;
return;
}else{
//单词没有出现过,标记为出现,计数器加1
pNode -> flag = 1;
pNode -> count++;
pNode -> word = str;
}
}
//搜索指定单词,并返回单词出现次数(如果存在)
bool Trie::search(string str, int &count){
Node* pNode = pRoot;
int index = 0;
int i = 0;
while(pNode != NULL && i < str.size()){
index = str[i] - 'a';
if(index >= 0 && index < Son_Num){
//字符在指定的范围内时
pNode = pNode -> son[index];
i++;
}else{
//字符不再指定的范围内
return false;
}
}
//判断字符串是否出现过
if(pNode != NULL && pNode -> flag == 1){
count = pNode -> count;
return true;
}else{
return false;
}
}
//删除指定的单词
bool Trie::remove(string str){
Node* pNode = pRoot;
int i = 0;
int index = 0;
while(pNode != NULL && i < str.size()){
index = str[i] - 'a';
if(index >= 0 && index < Son_Num){
pNode = pNode -> son[index];
i++;
}else{
return false;
}
}
if(pNode != NULL && pNode -> flag == 1){
pNode -> flag = 0;
pNode -> count = 0;
return true;
}else{
return false;
}
}
//销毁Trie树
void Trie::destory(Node* pRoot){
if(NULL == pRoot){
return;
}
for(int i = 0; i < Son_Num; i++){
destory(pRoot -> son[i]);
}
delete pRoot;
pRoot == NULL;
}
//穿线算法
void Trie::failjump(){
queue q;
//将根节点的孩子的failjump域和fail[]域都设置为根节点
for(int i = 0; i < Son_Num; i++){
if(pRoot -> son[i] != NULL){
pRoot -> son[i] -> failjump = pRoot;
q.push(pRoot -> son[i]);
}else{
pRoot -> fail[i] = pRoot;
}
}
//广度遍历树,为其他节点设置failjump域和fail[]域
while(!q.empty()){
Node *cur = q.front();
q.pop();
if(cur -> failjump != NULL){
for(int j = 0; j < Son_Num; j++){
//循环设置跳转域
Node* fail = cur -> failjump;
Node* cur_child = cur -> son[j];
if(cur_child != NULL){
//当孩子存在时,设置孩子的failjump
while(fail != NULL){
if(fail -> son[j] != NULL){
//设置failjump域
cur_child -> failjump = fail -> son[j];
//设置word_set集合
if((fail -> son[j] -> word_set).size()){
cur_child -> word_set = fail -> son[j] -> word_set;
}
if(cur_child -> flag == 1){
(cur_child -> word_set).push_back(cur_child -> word);
}
break;
}else{
fail = fail -> failjump;
}
}
if(cur_child -> failjump == NULL){
cur_child -> failjump = pRoot;
if(cur_child -> flag == 1){
(cur_child -> word_set).push_back(cur_child -> word);
}
}
q.push(cur_child);
}else{
//当孩子不存在时,设置父节点fail[]域
while(fail != NULL){
if(fail -> son[j] != NULL){
//设置对应的fail[j];
cur -> fail[j] = fail -> son[j];
break;
}else{
if(fail == pRoot){
cur -> fail[j] = pRoot;
break;
}else{
fail = fail -> failjump;
}
}
}
}
}
}
}
}
//多模匹配算法1(穷举匹配)
void Trie::multi_match_1(string &str, int begin, int end, vector &result){
int count = 0;
for(int pos = 0; pos < end; pos++){
Node* pNode = pRoot;
int index = 0;
int i = pos;
while(pNode != NULL && i < pos + dict_max_len && i < end){
index = str[i] - 'a';
if(index >= 0 && index < Son_Num){
//字符在指定的范围内时
pNode = pNode -> son[index];
count++;
i++;
//若字符串出现过,输出
if(pNode != NULL && pNode -> flag == 1){
//cout << pNode -> word << endl;
result.push_back(pNode -> word);
}
}
}
}
cout << " 跳转次数:count = " << count << endl;
cout << " 每个字符平均跳转次数:" << double(count)/(end - begin) << endl;
}
//多模匹配算法2(类KMP匹配)
void Trie::multi_match_2(string &str, int begin, int end, vector &result){
int count_1 = 0, count_2 = 0, count_3 = 0;
Node* pNode = pRoot;
int index = 0;
int i = begin;
int word_set_size = 0;
while(pNode != NULL && i < end){
index = str[i] - 'a';
if(index >= 0 && index < Son_Num){
//字符在指定的范围内时
if(pNode -> son[index]){
//该孩子存在,继续向下搜索
pNode = pNode -> son[index];
count_1++;
}else if(pNode != pRoot){
//该孩子不存在,并且当前不是根节点,进行跳转
pNode = pNode -> fail[index];
count_2++;
}else{
//该孩子不存在,并且当前已经是跟节点
count_3++;
}
//若该位置有匹配词语,输出
if(word_set_size = (pNode -> word_set).size()){
for(int i = 0; i < word_set_size; i++){
//cout << (pNode -> word_set)[i] << endl;
result.push_back((pNode -> word_set)[i]);
}
}
//目标串前移
i++;
}
}
cout << " 跳转次数:count_1 = " << count_1 << " count_2 = " << count_2 << " count_3 = " << count_3 << endl;
}
//----------------------辅助代码----------------------------
//生成随机字符串
string rand_string(int min, int max){
char a[MAX+1];
int len = rand()%(max - min) + min;
for(int i = 0; i < len; i++){
a[i] = rand()%26 + 'a';
}
a[len] = '\0';
string str(a);
return str;
}
//-----------------测试代码---------------------------
//获取当前时间(ms)
long getCurrentTime(){
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec*1000 + tv.tv_usec/1000;
}
//树的基本操作,增删查改测试
void Test_1(){
//1.建立对象
Trie trie;
string str;
ifstream fin;
fin.open("dict.txt");
//2.建立Trie树
long time_1 = getCurrentTime();
while(getline(fin, str, '\n')){
trie.insert(str);
}
long time_2 = getCurrentTime();
fin.close();
//3.查找单词
str = "siubetamwm";
int count = -1;
long time_3 = getCurrentTime();
bool isFind = trie.search(str, count);
cout << "要查找的字符串:" << str << endl;
long time_4 = getCurrentTime();
if(isFind){
cout << " 该单词存在,存在次数:" << count << endl;
}else{
cout << " 该单词不存在!" << endl;
}
//4.删除单词
str = "lutgjrxjavgfkij";
cout << "要删除的字符串:" << str << endl;
bool isRemove = trie.remove(str);
if(isRemove){
cout << " 该单词存在,删除成功!" << endl;
}else{
cout << " 该单词不存在,删除失败!" << endl;
}
}
//调用词典,多模匹配测试
void Test_2(){
//1.建立对象
Trie trie;
string str;
ifstream fin;
fin.open("dict.txt");
//2.建立Trie树
long time_1 = getCurrentTime();
while(getline(fin, str, '\n')){
trie.insert(str);
}
long time_2 = getCurrentTime();
fin.close();
//3.将短字符串组合为长字符串
string long_str;
ostringstream string_out;
fin.open("dict.txt");
while(getline(fin, str, '\n')){
string_out << str;
}
fin.close();
long_str = string_out.str();
//long_str = rand_string(MAX - 20, MAX);
vector result_1;
vector result_2;
//4.在长字符串中搜索字典中的字符串(方法一:穷举)
cout << "穷举匹配:" << endl;
long time_3 = getCurrentTime();
trie.multi_match_1(long_str, 0, long_str.size(), result_1);
long time_4 = getCurrentTime();
cout << "穷举匹配完毕!" << endl;
//5.进行穿线处理
cout << "穿线处理" << endl;
long time_5 = getCurrentTime();
trie.failjump();
long time_6 = getCurrentTime();
cout << "穿线完毕" << endl;
//6.在长字符串中搜索字典中的字符串(方法二)
cout << "KMP匹配:" << endl;
long time_7 = getCurrentTime();
trie.multi_match_2(long_str, 0, long_str.size(), result_2);
long time_8 = getCurrentTime();
cout << "KMP匹配完毕" << endl;
//7.输出结果
cout << "目标字符串长度:" << long_str.size() << endl;
cout << "穷举匹配结果数量:" << result_1.size() << endl;
cout << "KMP匹配结果数量:" < result_1;
vector result_2;
//4.在长字符串中搜索字典中的字符串(方法一:穷举)
cout << "穷举匹配:" << endl;
long time_5 = getCurrentTime();
trie.multi_match_1(long_str, 0, long_str.size(), result_1);
long time_6 = getCurrentTime();
cout << "穷举匹配完毕!" << endl;
//5.进行穿线处理
cout << "穿线处理" << endl;
trie.failjump();
cout << "穿线完毕" << endl;
//6.在长字符串中搜索字典中的字符串(方法二)
cout << "KMP匹配:" << endl;
long time_7 = getCurrentTime();
trie.multi_match_2(long_str, 0, long_str.size(), result_2);
long time_8 = getCurrentTime();
cout << "KMP匹配完毕" << endl;
//7.输出结果
cout << result_1.size() << endl;
cout << result_2.size() << endl;
sort(result_1.begin(), result_1.end());
sort(result_2.begin(), result_2.end());
cout << "目标字符传长度:" << long_str.size() << endl;
if(result_1 == result_2){
cout << "两种多模匹配方式结果相符:True" << endl;
}else{
cout << "两种多模匹配方式结果不相符:False" << endl;
}
cout << "多模匹配1用时(ms):" << time_6 - time_5 << endl;
cout << "多模匹配2用时(ms):" << time_8 - time_7 << endl;
}
int main(){
//Test_1();
Test_2();
//Test_3();
}