首页 > 代码库 > 利用Trie树求多个字符串编辑距离的进一步优化

利用Trie树求多个字符串编辑距离的进一步优化


1.引言

        题目的意思应该是:在一个给定的字典中,求与给定的字符串的编辑距离不大于2的所有的单词。原先写过两片关于此问题的文章,那两片篇章文章给出两种解决思路:其一是暴力求解法,这种方法最容易想到。就是将词典中的词一一与给定的字符串计算编辑距离,不大于2的输出,大于2的舍弃,这种方法思路简单但是很费时间。其二根据词典中这些词之间的编辑距离建立一个以单词为节点的Trie树,遍历的时候,通过计算根节点与给定字符串的编辑距离就可以排除掉一部分分支了,然后继续计算该字符串与剩余的分支的根的编辑距离,继续排除一部分……就这样通过不断的剪枝来达到逐步排除单词。总体上来讲,这种方法是通过排除节点,减少比较次数来达到节约时间的目的,相比暴力求解法,该方法能提高10倍的速度。有兴趣的读者请参阅这两篇文章:利用Trie树求多个字符串的最小编辑距离  和   Trie树求多个字符串最小编辑距离的空间优化

        这里再介绍一种方法,速度提高的更多。思路仍然是将词典中的单词建立Trie树,但是这里只是建立普通的Trie树,将每个字母作为一个节点。只是在遍历的时候讲究一些技巧,就可以在很少的时间内将所有与给定字符串编辑距离不大于2的单词找出来了。

        通俗来讲,从Trie树树根到叶子是标识一个单词的完整路径一条完整的路径A,这条路径上有数个节点。假定给定的字符串也在Trie树中,那么也必然形成这个字符串的独特路径B。比如:Trie树中的单词abcde的路径:a -> b -> c -> d -> e,给定的字符串abrde形成的路径:a -> b -> r -> d -> e。如果A和B能够大部分重合,不重合的部分不大于2的话,那么就是符合我们要求的单词。刚才的单词abcde和abrde只有在字母r这里不重合,别处都重合,因此符合要求,应将abcde输出。

        我们不能期望所有单词形成的路径都与给定的字符串重合(那样词典就蜕化为一个单词了),只要他们大部分重合就可以,这些单词都被赋予两项权利,允许他们在两处不重合,这些权利是:

        (1)增加一个字符

        (2)删除一个字符

        (3)修改一个字符

        (4)相邻两个字符交换

        只要将给定的字符串经过上述的操作变换后与字典中的路径重合即可。注意:这四种方式一次只能使用一种,最多只能用两次。这也就是说,词典中单词与给定字符串的编辑距离不大于2。

        明白了这一点,我们就可以构造算法,将给定的字符串经过最多两次变换后,检查词典中有没有对应的路径存在即可。由于上述的变换可能在给定的字符串的任意位置发生,因此要将所有的可能的情况都考虑到,所以用递归的思路来解决这个问题。具体的思路请直接参考源代码。


2.时间复杂度分析

        测试环境:词典中词的长度在5~30之间,目标字符串长度为6,随机取10组数据进行测试进行测试。

        测试结果:

序号Trie树搜索时间暴力匹配搜索时间时间比值
13.687960.46%
23.758460.44%
33.88100.47%
43.68210.44%
53.618100.45%
63.688120.45%
73.68320.43%
83.718210.45%
93.618160.44%
103.738130.46%
平均值3.677817.70.45%

        结果对比:

        (1)本文结果:3.67ms        0.45%

        (2)前文结果:64.4ms        7.90%

        (3)暴力匹配:817.7ms      100%

        将上表中的数据同前面文章中的数据进行对比,从上面的对比中可以看出本文的方法在性能上有了极大的提升。


3.源代码


#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <sys/time.h>

using namespace std;

const int SON_NUM = 26;		//树中每个节点分支的数量
const int MAX = 30;		//随机字符串的最大长度
const int Y = 30;		//字典中字符串的最大长度


/**
 *节点定义
 *
 */
class Node{
	public:
		char c;			//节点所代表的单词
		int flag;		//节点是否是单词的尾节点
		int count;		//词典中相同单词的数量
		string word;		//词典中的单词
		Node* son[SON_NUM];	//指向分支节点的指针
	public:
		Node() : flag(0), count(0), word(""){
			memset(son, NULL, sizeof(Node*)*SON_NUM);
		}
};


/*
 *Trie树的操作定义
 *
 */
class Trie{
	public:
		Node* pRoot;		//根节点
	public:
		Trie();
		~Trie();			
		void print(Node* r);			//打印某节点
		void destory(Node* r);			//销毁某节点
		void insert(string str);		//插入单词
		bool search(string str, int &count);	//搜索单词(普通搜索)
		bool search_2(string str, int pos, Node* pRoot, vector<string> &word_set, int lev);	//搜索单词(特殊搜索)
};

//构造函数
Trie::Trie(){
	pRoot = new Node();
}

//析构函数
Trie::~Trie(){
	destory(pRoot);
}

//打印某个节点
void Trie::print(Node* r){
	if(r != NULL){
		cout << r -> c << endl;
		for(int i = 0; i < SON_NUM; i++){
			print(r -> son[i]);
		}
		cout << endl;
	}
}

//销毁某个节点
void Trie::destory(Node* r){
	if(NULL == r){
		return;
	}
	for(int i = 0; i < SON_NUM; i++){
		destory(r -> son[i]);
	}
	delete r;
	r = NULL;
}

//向Trie树插入单词
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];
			pNode -> c = str[i];
		} else {
			return;
		}
	}
	//判断单词是否出现过
	if(pNode -> flag == 1){
		//单词已经出现过
		pNode -> count++;
	} else {
		//单词没有出现过
		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;
	} 
}


//特殊搜索:在词典中搜索与给定字符串编辑距离不大于lev的单词
bool Trie::search_2(string str, int pos, Node* pNode, vector<string> &word_set, int lev){
	
	//节点不为空的情况下操作	
	if(NULL != pNode){

		//1.增加字母:
		if(lev > 0 && pos < str.size() + 2){
			int pos_1 = pos;
			int lev_1 = lev;
			lev_1--;
			for(int j = 0; j < SON_NUM; j++){
				//增加任一个字母均可
				if((pos_1 < str.size() && j != (str[pos_1] - 'a') && NULL != pNode -> son[j]) ||
					(pos_1 >= str.size() && NULL != pNode -> son[j])){
					//Trie树中存在的任何一个字母都要顾及到
					Node* pCur_1 = pNode -> son[j];
					if(pos_1 == str.size() && NULL != pCur_1 && pCur_1 -> flag == 1){
						word_set.push_back(pCur_1 -> word);
					}
					//继续比较
					search_2(str, pos_1, pCur_1, word_set, lev_1);
				}
			}
		}
		
		//2.删除字母:
		if(lev > 0 && pos < str.size()){
			Node* pCur_2 = pNode;
			int pos_2 = pos;
			int lev_2 = lev;
			pos_2++;
			lev_2--;
			//继续比较
			if(pos_2 == str.size() && NULL != pCur_2 && pCur_2 -> flag == 1){
				word_set.push_back(pCur_2 -> word);
			}
			search_2(str, pos_2, pCur_2, word_set, lev_2);
		}
	
		//3.修改字母:
		if(lev > 0 && pos < str.size()){
			int pos_3 = pos;
			int lev_3 = lev;
			int index_3 = str[pos_3] - 'a';
			pos_3++;
			lev_3--;
			for(int j = 0; j < SON_NUM; j++){
				if(j != index_3 && NULL != pNode -> son[j]){
					Node* pCur_3 = pNode -> son[j];
					//继续比较
					if(pos_3 == str.size() && NULL != pCur_3 && pCur_3 -> flag == 1){
						word_set.push_back(pCur_3 -> word);
					}
					search_2(str, pos_3, pCur_3, word_set, lev_3);
				}
			}	
		}
	
		//4.交换字母:
		if(lev > 0 && pos < str.size() - 1){
			Node* pCur_4 = pNode;
			int pos_4 = pos;
			int lev_4 = lev;
			lev_4--;
			int index_up = str[pos_4] - 'a';
			int index_down = str[pos_4+1] - 'a';
			if(NULL != pCur_4 -> son[index_down] && 
				NULL != pCur_4 -> son[index_down] -> son[index_up]){
				pCur_4 = pCur_4 -> son[index_down] -> son[index_up];
				pos_4 = pos_4 + 2;
				//继续比较
				if(pos_4 == str.size() && NULL != pCur_4 && pCur_4 -> flag == 1){
					word_set.push_back(pCur_4 -> word);
				}
				search_2(str, pos_4, pCur_4, word_set, lev_4);
			}
		}
			
		//5.直接匹配;
		if(pos < str.size() && NULL != pNode -> son[str[pos] - 'a']){
			Node* pCur_5 = pNode;
			int pos_5 = pos;
			int lev_5 = lev;
			int index_5 = str[pos_5] - 'a';
			pCur_5 = pCur_5 -> son[index_5];
			pos_5++;
			if(pos_5 == str.size() && NULL != pCur_5 && pCur_5 -> flag == 1){
				word_set.push_back(pCur_5 -> word);
			}
			search_2(str, pos_5, pCur_5, word_set, lev_5);
		}
	}
}



/*
 *辅助函数
 *
 */

//求两个字符串的最断编辑距离
int edit_length(string &x, string &y){
	int xlen = x.length();
	int ylen = y.length();
	int edit[3][Y+1];
	memset(edit, 0, sizeof(edit));
	
	int i = 0;
	int j = 0;
	for(j = 0; j <= ylen; j++){
		edit[0][j] = j;
	}
	for(i = 1; i <= xlen; i++){
		edit[i%3][0] = edit[(i-1)%3][0] + 1;
		for(j = 1; j <= ylen; j++){
			if (x[i-1] == y[j-1]) {
				edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1),
							edit[(i-1)%3][j-1]);
			} else {
				if(i >= 2 && j >= 2 && x[i-2] == y[j-1] && x[i-1] == y[j-2]){
					edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1),
										min(edit[(i-1)%3][j-1] + 1, edit[(i-2)%3][j-2] + 1));
				} else {
					edit[i%3][j] = min(min(edit[i%3][j-1] + 1, edit[(i-1)%3][j] + 1),
										edit[(i-1)%3][j-1] + 1);
				}
			}
		}
	}
	return edit[(i-1)%3][j-1];
}


//生成随机字符串
string rand_string(int len){
	srand(time(NULL));
	char a[MAX+1];
	for(int i = 0; i < len; i++){
		a[i] = rand()%26 + 'a';
	}
	a[len] = '\0';
	string str(a);
	return str;
}

//获取当前时间(微秒)
long getCurrentTime(){
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec*1000*1000 + tv.tv_usec;
}

int main(){
	//1.定义对象
	Trie trie;
	string str;
	ifstream fin;

	//2.建立Trie树
	fin.open("dict.txt");
	if(!fin){
		cout << "open file fail!" << endl;
	}
	while(getline(fin, str, '\n')){
		trie.insert(str);
	}
	fin.close();

	/*
	//3.验证Trie树建立成功
	fin.open("dict.txt");
	if(!fin){
		cout << "open file fail!" << endl;
	}
	int count = 0;
	while(getline(fin, str, '\n')){
		bool isFind = trie.search(str, count);
		cout << count << "  " << str << endl;
	}
	fin.close();

	*/

	//3.产生随机字符串
	string rand_str = rand_string(6);
	cout << "随机字符串为:" << rand_str << endl;
	
	//4.利用Trie树计算结果
	vector<string> word_set_1;
	long time_3 = getCurrentTime();
	trie.search_2(rand_str, 0, trie.pRoot, word_set_1, 2);
	long time_4 = getCurrentTime();
	
	//消除重复元素
	sort(word_set_1.begin(), word_set_1.end());
	word_set_1.erase(unique(word_set_1.begin(), word_set_1.end()), word_set_1.end());


	//5.利用暴力匹配计算结果	
	vector<string> word_set_2;
	vector<string> word_dict;
	fin.open("dict.txt");
	if(!fin){
		cout << "打开文件失败!" << endl;
	}
	while(getline(fin, str, '\n')){
		word_dict.push_back(str);
	}
	fin.close();
	int size = word_dict.size();
	long time_5 = getCurrentTime();
	for(int j = 0; j < size; j++){
		if(edit_length(word_dict[j], rand_str) < 3){
			word_set_2.push_back(word_dict[j]);
		}
	}
	long time_6 = getCurrentTime();
	fin.close();

	//6.结果比较
	sort(word_set_1.begin(), word_set_1.end());
	sort(word_set_2.begin(), word_set_2.end());

	cout << "word_set_1的大小:" << word_set_1.size() << endl;
	cout << "结果为:";
	for(int i = 0; i < word_set_1.size(); i++){
		cout << "  " << word_set_1[i];
	}
	cout << endl;

	cout << "word_set_2的大小:" << word_set_2.size() << endl;
	cout << "结果为:";
	for(int i = 0; i < word_set_2.size(); i++){
		cout << "  " << word_set_2[i];
	}
	cout << endl;

	if(word_set_1 == word_set_2){
		cout << "验证正确" << endl;
	} else {
		cout << "验证错误" << endl;
	}
	
	//7.时间比较
	//cout << "建立Trie树用时(微秒):" << time_2 - time_1 << endl;
	cout << "Trie树搜索用时(ms):" << double(time_4 - time_3)/1000 << endl;
	cout << "暴力搜索用时(ms):"   << double(time_6 - time_5)/1000 << endl;
	cout << "百分比:" << double(time_4 -time_3)/(time_6 - time_5) << endl;
}