前奏
經過幾天的痛苦沉思,最終決定,把原程序員面試題狂想曲系列正式更名爲程序員編程藝術系列,同時,狂想曲創作組更名爲編程藝術室。之所以要改名,我們考慮到三點:1、爲面試服務不能成爲我們最終或最主要的目的,2、我更願把解答一道道面試題,ACM題等各類程序設計題目的過程,當做一種藝術來看待,3、藝術的提煉本身是一個非常非常艱難的過程,但我們樂意接受這個挑戰。
ok,如果任何人對本編程藝術系列有任何意見,或發現了本編程藝術系列任何問題,漏洞,bug,歡迎隨時提出,我們將虛心接受並感激不盡,以爲他人創造更好的價值,更好的服務。
第一節、如何給磁盤文件排序
問題描述:
輸入:給定一個文件,裏面最多含有n個不重複的正整數(也就是說可能含有少於n個不重複正整數),且其中每個數都小於等於n,n=10^7。
輸出:得到按從小到大升序排列的包含所有輸入的整數的列表。
條件:最多有大約1MB的內存空間可用,但磁盤空間足夠。且要求運行時間在5分鐘以下,10秒爲最佳結果。
分析:下面咱們來一步一步的解決這個問題,
1、歸併排序。你可能會想到把磁盤文件進行歸併排序,但題目要求你只有1MB的內存空間可用,所以,歸併排序這個方法不行。
2、位圖方案。熟悉位圖的朋友可能會想到用位圖來表示這個文件集合。例如正如編程珠璣一書上所述,用一個20位長的字符串來表示一個所有元素都小於20的簡單的非負整數集合,邊框用如下字符串來表示集合{1,2,3,5,8,13}:
0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0
上述集合中各數對應的位置則置1,沒有對應的數的位置則置0。
參考編程珠璣一書上的位圖方案,針對我們的10^7個數據量的磁盤文件排序問題,我們可以這麼考慮,由於每個7位十進制整數表示一個小於1000萬的整數。我們可以使用一個具有1000萬個位的字符串來表示這個文件,其中,當且僅當整數i在文件中存在時,第i位爲1。採取這個位圖的方案是因爲我們面對的這個問題的特殊性:1、輸入數據限制在相對較小的範圍內,2、數據沒有重複,3、其中的每條記錄都是單一的整數,沒有任何其它與之關聯的數據。
所以,此問題用位圖的方案分爲以下三步進行解決:
- 第一步,將所有的位都置爲0,從而將集合初始化爲空。
- 第二步,通過讀入文件中的每個整數來建立集合,將每個對應的位都置爲1。
- 第三步,檢驗每一位,如果該位爲1,就輸出對應的整數。
經過以上三步後,產生有序的輸出文件。令n爲位圖向量中的位數(本例中爲1000 0000),程序可以用僞代碼表示如下:
- //磁盤文件排序位圖方案的僞代碼
- //copyright@ Jon Bentley
- //July、updated,2011.05.29。
- //第一步,將所有的位都初始化爲0
- for i ={0,....n}
- bit[i]=0;
- //第二步,通過讀入文件中的每個整數來建立集合,將每個對應的位都置爲1。
- for each i in the input file
- bit[i]=1;
- //第三步,檢驗每一位,如果該位爲1,就輸出對應的整數。
- for i={0...n}
- if bit[i]==1
- write i on the output file
上面只是爲了簡單介紹下位圖算法的僞代碼之抽象級描述。顯然,咱們面對的問題,可不是這麼簡單。下面,我們試着針對這個要分兩趟給磁盤文件排序的具體問題編寫完整代碼,如下。
- //copyright@ yansha
- //July、2010.05.30。
- //位圖方案解決10^7個數據量的文件的排序問題
- //如果有重複的數據,那麼只能顯示其中一個 其他的將被忽略
- #include <iostream>
- #include <bitset>
- #include <assert.h>
- #include <time.h>
- using namespace std;
- const int max_each_scan = 5000000;
- int main()
- {
- clock_t begin = clock();
- bitset<max_each_scan> bit_map;
- bit_map.reset();
- // open the file with the unsorted data
- FILE *fp_unsort_file = fopen("data.txt", "r");
- assert(fp_unsort_file);
- int num;
- // the first time scan to sort the data between 0 - 4999999
- while (fscanf(fp_unsort_file, "%d ", &num) != EOF)
- {
- if (num < max_each_scan)
- bit_map.set(num, 1);
- }
- FILE *fp_sort_file = fopen("sort.txt", "w");
- assert(fp_sort_file);
- int i;
- // write the sorted data into file
- for (i = 0; i < max_each_scan; i++)
- {
- if (bit_map[i] == 1)
- fprintf(fp_sort_file, "%d ", i);
- }
- // the second time scan to sort the data between 5000000 - 9999999
- int result = fseek(fp_unsort_file, 0, SEEK_SET);
- if (result)
- cout << "fseek failed!" << endl;
- else
- {
- bit_map.reset();
- while (fscanf(fp_unsort_file, "%d ", &num) != EOF)
- {
- if (num >= max_each_scan && num < 10000000)
- {
- num -= max_each_scan;
- bit_map.set(num, 1);
- }
- }
- for (i = 0; i < max_each_scan; i++)
- {
- if (bit_map[i] == 1)
- fprintf(fp_sort_file, "%d ", i + max_each_scan);
- }
- }
- clock_t end = clock();
- cout<<"用位圖的方法,耗時:"<<endl;
- cout << (end - begin) / CLK_TCK << "s" << endl;
- fclose(fp_sort_file);
- fclose(fp_unsort_file);
- return 0;
- }
而後測試了一下上述程序的運行時間,採取位圖方案耗時14s,即14000ms:
本章中,生成大數據量(1000w)的程序如下,下文第二節的多路歸併算法的c++實現和第三節的磁盤文件排序的編程實現中,生成的1000w數據量也是用本程序產生的,且本章內生成的1000w數據量的數據文件統一命名爲“data.txt”。
- //purpose: 生成隨機的不重複的測試數據
- //copyright@ 2011.04.19 yansha
- //1000w數據量,要保證生成不重複的數據量,一般的程序沒有做到。
- //但,本程序做到了。
- //July、2010.05.30。
- #include <iostream>
- #include <time.h>
- #include <assert.h>
- using namespace std;
- const int size = 10000000;
- int num[size];
- int main()
- {
- int n;
- FILE *fp = fopen("data.txt", "w");
- assert(fp);
- for (n = 1; n <= size; n++)
- //之前此處寫成了n=0;n<size。導致下面有一段小程序的測試數據出現了0,特此訂正。
- num[n] = n;
- srand((unsigned)time(NULL));
- int i, j;
- for (n = 0; n < size; n++)
- {
- i = (rand() * RAND_MAX + rand()) % 10000000;
- j = (rand() * RAND_MAX + rand()) % 10000000;
- swap(num[i], num[j]);
- }
- for (n = 0; n < size; n++)
- fprintf(fp, "%d ", num[n]);
- fclose(fp);
- return 0;
- }
不過很快,我們就將意識到,用此位圖方法,嚴格說來還是不太行,空間消耗10^7/8還是大於1M(1M=1024*1024空間,小於10^7/8)。
既然如果用位圖方案的話,我們需要約1.25MB(若每條記錄是8位的正整數的話,則10000000/(1024*1024*8) ~= 1.2M)的空間,而現在只有1MB的可用存儲空間,那麼究竟該作何處理呢?
updated && correct:
@yansha: 上述的位圖方案,共需要掃描輸入數據兩次,具體執行步驟如下:
- 第一次,只處理1—4999999之間的數據,這些數都是小於5000000的,對這些數進行位圖排序,只需要約5000000/8=625000Byte,也就是0.625M,排序後輸出。
- 第二次,掃描輸入文件時,只處理4999999-10000000的數據項,也只需要0.625M(可以使用第一次處理申請的內存)。
因此,總共也只需要0.625M
位圖的的方法有必要強調一下,就是位圖的適用範圍爲針對不重複的數據進行排序,若數據有重複,位圖方案就不適用了。
3、多路歸併。誠然,在面對本題時,還可以通過計算分析出可以用如2的位圖法解決,但實際上,很多的時候,我們都面臨着這樣一個問題,文件太大,無法一次性放入內存中計算處理,那這個時候咋辦呢?分而治之,大而化小,也就是把整個大文件分爲若干大小的幾塊,然後分別對每一塊進行排序,最後完成整個過程的排序。k趟算法可以在kn的時間開銷內和n/k的空間開銷內完成對最多n個小於n的無重複正整數的排序。
比如可分爲2塊(k=2,1趟反正佔用的內存只有1.25/2M),1~4999999,和5000000~9999999。先遍歷一趟,首先排序處理1~4999999之間的整數(用5000000/8=625000個字的存儲空間來排序0~4999999之間的整數),然後再第二趟,對5000001~1000000之間的整數進行排序處理。在稍後的第二節、第三節、第四節,我們將詳細闡述並實現這種多路歸併排序磁盤文件的方案。
4、讀者思考。經過上述思路3的方案之後,現在有兩個局部有序的數組了,那麼要得到一個完整的排序的數組,接下來改怎麼做呢?或者說,如果是K路歸併,得到k個排序的子數組,把他們合併成一個完整的排序數組,如何優化?或者,我再問你一個問題,K路歸併用敗者樹 和 勝者樹 效率有什麼差別?這些問題,請讀者思考。
第二節、多路歸併算法的c++實現
本節咱們暫拋開咱們的問題,闡述下有關多路歸併算法的c++實現問題。在稍後的第三節,咱們再來具體針對咱們的磁盤文件排序問題闡述與實現。
在瞭解多路歸併算法之前,你還得了解歸併排序的過程,因爲下面的多路歸併算法就是基於這個流程的。其實歸併排序就是2路歸併,而多路歸併算法就是把2換成了k,即多(k)路歸併。下面,舉個例子來說明下此歸併排序算法,如下圖所示,我們對數組8 3 2 6 7 1 5 4進行歸併排序:
歸併排序算法簡要介紹:
一、思路描述:
設兩個有序的子文件(相當於輸入堆)放在同一向量中相鄰的位置上:R[low..m],R[m+1..high],先將它們合併到一個局部的暫存向量R1(相當於輸出堆)中,待合併完成後將R1複製回R[low..high]中。
二路歸併排序的過程是:
(1)把無序表中的每一個元素都看作是一個有序表,則有n個有序子表;
(2)把n個有序子表按相鄰位置分成若干對(若n爲奇數,則最後一個子表單獨作爲一組),每對中的兩個子表進行歸併,歸併後子表數減少一半;
(3)反覆進行這一過程,直到歸併爲一個有序表爲止。二路歸併排序過程的核心操作是將一維數組中相鄰的兩個有序表歸併爲一個有序表。
二、分類:
歸併排序可分爲:多路歸併排序、兩路歸併排序 。
若歸併的有序表有兩個,叫做二路歸併。一般地,若歸併的有序表有k個,則稱爲k路歸併。二路歸併最爲簡單和常用,既適用於內部排序,也適用於外部排序。本文着重討論外部排序下的多(K)路歸併算法。三、算法分析:
1、穩定性:歸併排序是一種穩定的排序。
2、存儲結構要求:可用順序存儲結構。也易於在鏈表上實現。
3、時間複雜度: 對長度爲n的文件,需進行lgn趟二路歸併,每趟歸併的時間爲O(n),故其時間複雜度無論是在最好情況下還是在最壞情況下均是O(nlgn)。。
4、空間複雜度:需要一個輔助向量來暫存兩有序子文件歸併的結果,故其輔助空間複雜度爲O(n),顯然它不是就地排序。
注意:若用單鏈表做存儲結構,很容易給出就地的歸併排序。
總結:與快速排序相比,歸併排序的最大特點是,它是一種穩定的排序方法。歸併排序一般多用於外排序。但它在內排方面也佔有重要地位,因爲它是基於比較的時間複雜度爲O(N*Log(N))的排序算法中唯一穩定的排序,所以在需要穩定內排序時通常會選擇歸併排序。歸併排序不要求對序列可以很快地進行隨機訪問,所以在鏈表排序的實現中很受歡迎。
好的,介紹完了歸併排序後,回到咱們的問題。由第一節,我們已經知道,當數據量大到不適合在內存中排序時,可以利用多路歸併算法對磁盤文件進行排序。
我們以一個包含很多個整數的大文件爲例,來說明多路歸併的外排序算法基本思想。假設文件中整數個數爲N(N是億級的),整數之間用空格分開。首先分多次從該文件中讀取M(十萬級)個整數,每次將M個整數在內存中使用快速排序之後存入臨時文件,然後使用多路歸併將各個臨時文件中的數據再次整體排好序後存入輸出文件。顯然,該排序算法需要對每個整數做2次磁盤讀和2次磁盤寫。以下是本程序的流程圖:
本程序是基於以上思想對包含大量整數文件的從小到大排序的一個簡單實現,這裏沒有使用內存緩衝區,在歸併時簡單使用一個數組來存儲每個臨時文件的第一個元素。下面是多路歸併排序算法的c++實現代碼(在第四節,將給出多路歸併算法的c實現):
- //copyright@ 純淨的天空 && yansha
- //5、July,updated,2010.05.28。
- #include <iostream>
- #include <ctime>
- #include <fstream>
- //#include "ExternSort.h"using namespace std;
- //使用多路歸併進行外排序的類
- //ExternSort.h
- /** 大數據量的排序* 多路歸併排序* 以千萬級整數從小到大排序爲例* 一個比較簡單的例子,沒有建立內存緩衝區*/
- #ifndef EXTERN_SORT_H
- #define EXTERN_SORT_H
- #include <cassert>class ExternSort
- {
- public:
- void sort()
- {
- time_t start = time(NULL);
- //將文件內容分塊在內存中排序,並分別寫入臨時文件
- int file_count = memory_sort();
- //歸併臨時文件內容到輸出文件
- merge_sort(file_count);
- time_t end = time(NULL);printf("total time:%f/n", (end - start) * 1000.0/ CLOCKS_PER_SEC);
- }
- //input_file:輸入文件名
- //out_file:輸出文件名
- //count: 每次在內存中排序的整數個數
- ExternSort(const char *input_file, const char * out_file, int count)
- {
- m_count = count;
- m_in_file = new char[strlen(input_file) + 1];
- strcpy(m_in_file, input_file);
- m_out_file = new char[strlen(out_file) + 1];
- strcpy(m_out_file, out_file);
- }
- virtual ~ExternSort()
- {
- delete [] m_in_file;
- delete [] m_out_file;
- }
- private:
- int m_count;
- //數組長度char *m_in_file;
- //輸入文件的路徑
- char *m_out_file;
- //輸出文件的路徑
- protected:
- int read_data(FILE* f, int a[], int n)
- {
- int i = 0;
- while(i < n && (fscanf(f, "%d", &a[i]) != EOF))
- i++;
- printf("read:%d integer/n", i);
- return i;
- }
- void write_data(FILE* f, int a[], int n)
- {
- for(int i = 0; i < n; ++i)
- fprintf(f, "%d ", a[i]);
- }
- char* temp_filename(int index)
- {
- char *tempfile = new char[100];
- sprintf(tempfile, "temp%d.txt", index);
- return tempfile;
- }
- static int cmp_int(const void *a, const void *b)
- {
- return *(int*)a - *(int*)b;
- }
- int memory_sort()
- {
- FILE* fin = fopen(m_in_file, "rt");
- int n = 0, file_count = 0;int *array = new int[m_count];
- //每讀入m_count個整數就在內存中做一次排序,並寫入臨時文件
- while(( n = read_data(fin, array, m_count)) > 0)
- {
- qsort(array, n, sizeof(int), cmp_int); //這裏,調用了庫函數阿,在第四節的c實現裏,不再調qsort。
- char *fileName = temp_filename(file_count++);
- FILE *tempFile = fopen(fileName, "w");
- free(fileName);
- write_data(tempFile, array, n);
- fclose(tempFile);
- }
- delete [] array;
- fclose(fin);
- return file_count;
- }
- void merge_sort(int file_count)
- {
- if(file_count <= 0)
- return;
- //歸併臨時文件FILE *fout = fopen(m_out_file, "wt");
- FILE* *farray = new FILE*[file_count];
- int i;
- for(i = 0; i < file_count; ++i)
- {
- char* fileName = temp_filename(i);
- farray[i] = fopen(fileName, "rt");
- free(fileName);
- }
- int *data = new int[file_count];
- //存儲每個文件當前的一個數字
- bool *hasNext = new bool[file_count];
- //標記文件是否讀完
- memset(data, 0, sizeof(int) * file_count);
- memset(hasNext, 1, sizeof(bool) * file_count);
- for(i = 0; i < file_count; ++i)
- {
- if(fscanf(farray[i], "%d", &data[i]) == EOF)
- //讀每個文件的第一個數到data數組
- hasNext[i] = false;
- }
- while(true)
- {
- //求data中可用的最小的數字,並記錄對應文件的索引
- int min = data[0];
- int j = 0;
- while (j < file_count && !hasNext[j])
- j++;
- if (j >= file_count)
- //沒有可取的數字,終止歸併
- break;
- for(i = j + 1; i < file_count; ++i)
- {
- if(hasNext[i] && min > data[i])
- {
- min = data[i];
- j = i;
- }
- }
- if(fscanf(farray[j], "%d", &data[j]) == EOF)
- //讀取文件的下一個元素
- hasNext[j] = false;
- fprintf(fout, "%d ", min);
- }
- delete [] hasNext;
- delete [] data;
- for(i = 0; i < file_count; ++i)
- {
- fclose(farray[i]);
- }
- delete [] farray;
- fclose(fout);
- }
- };
- #endif
- //測試主函數文件
- /** 大文件排序* 數據不能一次性全部裝入內存* 排序文件裏有多個整數,整數之間用空格隔開*/
- const unsigned int count = 10000000;
- // 文件裏數據的行數const unsigned int number_to_sort = 1000000;
- //在內存中一次排序的數量
- const char *unsort_file = "unsort_data.txt";
- //原始未排序的文件名
- const char *sort_file = "sort_data.txt";
- //已排序的文件名
- void init_data(unsigned int num);
- //隨機生成數據文件
- int main(int argc, char* *argv)
- {
- srand(time(NULL));
- init_data(count);
- ExternSort extSort(unsort_file, sort_file, number_to_sort);
- extSort.sort();
- system("pause");
- return 0;
- }
- void init_data(unsigned int num)
- {
- FILE* f = fopen(unsort_file, "wt");
- for(int i = 0; i < num; ++i)
- fprintf(f, "%d ", rand());
- fclose(f);
- }
程序測試:讀者可以繼續用小文件小數據量進一步測試。
第三節、磁盤文件排序的編程實現
ok,接下來,我們來編程實現上述磁盤文件排序的問題,本程序由兩部分構成:
1、內存排序
由於要求的可用內存爲1MB,那麼每次可以在內存中對250K的數據進行排序,然後將有序的數寫入硬盤。
那麼10M的數據需要循環40次,最終產生40個有序的文件。
2、歸併排序
- 將每個文件最開始的數讀入(由於有序,所以爲該文件最小數),存放在一個大小爲40的first_data數組中;
- 選擇first_data數組中最小的數min_data,及其對應的文件索引index;
- 將first_data數組中最小的數寫入文件result,然後更新數組first_data(根據index讀取該文件下一個數代替min_data);
- 判斷是否所有數據都讀取完畢,否則返回2。
所以,本程序按順序分兩步,第一步、Memory Sort,第二步、Merge Sort。程序的流程圖,如下圖所示(感謝F的繪製)。
然後,編寫的完整代碼如下:
- //copyright@ yansha
- //July、updated,2011.05.28。
- #include <iostream>
- #include <string>
- #include <algorithm>
- #include <time.h>
- using namespace std;
- int sort_num = 10000000;
- int memory_size = 250000;
- //每次只對250k個小數據量進行排序
- int read_data(FILE *fp, int *space)
- {
- int index = 0;
- while (index < memory_size && fscanf(fp, "%d ", &space[index]) != EOF)
- index++;
- return index;
- }
- void write_data(FILE *fp, int *space, int num)
- {
- int index = 0;
- while (index < num)
- {
- fprintf(fp, "%d ", space[index]);
- index++;
- }
- }
- // check the file pointer whether valid or not.
- void check_fp(FILE *fp)
- {
- if (fp == NULL)
- {
- cout << "The file pointer is invalid!" << endl;
- exit(1);
- }
- }
- int compare(const void *first_num, const void *second_num)
- {
- return *(int *)first_num - *(int *)second_num;
- }
- string new_file_name(int n)
- {
- char file_name[20];
- sprintf(file_name, "data%d.txt", n);
- return file_name;
- }
- int memory_sort()
- {
- // open the target file.
- FILE *fp_in_file = fopen("data.txt", "r");
- check_fp(fp_in_file);
- int counter = 0;
- while (true)
- {
- // allocate space to store data read from file.
- int *space = new int[memory_size];
- int num = read_data(fp_in_file, space);
- // the memory sort have finished if not numbers any more.
- if (num == 0)
- break;
- // quick sort.
- qsort(space, num, sizeof(int), compare);
- // create a new auxiliary file name.
- string file_name = new_file_name(++counter);
- FILE *fp_aux_file = fopen(file_name.c_str(), "w");
- check_fp(fp_aux_file);
- // write the orderly numbers into auxiliary file.
- write_data(fp_aux_file, space, num);
- fclose(fp_aux_file);
- delete []space;
- }
- fclose(fp_in_file);
- // return the number of auxiliary files.
- return counter;
- }
- void merge_sort(int file_num)
- {
- if (file_num <= 0)
- return;
- // create a new file to store result.
- FILE *fp_out_file = fopen("result.txt", "w");
- check_fp(fp_out_file);
- // allocate a array to store the file pointer.
- FILE **fp_array = new FILE *[file_num];
- int i;
- for (i = 0; i < file_num; i++)
- {
- string file_name = new_file_name(i + 1);
- fp_array[i] = fopen(file_name.c_str(), "r");
- check_fp(fp_array[i]);
- }
- int *first_data = new int[file_num];
- //new出個大小爲0.1億/250k數組,由指針first_data指示數組首地址
- bool *finish = new bool[file_num];
- memset(finish, false, sizeof(bool) * file_num);
- // read the first number of every auxiliary file.
- for (i = 0; i < file_num; i++)
- fscanf(fp_array[i], "%d ", &first_data[i]);
- while (true)
- {
- int index = 0;
- while (index < file_num && finish[index])
- index++;
- // the finish condition of the merge sort.
- if (index >= file_num)
- break;
- //主要的修改在上面兩行代碼,就是merge sort結束條件。
- //要保證所有文件都讀完,必須使得finish[0]...finish[40]都爲真
- //July、yansha,555,2011.05.29。
- int min_data = first_data[index];
- // choose the relative minimum in the array of first_data.
- for (i = index + 1; i < file_num; i++)
- {
- if (min_data > first_data[i] && !finish[i])
- //一旦發現比min_data更小的數據first_data[i]
- {
- min_data = first_data[i];
- //則置min_data<-first_data[i]index = i;
- //把下標i 賦給index。
- }
- }
- // write the orderly result to file.
- fprintf(fp_out_file, "%d ", min_data);
- if (fscanf(fp_array[index], "%d ", &first_data[index]) == EOF)
- finish[index] = true;
- }
- fclose(fp_out_file);
- delete []finish;
- delete []first_data;
- for (i = 0; i < file_num; i++)
- fclose(fp_array[i]);
- delete [] fp_array;
- }
- int main()
- {
- clock_t start_memory_sort = clock();
- int aux_file_num = memory_sort();
- clock_t end_memory_sort = clock();
- cout << "The time needs in memory sort: " << end_memory_sort - start_memory_sort << endl;
- clock_t start_merge_sort = clock();
- merge_sort(aux_file_num);
- clock_t end_merge_sort = clock();
- cout << "The time needs in merge sort: " << end_merge_sort - start_merge_sort << endl;
- system("pause");
- return 0;
- }
其中,生成數據文件data.txt的代碼在第一節已經給出。
程序測試:
1、咱們對1000W數據進行測試,打開半天沒看到數據,
2、編譯運行上述程序後,data文件先被分成40個小文件data[1....40],然後程序再對這40個小文件進行歸併排序,排序結果最終生成在result文件中,自此result文件中便是由data文件的數據經排序後得到的數據。
3、且,我們能看到,data[i],i=1...40的每個文件都是有序的,如下圖:
4、最終的運行結果,如下,單位統一爲ms:
由上觀之,我們發現,第一節的位圖方案的程序效率是最快的,約爲14s,而採用上述的多路歸併算法的程序運行時間約爲25s。時間主要浪費在讀寫磁盤IO上,且程序中用的庫函數qsort也耗費了不少時間。所以,總的來說,採取位圖方案是最佳方案。
小數據量測試:
我們下面針對小數據量的文件再測試一次,針對20個小數據,每趟對4個數據進行排序,即5路歸併,程序的排序結果如下圖所示。
運行時間:
0ms,可以忽略不計了,畢竟是對20個數的小數據量進行排序:
沙海拾貝:
我們不在乎是否能把一個軟件產品或一本書最終完成,我們更在乎的是,在完成這個產品或創作這本書的過程中,讀者學到了什麼,能學到什麼?所以,不要一味的馬上就想得到一道題目的正確答案,請跟着我們一起逐步走向山巔。
第四節、多路歸併算法的c實現
本多路歸併算法的c實現原理與上述c++實現一致,不同的地方體現在一些細節處理上,且對臨時文件的排序,不再用系統提供的快排,即上面的qsort庫函數,是採用的三數中值的快速排序(個數小於3用插入排序)的。而我們知道,純正的歸併排序其實就是比較排序,在歸併過程中總是不斷的比較,爲了從兩個數中挑小的歸併到最終的序列中。ok,此程序的詳情請看:
- //copyright@ 555
- //July、2011.05.29。
- #include <assert.h>
- #include <time.h>
- #include <stdio.h>
- #include <memory.h>
- #include <stdlib.h>
- void swap_int(int* a,int* b)
- {
- int c;
- c = *a;
- *a = *b;
- *b = c;
- }
- //插入排序
- void InsertionSort(int A[],int N)
- {
- int j,p;
- int tmp;
- for(p = 1; p < N; p++)
- {
- tmp = A[p];
- for(j = p;j > 0 && A[j - 1] >tmp;j--)
- {
- A[j] = A[j - 1];
- }
- A[j] = tmp;
- }
- }
- //三數取中分割法
- int Median3(int A[],int Left,int Right)
- {
- int Center = (Left + Right) / 2;
- if (A[Left] > A[Center])
- swap_int(&A[Left],&A[Center]);
- if (A[Left] > A[Right])
- swap_int(&A[Left],&A[Right]);
- if (A[Center] > A[Right])
- swap_int(&A[Center],&A[Right]);
- swap_int(&A[Center],&A[Right - 1]);
- return A[Right - 1];
- }
- //快速排序
- void QuickSort(int A[],int Left,int Right)
- {
- int i,j;
- int Pivot;
- const int Cutoff = 3;
- if (Left + Cutoff <= Right)
- {
- Pivot = Median3(A,Left,Right);
- i = Left;
- j = Right - 1;
- while (1)
- {
- while(A[++i] < Pivot){;}
- while(A[--j] > Pivot){;}
- if (i < j)
- swap_int(&A[i],&A[j]);
- else
- break;
- }
- swap_int(&A[i],&A[Right - 1]);
- QuickSort(A,Left,i - 1);
- QuickSort(A,i + 1,Right);
- }
- else
- {
- InsertionSort(A+Left,Right - Left + 1);
- }
- }
- //const int KNUM = 40;
- //分塊數
- const int NUMBER = 10000000;
- //輸入文件最大讀取的整數的個數
- //爲了便於測試,我決定改成小文件小數據量進行測試。
- const int KNUM = 4;
- //分塊數const int NUMBER = 100;
- //輸入文件最大讀取的整數的個數
- const char *in_file = "infile.txt";
- const char *out_file = "outfile.txt";
- //#define OUTPUT_OUT_FILE_DATA
- //數據量大的時候,沒必要把所有的數全部打印出來,所以可以把上面這句註釋掉。
- void gen_infile(int n)
- {
- int i;
- FILE *f = fopen(in_file, "wt");
- for(i = 0;i < n; i++)
- fprintf(f,"%d ",rand());
- fclose(f);
- }
- int read_data(FILE *f,int a[],int n)
- {
- int i = 0;
- while ((i < n) && (fscanf(f,"%d",&a[i]) != EOF))
- i++;
- printf("read: %d integer/n",i);
- return i;
- }
- void write_data(FILE *f,int a[],int n)
- {
- int i;for(i = 0; i< n;i++)
- fprintf(f,"%d ",a[i]);
- }
- char* temp_filename(int index)
- {
- char *tempfile = (char*) malloc(64*sizeof(char));
- assert(tempfile);
- sprintf(tempfile, "temp%d.txt", index);
- return tempfile;
- }
- //K路串行讀取
- void k_num_read(void)
- {
- char* filename;
- int i,cnt,*array;
- FILE* fin;
- FILE* tmpfile;
- //計算knum,每路應讀取的整數個數int n = NUMBER/KNUM;
- if (n * KNUM < NUMBER)n++;
- //建立存儲分塊讀取的數據的數組
- array = (int*)malloc(n * sizeof(int));assert(array);
- //打開輸入文件
- fin = fopen(in_file,"rt");
- i = 0;
- //分塊循環讀取數據,並寫入硬盤上的臨時文件
- while ( (cnt = read_data(fin,array,n))>0)
- {
- //對每次讀取的數據,先進行快速排序,然後寫入硬盤上的臨時文件
- QuickSort(array,0,cnt - 1);
- filename = temp_filename(i++);
- tmpfile = fopen(filename,"w");
- free(filename);
- write_data(tmpfile,array,cnt);
- fclose(tmpfile);
- }
- assert(i == KNUM);
- //沒有生成K路文件時進行診斷
- //關閉輸入文件句柄和臨時存儲數組
- fclose(fin);
- free(array);
- }
- //k路合併(敗者樹)
- void k_num_merge(void)
- {
- FILE *fout;
- FILE **farray;
- char *filename;
- int *data;
- char *hasNext;
- int i,j,m,min;
- #ifdef OUTPUT_OUT_FILE_DATAint id;
- #endif
- //打開輸出文件
- fout = fopen(out_file,"wt");
- //打開各路臨時分塊文件
- farray = (FILE**)malloc(KNUM*sizeof(FILE*));
- assert(farray);
- for(i = 0; i< KNUM;i++)
- {
- filename = temp_filename(i);
- farray[i] = fopen(filename,"rt");
- free(filename);
- }
- //建立KNUM個元素的data,hasNext數組,存儲K路文件的臨時數組和讀取結束狀態
- data = (int*)malloc(KNUM*sizeof(int));
- assert(data);
- hasNext = (char*)malloc(sizeof(char)*KNUM);
- assert(hasNext);
- memset(data, 0, sizeof(int) * KNUM);
- memset(hasNext, 1, sizeof(char) * KNUM);
- //讀K路文件先讀取第一組數據,並對讀取結束的各路文件設置不可再讀狀態
- for(i = 0; i < KNUM; i++)
- {
- if(fscanf(farray[i], "%d", &data[i]) == EOF)
- {
- hasNext[i] = 0;
- }
- }
- //讀取各路文件,利用敗者樹從小到大輸出到輸出文件
- #ifdef OUTPUT_OUT_FILE_DATAid = 0;
- #endif
- j = 0;F_LOOP:
- if (j < KNUM)
- //以下這段代碼嵌套過深,日後應儘量避免此類問題。
- {
- while(1==1)
- {
- min = data[j];
- m = j;
- for(i = j+1; i < KNUM; i++)
- {
- if(hasNext[i] == 1 && min > data[i])
- {
- min = data[i];m = i;
- }
- }
- if(fscanf(farray[m], "%d", &data[m]) == EOF)
- {
- hasNext[m] = 0;
- }
- fprintf(fout, "%d ", min);
- #ifdef OUTPUT_OUT_FILE_DATAprintf("fout :%d %d/n",++id,min);
- #endif
- if (m == j && hasNext[m] == 0)
- {
- for (i = j+1; i < KNUM; i++)
- {
- if (hasNext[m] != hasNext[i])
- {
- m = i;
- //第i個文件未讀完,從第i個繼續往下讀
- break;
- }
- }
- if (m != j)
- {
- j = m;
- goto F_LOOP;
- }
- break;
- }
- }
- }
- //關閉分配的數據和數組
- free(hasNext);
- free(data);
- for(i = 0; i < KNUM; ++i)
- {
- fclose(farray[i]);
- }
- free(farray);
- fclose(fout);
- }
- int main()
- {
- time_t start = time(NULL),end,start_read,end_read,start_merge,end_merge;
- gen_infile(NUMBER);
- end = time(NULL);
- printf("gen_infile data time:%f/n", (end - start) * 1000.0/ CLOCKS_PER_SEC);
- start_read = time(NULL);k_num_read();
- end_read = time(NULL);
- printf("k_num_read time:%f/n", (end_read - start_read) * 1000.0/ CLOCKS_PER_SEC);
- start_merge = time(NULL);
- k_num_merge();
- end_merge = time(NULL);
- printf("k_num_merge time:%f/n", (end_merge - start_merge) * 1000.0/ CLOCKS_PER_SEC);
- end = time(NULL);
- printf("total time:%f/n", (end - start) * 1000.0/ CLOCKS_PER_SEC);
- return 0;
- }
程序測試:
在此,我們先測試下對10000000個數據的文件進行40趟排序,然後再對100個數據的文件進行4趟排序(讀者可進一步測試)。如弄幾組小點的數據,輸出ID和數據到屏幕,再看程序運行效果。
- 10個數, 4組
- 40個數, 5組
- 55個數, 6組
- 100個數, 7組
(備註:1、以上所有各節的程序運行環境爲windows xp + vc6.0 + e5200 cpu 2.5g主頻,2、感謝5爲本文程序所作的大量測試工作)
全文總結:
1、關於本章中位圖和多路歸併兩種方案的時間複雜度及空間複雜度的比較,如下:
時間複雜度 空間複雜度
位圖 O(N) 0.625M
多位歸併 O(Nlogn) 1M
(多路歸併,時間複雜度爲O(k*n/k*logn/k ),嚴格來說,還要加上讀寫磁盤的時間,而此算法絕大部分時間也是浪費在這上面)
2、bit-map
適用範圍:可進行數據的快速查找,判重,刪除,一般來說數據範圍是int的10倍以下
基本原理及要點:使用bit數組來表示某些元素是否存在,比如8位電話號碼
擴展:bloom filter可以看做是對bit-map的擴展
問題實例:
1)已知某個文件內包含一些電話號碼,每個號碼爲8位數字,統計不同號碼的個數。
8位最多99 999 999,大概需要99m個bit,大概10幾m字節的內存即可。
2)2.5億個整數中找出不重複的整數的個數,內存空間不足以容納這2.5億個整數。
將bit-map擴展一下,用2bit表示一個數即可,0表示未出現,1表示出現一次,2表示出現2次及以上。或者我們不用2bit來進行表示,我們用兩個bit-map即可模擬實現這個2bit-map。
3、[外排序適用範圍]大數據的排序,去重基本原理及要點:外排序的歸併方法,置換選擇敗者樹原理,最優歸併樹擴展。問題實例:1).有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16個字節,內存限制大小是1M。返回頻數最高的100個詞。這個數據具有很明顯的特點,詞的大小爲16個字節,但是內存只有1m做hash有些不夠,所以可以用來排序。內存可以當輸入緩衝區使用。
4、海量數據處理
有關海量數據處理的方法或面試題可參考此文,十道海量數據處理面試題與十個方法大總結。日後,會逐步實現這十個處理海量數據的方法。同時,送給各位一句話,解決問題的關鍵在於熟悉一個算法,而不是某一個問題。熟悉了一個算法,便通了一片題目。
本章完。
updated:有一讀者朋友針對本文寫了一篇文章爲,海量數據多路歸併排序的c++實現(歸併時利用了敗者樹),地址爲:http://www.cnblogs.com/harryshayne/archive/2011/07/02/2096196.html。謝謝,歡迎參考。
以下是自己已編譯OK 的程序代碼,(可能與上面的有衝突的地方)
- /*=====================================
- Filename:
- sortfile.c
- Copyright (C) 2006 caicai
- All rights reserved.
- Description:
- This file is used to sort about file.
- Author/Created Date:
- Lumi-liu, Nov19'14
- Modification History:
- Note:
- =======================================*/
- #include <stdio.h>
- #include <stdlib.h>
- #include <memory.h>
- #include <time.h>
- #include <assert.h>
- //數據交換函數
- void swap_int(int* a,int* b)
- {
- int c;
- c = *a;
- *a = *b;
- *b = c;
- }
- //插入排序
- void InsertionSort(int a[],int N)
- {
- int j;
- int p;
- int tmp;
- for(p=0; p<N; p++)//p=1
- {
- tmp = a[p]; //p=0
- for(j=p;/* j>0 && */a[j-1]>tmp; j--)//
- {
- a[j] = a[j-1];
- }
- a[j] = tmp;
- }
- }
- //三數取中分割法
- int Median3(int a[],int Left,int Right)
- {
- //if ( Left+3 >= Right )
- // return -1;
- int Cent = (Left + Right) / 2;
- if (a[Left] > a[Cent])
- swap_int(&a[Left],&a[Cent]);
- if (a[Left] > a[Right])
- swap_int(&a[Left],&a[Right]);
- if (a[Cent] > a[Right])
- swap_int(&a[Cent],&a[Right]);
- swap_int(&a[Cent],&a[Right-1]);
- return a[Right-1];
- }
- //快速排序
- void QuickSort(int a[],int Left,int Right)
- {
- int i;
- int j;
- int Center;
- const int Cutoff = 3;
- if (Left + Cutoff <= Right)
- {
- Center = Median3(a,Left,Right);
- i = Left;
- j = Right-1;
- while (1)//for()
- {
- while(a[++i] < Center){;}//下標右移
- while(a[--j] > Center){;}//下標左移
- if (i < j)
- swap_int(&a[i],&a[j]);
- else
- break;
- }
- swap_int(&a[i],&a[Right-1]);
- QuickSort(a,Left,i-1);
- QuickSort(a,i+1,Right);
- }
- else
- {
- InsertionSort(a+Left,Right-Left+1);
- }
- }
- const int Num = 100000000;
- //const int Num = 10000000;//文件內整數的個數
- //便於測試,可先定義Num = 1000;
- const int Knum = 4;//分割成文件的個數
- const char *Ifile = "infile.txt";//未排序的文件名
- const char *Ofile = "outfile.txt";//最終的文件名
- //生成隨機數文件
- void gen_infile(int n)
- {
- int i;
- FILE *fp = fopen(Ifile, "wt");
- for(i = 0;i < n; i++)
- fprintf(fp,"%d ",rand()%1000);
- fclose(fp);
- }
- //讀取臨時文件中數據的個數
- int read_data(FILE *fp,int a[],int n)
- {
- int i = 0;
- while ((i < n) && (fscanf(fp,"%d",&a[i]) != EOF))
- i++;
- printf("read: %d integer\n",i);
- return i;
- }
- //寫入文件
- void write_data(FILE *fp,int a[],int n)
- {
- int i;
- for(i=0; i<n; i++)
- fprintf(fp,"%d ",a[i]);
- }
- //臨時文件的命名
- char* temp_filename(int index)
- {
- char *tempfile = (char*) malloc(64*sizeof(char));
- assert(tempfile);
- sprintf(tempfile, "temp%d.txt", index+1);
- return tempfile;
- }
- //k路串行讀取數據
- void k_num_read(void)
- {
- char* filename;
- int i;
- int cnt;
- int *arr;
- int n=Num/Knum;
- FILE* fin;
- FILE* tmpfile;
- if (n*Knum < Num)
- n++;//各個文件要讀取數據的個數
- arr = (int*)malloc(n * sizeof(int));
- assert(arr);
- fin = fopen(Ifile,"rt");
- i = 0;
- //分塊循環讀取數據並寫入臨時文件
- while ( (cnt = read_data(fin,arr,n))>0)
- {
- //每次讀取時先進行排序
- QuickSort(arr,0,cnt-1);
- filename = temp_filename(i++);
- tmpfile = fopen(filename,"w");
- free(filename);
- write_data(tmpfile,arr,cnt);
- fclose(tmpfile);
- }
- //沒有生成k路文件時進行診斷
- assert(i == Knum);
- fclose(fin);
- free(arr);
- }
- //對臨時文件進行合併(敗者樹)
- void k_num_merge(void)
- {
- FILE *fout;
- FILE **farr;
- char *filename;
- int *data;
- char *hasNext;
- int i,j,m,min;
- //#ifdef OUTPUT_Ofile_DaTaint id;
- //#endif
- fout = fopen(Ofile,"wt");
- //打開文件
- farr = (FILE**)malloc(Knum*sizeof(FILE*));
- assert(farr);
- for(i = 0; i< Knum;i++)
- {
- filename = temp_filename(i);
- farr[i] = fopen(filename,"rt");
- free(filename);
- }
- //建立包含knum個元素的data,hasnext數組
- //存儲k路文件的臨時數組和讀取結束狀態
- data = (int*)malloc(Knum*sizeof(int));
- assert(data);
- hasNext = (char*)malloc(sizeof(char)*Knum);
- assert(hasNext);
- memset(data, 0, sizeof(int) * Knum);
- memset(hasNext, 1, sizeof(char) * Knum);
- //先讀取第一組數據
- for(i = 0; i < Knum; i++)
- {
- if(fscanf(farr[i], "%d", &data[i]) == EOF)
- {
- hasNext[i] = 0;
- }
- }
- //#ifdef OUTPUT_Ofile_DaTaid = 0;
- //#endif
- j = 0;
- F_LOOP:
- if (j < Knum)
- {
- while(1)
- {
- min = data[j];
- m = j;
- for(i = j+1; i < Knum; i++)
- {
- if(hasNext[i] == 1 && min > data[i])
- {
- min = data[i];
- m = i;
- }
- }
- if(fscanf(farr[m], "%d", &data[m]) == EOF)
- {
- hasNext[m] = 0;
- }
- fprintf(fout, "%d ", min);
- //#ifdef OUTPUT_Ofile_DaTaprintf("fout :%d %d/n",++id,min);
- //#endif
- if (hasNext[m] == 0) //m == j &&
- {
- for (i = j+1; i < Knum; i++)
- {
- if (hasNext[m] != hasNext[i])
- {
- m = i;//
- break;
- }
- }
- if (m != j)
- {
- j = m;
- goto F_LOOP;
- }
- break;
- }
- }
- }
- free(hasNext);
- free(data);
- for(i = 0; i < Knum; ++i)
- {
- fclose(farr[i]);
- }
- free(farr);
- fclose(fout);
- }
- int main()
- {
- time_t start = time(NULL),end,start_read,end_read,start_merge,end_merge;
- gen_infile(Num);
- end = time(NULL);
- //生成文件的時間
- printf("gen_infile data time:%f\n",
- (end - start) * 1000.0/ CLOCKS_PER_SEC);
- start_read = time(NULL);
- k_num_read();
- end_read = time(NULL);
- //讀取文件的時間
- printf("k_num_read time:%f\n",
- (end_read - start_read) * 1000.0/ CLOCKS_PER_SEC);
- start_merge = time(NULL);
- k_num_merge();
- end_merge = time(NULL);
- //臨時文件合併的時間
- printf("k_num_merge time:%f\n",
- (end_merge - start_merge) * 1000.0/ CLOCKS_PER_SEC);
- end = time(NULL);
- //總時間
- printf("total time:%f\n",
- (end - start) * 1000.0/ CLOCKS_PER_SEC);
- return 0;
- }