微軟筆試題 大型檔案外部排序(二路歸併和k路歸併的實現和比較)
阿新 • • 發佈:2018-12-31
這兩種排序方法都是先將一個無序的大的外部檔案,分成若干塊,分別讀到記憶體中。
將每一塊都先排好序,放到一個新的外部檔案中。
- 二路歸併的思路是每次將外部排序的檔案兩兩合併,變成一個二倍大小的檔案,然後對二倍大小的檔案繼續兩兩合併。直到最終合併為一個檔案為止。
- k路歸併是將外部排好序的子檔案一次合併。先在各個檔案中取出第一個資料,放到一個優先順序佇列中。然後選出最小的資料輸出到外部結果檔案裡。並從最小資料對應的檔案中讀取下一個資料。這種方法的關鍵在於,要將每次從檔案中讀到的資料和對應的檔案關聯起來。這樣才可以持續的讀取。另一個重要的地方在於,當一個檔案讀取結束時,放置一個最大的資料MAX到優先順序佇列中當做標記。當某一次從優先順序佇列中讀到的資料是MAX時,表明所有檔案都已經讀取完畢。結果檔案輸出完成。
二路歸併的C++程式碼:
////對n(10000000)個整數排序,採用二路歸併的方法。每次總是將兩個檔案合併排序為一個。 string get_file_name(int count_file) { stringstream s; s<<count_file; string count_file_string; s>>count_file_string; string file_name="data"; file_name+=count_file_string; return file_name; } ////用二路歸併的方法將n個整數分為每個大小為per的部分。然後逐級遞增的合併 //void push_random_data_to_file(const string& filename ,unsigned long number) //{ // if (number<100000) // { // vector<int> a; // push_rand(a,number,0,number); // write_data_to_file(a,filename.c_str()); // } // else // { // vector<int> a; // const int per=100000,n=number/per; // push_rand(a,number%per,0,number); // write_data_to_file(a,filename.c_str()); // for (int i=0;i<n;i++) // { // a.clear(); // push_rand(a,100000,0,100000); // write_data_append_file(a,filename.c_str()); // } // } //} //void split_data(const string& datafrom,deque<string>& file_name_array,unsigned long per,int& count_file) //{ // unsigned long position=0; // while (true) // { // vector<int> a; // a.clear(); // //讀檔案中的一段資料到陣列中 // if (read_data_to_array(datafrom,a,position,per)==true) // { // break; // } // position+=per; // //將陣列中的資料在記憶體中排序 // sort(a.begin(),a.end()); // ofstream fout; // string filename=get_file_name(count_file++); // file_name_array.push_back(filename); // fout.open(filename.c_str(),ios::in | ios::binary); // //將排好序的陣列輸出到外部檔案中 // write_data_to_file(a,filename.c_str()); // print_file(filename); // fout.close(); // } //} //void sort_big_file_with_binary_merge(unsigned long n,unsigned long per) //{ // unsigned long traverse=n/per; // vector<int> a; // //製造大量資料放入檔案中 // cout<<"對"<<n<<"個整數進行二路歸併排序,每一路的大小為"<<per<<endl // <<"全部資料被分割放在"<<traverse<<"個檔案中"<<endl; // // SingletonTimer::Instance(); // //將待排序檔案分成小檔案,在記憶體中排序後放到磁碟檔案中 // string datafrom="data.txt"; // deque<string> file_name_array; // int count_file=0; // split_data(datafrom,file_name_array,per,count_file); // // SingletonTimer::Instance()->print("將待排序檔案分成小檔案,在記憶體中排序後放到磁碟檔案中"); // //合併排序,二路歸併的方法。 // while (file_name_array.size()>=2) // { // //獲取兩個有序檔案中的內容,將其合併為一個有序的檔案,直到最後合併為一個有序檔案 // string file1=file_name_array.front(); // file_name_array.pop_front(); // string file2=file_name_array.front(); // file_name_array.pop_front(); // string fileout=get_file_name(count_file++); // file_name_array.push_back(fileout); // merge_file(file1,file2,fileout); // print_file(fileout); // } // SingletonTimer::Instance()->print("獲取兩個有序檔案中的內容,將其合併為一個有序的檔案,直到最後合併為一個有序檔案"); // cout<<"最終的檔案中存放所有排好序的資料,其中前一百個為:"<<endl; // print_file(file_name_array.back(),100); // //}
k路歸併的C++程式碼:
////k路歸併排序大檔案1000*10000 // //void write_random_data_to_file(unsigned long number) //{ // cout<<"writing "<<number<<" to file data ..."<<endl; // unsigned long traverse=number/100000; // cout<<traverse<<"s times have to write."<<endl; // ////製造大量資料放入檔案中 // vector<int> a; // if (number<100000) // { // push_rand(a,number,0,number); // write_data_to_file(a,"data"); // } // else // { // push_rand(a,100000,0,1000000); // write_data_to_file(a,"data"); // cout<<"the "<<0<<" times finished."<<endl; // for (unsigned long i=1;i<traverse;i++) // { // a.clear(); // push_rand(a,100000,0,100000); // write_data_append_file(a,"data"); // cout<<"the "<<i<<" times finished."<<endl // <<(traverse-1-i)<<" times left."<<endl; // } // } // cout<<number<<" integers wrote to file data finished."<<endl; // ///////////////////TEST///////////////// // //print_file("data",100); // //sort(a.begin(),a.end()); // //print(a.begin(),a.end()); //} //list<string> divide_big_file_into_small_sorted_file(long number) //{ // vector<int> a; // a.clear(); // long position=0; // int count_file=0; // list<string> file_name_array; // //get part files and file names // while (true) // { // a.clear(); // if (read_data_to_array("data.txt",a,position,number)==true) // { // break; // } // position+=number; // sort(a.begin(),a.end()); // string filename=get_file_name(count_file++); // file_name_array.push_back(filename); // write_data_to_file(a,filename.c_str()); // cout<<"sorted file"<<(count_file-1)<<" builded."<<endl; // } // // return file_name_array; //} //void k_way_merge_sort(const list<string>& file_name_array) //{ // // //get ifstreams and put them to list<ifstream> readfiles // vector<ifstream> readfiles; // for (list<string>::const_iterator i=file_name_array.begin(); // i!=file_name_array.end();i++) // { // readfiles.push_back(ifstream()); // readfiles.back().open(i->c_str(),ios::binary | ios::in ); // } // //init priority queue by read one data from each file // //初始化優先佇列:從每個檔案中讀取第一個資料 // priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int> > > prioritydata; // for (vector<ifstream>::size_type i=0; // i<readfiles.size();i++) // { // int temp; // readfiles[i].read(reinterpret_cast<char*>(&temp),sizeof(int)); // prioritydata.push(make_pair(temp,i)); // } // //merge sort file // ofstream fout; // fout.open("result",ios::binary); // while (true) // { // int onedata=prioritydata.top().first; // if (onedata==numeric_limits<int>().max()) // { // break; // } // else // { // // fout.write(reinterpret_cast<const char*>(&onedata),sizeof(int)); // //從第i個檔案中讀取一個整數 // int i=prioritydata.top().second; // prioritydata.pop(); // int temp; // readfiles[i].read(reinterpret_cast<char*>(&temp),sizeof(int)); // if (readfiles[i].eof()) // { // //當此檔案讀到最後結束時,放入標記到優先順序佇列中 // prioritydata.push(make_pair(numeric_limits<int>().max(),i)); // } // else // { // //否則將讀取到的資料直接放到優先順序佇列中 // prioritydata.push(make_pair(temp,i)); // } // } // } // //關閉所有開啟的檔案 // fout.close(); // for (vector<ifstream>::size_type i=0; // i<readfiles.size();i++) // { // readfiles[i].close(); // } //} //void sort_big_file_with_k_way_merge(unsigned long n,unsigned long partitionfilesize) //{ // // //write_random_data_to_file(n); // timer t; // k_way_merge_sort(divide_big_file_into_small_sorted_file(partitionfilesize)); // //將待排序檔案分成小檔案,在記憶體中排序後放到磁碟檔案中 // //假設記憶體只有1MB,26W個整數 // cout<<n/partitionfilesize<<"路歸併排序大檔案 "<<n<<" ,記憶體一次排序 "<<partitionfilesize<<endl; // print(t.elapsed()); // print("秒"); // print_file("result",1000); //}
輸出結果及其比較:
K路歸併 |
4路 |
209秒 |
8路 |
190秒 |
|
16路 |
223秒 |
|
二路歸併 |
4個子檔案 |
257秒 |
8個子檔案 |
281秒 |
從上面多次實驗結果來看,在外部排序時,二路歸併的方法不是最優的。因為它每次總是合併兩個檔案,這樣做造成了全部資料被遍歷的次數比較多。在外部排序中,由於資料量比較大,所以遍歷的次數直接影響了排序的時間。而k路歸併強調一次將k個排好序的子檔案合併為一個最終的結果檔案,所以,遍歷了檔案兩次,讀一次,寫一次。其他的時間主要花在優先順序佇列的出隊,入隊的調整上。所以k的值不能過大,太大,導致調整堆佔用了過多的時間,太小導致內部排序佔用過大記憶體。上面的結果說明,8路歸併排序速度最快。