先看问题的简化版本:
题目描述:有一个文件,其大小不大,可以全部装入内存,要求去除文件中重复的行。
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次了。