面试题:超大文件去除重复行

先看问题的简化版本:
题目描述:有一个文件,其大小不大,可以全部装入内存,要求去除文件中重复的行。
solution:
遍历每一行,先查找是不是已经存在unordered_set中,如果存在,跳过,如果不存在,接到输出文件后面。
代码:

//去除单个小文件中的重复行
void deleteDuplicateRow(string &originFile, string &resFile) {
	string curRow;
	unordered_set<string>se;
	ifstream fsOrigin(originFile);
	ofstream fsRes(resFile,ofstream::app);
	while (getline(fsOrigin,curRow))
	{
		if (se.find(curRow)==se.end())
		{
			se.insert(curRow);
			fsRes << curRow<<endl;
		}
	}
}

那么如果文件太大了,比如说100G,远远超过了内存大小16G,上面的方法行不通了,因为上面方法中unordered_set的大小是随着不重复行的数量增大而增大,如果不重复的行所占空间大于内存大小,就会导致内存不足,程序崩溃。
思路历程:将原文件分成m份大小小于内存大小的小文件,比如说每次读取1G数据到一个小文件中,得到100个小文件,再对各个小文件运行上面的函数去重,得到去完重后的100个小文件,这时100个小文件内部是没有重复的行了,但是小文件之间还可能有重复的行。
于是同时打开这m个小文件,按上面的方法,遍历每一行,如果unordered_set中已经存在就跳过,如果不存在就接到输出文件后面。但是这样做其实跟上面的方法并没有差别,因为unordered_set还是可能超出内存。
有没有什么方法能不用把每一个不重复的行都存在内存中呢?
联想到归并排序,如果两个子数组是有序的,那么归并的过程只需要从子数组的首元素开始比较,较小的装入临时数组中就好了。

void merge(vector<int>&nums,int start,int mid,int end){
	vector<int>tem(end-start+1);
	int temIndex=0,i=start,j=mid+1;
	while(i<=mid&&j<=end){
		if(nums[i]<=nums[j])tem[temIndex++]=nums[i++];
		else tem[temIndex++]=nums[j++];
	}
	while(i<=mid)tem[temIndex++]=nums[i++];
	while(j<=end)tem[temIndex++]=nums[j++];
	copy(tem.begin(),tem.end(),nums.begin()+start);
}

类似的,我们把这100个小文件分别按行排序,得到100个按行有序的小文件,
然后同时打开这100个小文件,从他们的第一行开始,维护100个指针,初始时每个指针指向文件的第1行,每次比较得到这100行中最小的那行放入输出文件,比如:第i个小文件的第1行最小,那么指针pi++;
如果第i次比较得到的最小行跟第i-1次比较得到的最小行相等,即重复,那么跳过,如此往复直到所有文件中指针都指到了EOF。
至此我们得到了第一种可行的算法
(1)大文件按行读取,每1G数据,放到一个小文件中
(2)对每个小文件,按行排序
(3)每个文件读取第一行,比较得到其中最小的那一行minRow,将minRow接到输出文件后面
(4)从minRow所在的文件中再读一行,取代minRow的位置,继续比较得到最小的一行,如果第i次比较得到的minRow等于第i-1次的minRow,说明重复了,那么这一行不放入输出文件。
(5)重复步骤4,直到所有小文件都读取到EOF。
时间复杂度分析
假设按行排序使用快排,平均时间复杂度为O(nlogn),其中n为小文件的行数。第2步进行m次排序,得到O(mnlogn)。
第4步总共比较N次,其中N为原文件的总行数,第一次使用快排,然后后面每次相当于将一个新数字插入m-1个有序序列中,时间复杂度为O(m),第4,5步总共就是O(Nm)。
总的时间复杂度是max(O(mnlogn),O(Nm))=O(Nm)。
进一步,如果我们把第4,5步中的比较省去,那么时间复杂度会优化很多。
怎么才能省去各个小文件之间的比较呢
我们之所以要对每个小文件之间进行比较,是因为他们之间可能有重复的行。如果可以找到一种方法,按这种方法分得的小文件之间没有重复行,那么就可以省去比较了。
自然而然地想到了hash表,遍历原文件每一行的时候,将每行进行hash,得到hashcode,然后将该行分到第hashcode%m个小文件中,这样每个小文件之间的行的hashcode%m都不相等,连hashcode%m都不相等,hashcode更不可能相等,也就是说行本身也不相等。
这样一来,我们使分得的m个小文件之间都不存在重复行,只需要对每个小文件内部分别去重,再把去重后的小文件每一行都接到输出文件里就好了。
时间复杂度分析:分成m个小文件:O(N),对每个小文件去重O(mn),将每个去重后的小文件的行放到输出文件中,O(N)。总的时间复杂度:O(N)。
至此得到第二种较优的解法:
代码如下:

//去除单个小文件中的重复行
void deleteDuplicateRow(string &originFile, string &resFile) {
	string curRow;
	unordered_set<string>se;
	ifstream fsOrigin(originFile);
	ofstream fsRes(resFile,ofstream::app);
	while (getline(fsOrigin,curRow))
	{
		if (se.find(curRow)==se.end())
		{
			se.insert(curRow);
			fsRes << curRow<<endl;
		}
	}
}
//将大文件分成多个小文件
vector<string> splitBigfile(string &originFile,int splitSize) {
	 vector<string>smallFiles(splitSize);
	string smallFileName;
	for (int i=0;i<splitSize;++i)
	{
		smallFileName = to_string(i) + "thSmallFile.txt";
		smallFiles[i] = smallFileName;
	}
	string curRow;
	ifstream fsOrigin(originFile);
	
	while (getline(fsOrigin,curRow))
	{
		auto hashcode=hash<string>()(curRow);
		fstream fsSmall(smallFiles[hashcode%splitSize], fstream::app);
		fsSmall<< curRow<<endl;
	}
	return smallFiles;
}
void handleSmallFiles(vector<string>& smallFiles, string &resFile) {
	vector<string> curRess(smallFiles.size());
	for (int i=0;i<smallFiles.size();++i)
	{
		string curResName = to_string(i) + "thCurResName.txt";
		curRess[i] = curResName;
		deleteDuplicateRow(smallFiles[i], curRess[i]);
	}
	ofstream fsRes(resFile,ofstream::app);
	
	for (auto &file:curRess)
	{
		fstream fs(file,fstream::app);
		string curRow;
		while (getline(fs,curRow))
		{
				fsRes << curRow << endl;
		}
	}
}

int main() {
	
	string filename;
	cin >> filename;

	string resFilename;
	cin >> resFilename;
	
	int splitSize = 100;
	vector<string>smallFiles=splitBigfile(filename,splitSize);
	handleSmallFiles(smallFiles,resFilename);
	string s;
	ifstream fs(resFilename);
	while (getline(fs,s))
	{
		cout << s;
	}
	fs.close();
}

更多的考虑:如果不幸某一个小文件还是很大,不能读入到内存,那么可以考虑对这个小文件重复上述算法,他就会变的更小。但如果原来的文件非常bias,极限情况比如就是几个字符串一直在重复,那么就会导致小文件还是很大,并且重复这个算法永远也不可能使他变小了。那么可以先把文件分成n份,每份去重,合并后得到一个新的文件。然后对新的文件使用原来的算法。这样子就可以保证一个字符串最多出现n次了。

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值