1. 程式人生 > >微軟筆試題 大型檔案外部排序(二路歸併和k路歸併的實現和比較)

微軟筆試題 大型檔案外部排序(二路歸併和k路歸併的實現和比較)

這兩種排序方法都是先將一個無序的大的外部檔案,分成若干塊,分別讀到記憶體中。

將每一塊都先排好序,放到一個新的外部檔案中。

  1. 二路歸併的思路是每次將外部排序的檔案兩兩合併,變成一個二倍大小的檔案,然後對二倍大小的檔案繼續兩兩合併。直到最終合併為一個檔案為止。
  2. 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路歸併排序速度最快。