数据结构(c++)--map划分词典的相似单词

        这次分享一下看到的一个map对于相似单词的划分的示例。

        首先,我们需要一个存放了很多单词的字典,在上一篇博客中,我已经做好了相关的处理 ,可以参考点击打开链接

        下面我们切入正题。

         在我们所用过的英文单词中,许多单词都和其它的单词是相似的,而这些往往记忆起来是特别头疼的,大家都懂的。例如,对于单词wine,替换第一个字母,可以有dine、fine、line、mine、pine、vine等。替换第三个字母,就有了wide、wife、wipe、wire等。替换第四个字母,就有了wind、wing、wink、wins等。于是,通过这些变换,我们就得到了15个不同的单词(当然,还可以得到更多的单词)。现在我们想做的就是,为字典中的每一个单词都量身定做一个“集合”,这些集合的构成元素就是通过变换某一个位置的字母所能得到的“合法”的单词。

       下面来让看一下字典中单词长度的大致分布:

#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<fstream>
#include<iomanip>

using namespace std;

/*
此函数用于从文件中获取单词并存储到向量中
*/
vector<string> getWords(const string &path)
{
	ifstream input(path);
	vector<string> words;
	if (!input)
	{
		cerr << "无法打开文件" << endl;
		exit(EXIT_FAILURE);
	}
	string word;
	while (input >> word)
	{
		words.push_back(word);
	}
	return words;
}

int main()
{
	string path = "D://newfile.txt";     //文件的路径可以自行修改
	vector<string> words = getWords(path);
	cout << "文件的大小为:" << words.size() << endl;
	map<int, vector<string> > wordsMap;
	for (int i = 0; i < words.size(); i++)
	{
		wordsMap[words[i].size()].push_back(words[i]);
	}
	cout << "单词的字母个数" << "\t对应的单词的个数" << endl;
	for (auto itr = wordsMap.begin(); itr != wordsMap.end(); ++itr)
	{
		cout << left << setw(10)<< itr->first << right << setw(10) << itr->second.size() << endl;
	}
	return 0;
}
       程序运行的结果如下所示:

        从上图可以看出单词的大致分布情况。事实上,哪些最具有可变性的是有3、4、5个字母的单词,而较长的单词却消耗最多的时间。

        首先,我们先来说一下几个基础的模块:

(1)读取文件中单词的模块,我们交个函数getWords来实现,这在上面的程序中已经展现过了。

(2)判断两个单词是不是相似(只有一个位置的字母不同),函数为oneCharOff。

(3)输出函数printHighChangebles,用于输出最终的结果。

(4)对字典中的所有单词进行“相似”判断的函数computeAdjacentWords(有三个版本)。

       下面先看检测两个单词是否只有一个字母不同的函数:

/*
此函数用于比较两个单词是否是只有一个位置的字母不同
*/
bool oneCharOff(const string &word1, const string &word2)
{
	if (word1.size() != word2.size())
		return false;
	int diffs = 0;
	for (int i = 0; i < word1.size(); i++)
	{
		if (word1[i] != word2[i])
			if (++diffs > 1)
				return false;
	}
	return diffs == 1;
} 
       输出最终结果的函数,在这里我们使用map来存放最终的结果,map的键为我们关心的单词,值为存放所有相似的单词的vector:
void printHighChangeables(const map< string, vector<string> > &adjWords, int minWords = 15)
{
	map<string, vector<string> >::const_iterator itr;
	for (itr = adjWords.begin(); itr != adjWords.end(); ++itr)
	{
		const pair<string, vector<string> > &entry = *itr;
		const vector<string> &words = entry.second;
		if (words.size() >= minWords)
		{
			cout << entry.first << "(" << words.size() << "):";
			for (int i = 0; i < words.size(); i++)
			{
				cout << " " << words[i];
			}
			cout << endl;
		}
	}
}
         在这里,我了测试几个不同的computeAdjacentWords函数的差别,我加了个可以计算时间的类的实现:

//====================Timer类的定义=========================
class Timer
{
public:
	Timer();
	double elapsed_time();  //消逝,时间过去
	void reset();
private:
	clock_t start_time;
};
Timer::Timer()
{
	start_time = clock();
}
double Timer::elapsed_time()
{
	clock_t end_time = clock();
	return ((double)(end_time - start_time)) / ((double)CLK_TCK);
}
void Timer::reset()
{
	start_time = clock();
}
//======================Timer类定义结束=====================
       下面来看一下几个不同的computeAdjacentWords函数的实现:

方法一:

       在这个方法里,我们就直接对单词表进行循环处理,依次比对所有的单词:
/*
此函数数用于计算字典中各个单词通过变动一个字母所能得到的单词的集合
*/
map<string, vector<string> > computeAdjacentWords(const vector<string> &words)
{
	map<string, vector<string> > adjWords;
	for (int i = 0; i < words.size(); i++)
	{
		for (int j = i + 1; j < words.size(); j++)
		{
			if (oneCharOff(words[i], words[j]))
			{
				adjWords[words[i]].push_back(words[j]);
				adjWords[words[j]].push_back(words[i]);
			}
		}
	}
	return adjWords;
}
       将所有的程序合并在一起后如下:
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<fstream>
#include<ctime>
#include<iomanip>

using namespace std;


//====================Timer类的定义=========================
class Timer
{
public:
	Timer();
	double elapsed_time();  //消逝,时间过去
	void reset();
private:
	clock_t start_time;
};
Timer::Timer()
{
	start_time = clock();
}
double Timer::elapsed_time()
{
	clock_t end_time = clock();
	return ((double)(end_time - start_time)) / ((double)CLK_TCK);
}
void Timer::reset()
{
	start_time = clock();
}
//======================Timer类定义结束=====================

void printHighChangeables(const map< string, vector<string> > &adjWords, int minWords = 15)
{
	map<string, vector<string> >::const_iterator itr;
	for (itr = adjWords.begin(); itr != adjWords.end(); ++itr)
	{
		const pair<string, vector<string> > &entry = *itr;
		const vector<string> &words = entry.second;
		if (words.size() >= minWords)
		{
			cout << entry.first << "(" << words.size() << "):";
			for (int i = 0; i < words.size(); i++)
			{
				cout << " " << words[i];
			}
			cout << endl;
		}
	}
}

/*
此函数用于比较两个单词是否是只有一个位置的字母不同
*/
bool oneCharOff(const string &word1, const string &word2)
{
	if (word1.size() != word2.size())
		return false;
	int diffs = 0;
	for (int i = 0; i < word1.size(); i++)
	{
		if (word1[i] != word2[i])
			if (++diffs > 1)
				return false;
	}
	return diffs == 1;
}

/*
此函数数用于计算字典中各个单词通过变动一个字母所能得到的单词的集合
*/
map<string, vector<string> > computeAdjacentWords(const vector<string> &words)
{
	map<string, vector<string> > adjWords;
	for (int i = 0; i < words.size(); i++)
	{
		for (int j = i + 1; j < words.size(); j++)
		{
			if (oneCharOff(words[i], words[j]))
			{
				adjWords[words[i]].push_back(words[j]);
				adjWords[words[j]].push_back(words[i]);
			}
		}
	}
	return adjWords;
}

/*
此函数用于从文件中获取单词并存储到向量中
*/
vector<string> getWords(const string &path)
{
	ifstream input(path);
	vector<string> words;
	if (!input)
	{
		cerr << "无法打开文件" << endl;
		exit(EXIT_FAILURE);
	}
	string word;
	while (input >> word)
	{
		words.push_back(word);
	}
	return words;
}

int main()
{
	string path = "D://newfile.txt";     //文件的路径可以自行修改
	vector<string> words = getWords(path);
	cout << "文件的大小为:" << words.size() << endl;
	Timer time;
	map<string, vector<string> > adjWords=computeAdjacentWords(words);
	printHighChangeables(adjWords,3);
	cout << "时间为:" << time.elapsed_time() << endl;
	return 0;
}
        运行的结果如下:



     

          从上图看可以看出,这个程序的效率是底下的,共用了1887.01秒。

方法二:

         对单词按照个数进行分组,这样的话在比较的时候,我们就可以避免不同长度的单词之间的比较(当然就无可避免的要多使用一部分的空间了),先来看程序,看效果,之后我们再来说原理:

/*
此函数数用于计算字典中各个单词通过变动一个字母所能得到的单词的集合
*/
map<string, vector<string> > computeAdjacentWords(const vector<string> &words)
{
	map<string, vector<string> > adjWords;
	map<int, vector<string> > wordsByLength;
	for (int i = 0; i < words.size(); i++)
	{
		wordsByLength[words[i].size()].push_back(words[i]);
	}
	for (auto itr = wordsByLength.begin(); itr != wordsByLength.end(); ++itr)
	{
		const vector<string> &groupsWords = itr->second;
		for (int i = 0; i < groupsWords.size(); i++)
		{
			for (int j = i + 1; j < groupsWords.size(); j++)
			{
				if (oneCharOff(groupsWords[i], groupsWords[j]))
				{
					adjWords[groupsWords[i]].push_back(groupsWords[j]);
					adjWords[groupsWords[j]].push_back(groupsWords[i]);
				}
			}
		}
	}
	return adjWords;
}
        下面是程序的整体代码:

#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<fstream>
#include<ctime>
#include<iomanip>

using namespace std;


//====================Timer类的定义=========================
class Timer
{
public:
	Timer();
	double elapsed_time();  //消逝,时间过去
	void reset();
private:
	clock_t start_time;
};
Timer::Timer()
{
	start_time = clock();
}
double Timer::elapsed_time()
{
	clock_t end_time = clock();
	return ((double)(end_time - start_time)) / ((double)CLK_TCK);
}
void Timer::reset()
{
	start_time = clock();
}
//======================Timer类定义结束=====================

void printHighChangeables(const map< string, vector<string> > &adjWords, int minWords = 15)
{
	map<string, vector<string> >::const_iterator itr;
	for (itr = adjWords.begin(); itr != adjWords.end(); ++itr)
	{
		const pair<string, vector<string> > &entry = *itr;
		const vector<string> &words = entry.second;
		if (words.size() >= minWords)
		{
			cout << entry.first << "(" << words.size() << "):";
			for (int i = 0; i < words.size(); i++)
			{
				cout << " " << words[i];
			}
			cout << endl;
		}
	}
}

/*
此函数用于比较两个单词是否是只有一个位置的字母不同
*/
bool oneCharOff(const string &word1, const string &word2)
{
	if (word1.size() != word2.size())
		return false;
	int diffs = 0;
	for (int i = 0; i < word1.size(); i++)
	{
		if (word1[i] != word2[i])
			if (++diffs > 1)
				return false;
	}
	return diffs == 1;
}

/*
此函数数用于计算字典中各个单词通过变动一个字母所能得到的单词的集合
*/
map<string, vector<string> > computeAdjacentWords(const vector<string> &words)
{
	map<string, vector<string> > adjWords;
	map<int, vector<string> > wordsByLength;
	for (int i = 0; i < words.size(); i++)
	{
		wordsByLength[words[i].size()].push_back(words[i]);
	}
	for (auto itr = wordsByLength.begin(); itr != wordsByLength.end(); ++itr)
	{
		const vector<string> &groupsWords = itr->second;
		for (int i = 0; i < groupsWords.size(); i++)
		{
			for (int j = i + 1; j < groupsWords.size(); j++)
			{
				if (oneCharOff(groupsWords[i], groupsWords[j]))
				{
					adjWords[groupsWords[i]].push_back(groupsWords[j]);
					adjWords[groupsWords[j]].push_back(groupsWords[i]);
				}
			}
		}
	}
	return adjWords;
}


/*
此函数用于从文件中获取单词并存储到向量中
*/
vector<string> getWords(const string &path)
{
	ifstream input(path);
	vector<string> words;
	if (!input)
	{
		cerr << "无法打开文件" << endl;
		exit(EXIT_FAILURE);
	}
	string word;
	while (input >> word)
	{
		words.push_back(word);
	}
	return words;
}

int main()
{
	string path = "D://newfile.txt";     //文件的路径可以自行修改
	vector<string> words = getWords(path);
	cout << "文件的大小为:" << words.size() << endl;
	Timer time;
	map<string, vector<string> > adjWords=computeAdjacentWords(words);
	printHighChangeables(adjWords,3);
	cout << "时间为:" << time.elapsed_time() << endl;
	return 0;
}
       程序的执行结果为:



       这个程序的执行结果为321.098,这个时间是方法一种结果1887.01的1/6左右,这是什么原因呢?这就分组带来的好处,减少了比对的次数。简单来说一下,假设我们有n个单词,那么相对于 第一种方法而言,这些单词要进行t1=n*(n-1)/2次的比较,而对于方法二呢,我们就是将n进行了分组,假设分为了k组,分别为m1,m2,.......,mk,则n=m1+m2+...+mk,那么比较的次数为t2=m1*(m1-1)/2+m2*(m2-1)/2+....+mk*(mk-1)/2,t1-t2=[(n²-(m1²+m2²+...+mk²)]/2,将n=m1+m2+....+mk代入化简,由于结果比较繁杂,这里就不进行展开了,有兴趣的可以自己化简下,显然是t1比t2大的,特别是当m1=m2=....=mk时,这个时候我们是可以取到极值的,t2约为t1的1/k,这个在数学中有相关的定理可以佐证。所以,分组为我们带来了显著的好处。

方法三:

       在这个方法中,我们同样采用方法二中的分组,但是呢,我们都知道,比较时浪费 之间的,所以如果想进一步缩短程序的时间,我们就需要把比较给省掉。回想一下,在数据结构中,什么思想帮助我们能够在O(1)的情况下找到一个指定的数,当然是做哈希了。这里面呢,我们也引入这种思想,然而我们不是自己去做哈希,而是使用这种思想而已,我们使用map,然后对每一组中的所有单词,遍历删除它各个位置的字母,然后剩下的部分就可以作为字典的键了,键值一样的单词会被分配到一起,这一步就是相当于我们的两个单词之间的比较了。举个例子,对于fine,wine,nine,它们显然是一组的,那么对于位置0处,我们删除后生下来“ine”,那么“ine”就可以作为字典的键,然后这些单词作为值放在一起,而对于boot这种单词,删除第0位置字母后,剩余的部分为“oot”,不会对“ine”造成干扰,这样我们就做好了单词相似性的“比较”了,然而我们省去了耗时的单词间字母的比较,所以就提升了效率,当然,这里会产生一定的空间消耗,这也就是所谓的以空间换时间,对于现在而言,空间已经是越来越廉价了,所以这个方法是“好的”。
      此函数的代码如下:
/*
此函数数用于计算字典中各个单词通过变动一个字母所能得到的单词的集合
*/
map<string, vector<string> > computeAdjacentWords(const vector<string> &words)
{
	map<string, vector<string> > adjWords;
	map<int, vector<string> > wordsByLength;

    //分组
	for (int i = 0; i < words.size(); i++)
	{
		wordsByLength[words[i].size()].push_back(words[i]);
	}
	
	//遍历单词表中的每一组
	for (auto itr = wordsByLength.begin(); itr != wordsByLength.end(); ++itr)
	{
		const vector<string> &groupsWords = itr->second;
		int groupNum = itr->first;

		//处理每一组中的单词的各个字母
		for (int i = 0; i < groupNum; i++)
		{
			map<string, vector<string> > repToWord;

			//处理该组中的所有单词
			for (int j = 0; j < groupsWords.size(); j++)
			{
				string rep = groupsWords[j];
				rep.erase(i, 1);    //删除第i个位置的字母
				repToWord[rep].push_back(groupsWords[j]);  //只有第i个字母不同的单词会被放在一起
			}

			//处理第i个位置中的单词组成的map
			for (auto itr2 = repToWord.begin(); itr2 != repToWord.end(); ++itr2)
			{
				const vector<string> &clique = itr2->second;
				if (clique.size() >= 2)   //说明有两个以上的单词相似
				{
					for (int i = 0; i < clique.size(); i++)
					{
						for (int j = i+1; j < clique.size(); j++)
						{
							adjWords[clique[i]].push_back(clique[j]);
							adjWords[clique[j]].push_back(clique[i]);
						}
					}
				}
			}
		}

	}
	return adjWords;
}
        此程序的完整代码为:
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<fstream>
#include<ctime>
#include<iomanip>

using namespace std;


//====================Timer类的定义=========================
class Timer
{
public:
	Timer();
	double elapsed_time();  //消逝,时间过去
	void reset();
private:
	clock_t start_time;
};
Timer::Timer()
{
	start_time = clock();
}
double Timer::elapsed_time()
{
	clock_t end_time = clock();
	return ((double)(end_time - start_time)) / ((double)CLK_TCK);
}
void Timer::reset()
{
	start_time = clock();
}
//======================Timer类定义结束=====================

void printHighChangeables(const map< string, vector<string> > &adjWords, int minWords = 15)
{
	map<string, vector<string> >::const_iterator itr;
	for (itr = adjWords.begin(); itr != adjWords.end(); ++itr)
	{
		const pair<string, vector<string> > &entry = *itr;
		const vector<string> &words = entry.second;
		if (words.size() >= minWords)
		{
			cout << entry.first << "(" << words.size() << "):";
			for (int i = 0; i < words.size(); i++)
			{
				cout << " " << words[i];
			}
			cout << endl;
		}
	}
}

/*
此函数用于比较两个单词是否是只有一个位置的字母不同
*/
bool oneCharOff(const string &word1, const string &word2)
{
	if (word1.size() != word2.size())
		return false;
	int diffs = 0;
	for (int i = 0; i < word1.size(); i++)
	{
		if (word1[i] != word2[i])
			if (++diffs > 1)
				return false;
	}
	return diffs == 1;
}


/*
此函数数用于计算字典中各个单词通过变动一个字母所能得到的单词的集合
*/
map<string, vector<string> > computeAdjacentWords(const vector<string> &words)
{
	map<string, vector<string> > adjWords;
	map<int, vector<string> > wordsByLength;

    //分组
	for (int i = 0; i < words.size(); i++)
	{
		wordsByLength[words[i].size()].push_back(words[i]);
	}
	
	//遍历单词表中的每一组
	for (auto itr = wordsByLength.begin(); itr != wordsByLength.end(); ++itr)
	{
		const vector<string> &groupsWords = itr->second;
		int groupNum = itr->first;

		//处理每一组中的单词的各个字母
		for (int i = 0; i < groupNum; i++)
		{
			map<string, vector<string> > repToWord;

			//处理该组中的所有单词
			for (int j = 0; j < groupsWords.size(); j++)
			{
				string rep = groupsWords[j];
				rep.erase(i, 1);    //删除第i个位置的字母
				repToWord[rep].push_back(groupsWords[j]);  //只有第i个字母不同的单词会被放在一起
			}

			//处理第i个位置中的单词组成的map
			for (auto itr2 = repToWord.begin(); itr2 != repToWord.end(); ++itr2)
			{
				const vector<string> &clique = itr2->second;
				if (clique.size() >= 2)   //说明有两个以上的单词相似
				{
					for (int i = 0; i < clique.size(); i++)
					{
						for (int j = i+1; j < clique.size(); j++)
						{
							adjWords[clique[i]].push_back(clique[j]);
							adjWords[clique[j]].push_back(clique[i]);
						}
					}
				}
			}
		}

	}
	return adjWords;
}

/*
此函数用于从文件中获取单词并存储到向量中
*/
vector<string> getWords(const string &path)
{
	ifstream input(path);
	vector<string> words;
	if (!input)
	{
		cerr << "无法打开文件" << endl;
		exit(EXIT_FAILURE);
	}
	string word;
	while (input >> word)
	{
		words.push_back(word);
	}
	return words;
}

int main()
{
	string path = "D://newfile.txt";     //文件的路径可以自行修改
	vector<string> words = getWords(path);
	cout << "文件的大小为:" << words.size() << endl;
	Timer time;
	map<string, vector<string> > adjWords=computeAdjacentWords(words);
	printHighChangeables(adjWords,3);
	cout << "时间为:" << time.elapsed_time() << endl;
	return 0;
}


         程序执行的结果为:


        这里面我们可以看到时间有了明显的减少,已经为50.713了,当然,细心的朋友会发现我这个时间的计算是有点小问题的,main函数中的测试时间的部分,应该修改为如下:
int main()
{
	string path = "D://newfile.txt";     //文件的路径可以自行修改
	vector<string> words = getWords(path);
	cout << "文件的大小为:" << words.size() << endl;
	Timer time;
	map<string, vector<string> > adjWords=computeAdjacentWords(words);
	cout << "时间为:" << time.elapsed_time() << endl;
	printHighChangeables(adjWords,3);
	return 0;
}
         是的,仔细看你就会发现,输出时间的代码行上升了一行,这是因为printHighChangeables也是耗时的,修改后此方法耗时为36.024,前面的部分大家有兴趣的也可以修改后自己测试一下:

       
         这次的内容就到这里了。



posted on 2017-08-12 20:59  云端翱翔  阅读(427)  评论(0编辑  收藏  举报

导航