【參考】給大數據量的磁盤文件排序
如何給磁盤文件排序
問題描述
輸入:給定一個文件,裏面最多含有n個不重複的正整數(也就是說可能含有少於n個不重複正整數),且其中每個數都小於等於n,n=107。
輸出:得到按從小到大升序排列的包含所有輸入的整數的列表。
條件:最多有大約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;
參考編程珠璣裏的位圖解決方法,將這裏的7位十進制整數表示一個小於1000萬的整數。我們可以使用一個具有1000萬個位的字符串來表示這個文件,其中,當且僅當整數i在文件中存在時,第i位爲1,採用這個位圖的方案是建立在在這個問題的特殊性上的:
1. 輸入數據限制在相對較小的範圍內。
2. 數據沒有重複。
3. 其中的每條記錄都是單一的整數,沒有任何其他與之關聯的數據。
所以,此問題用位圖的方案分爲以下三步進行解決:
- 第一步:將所有的位都置爲0,從而將集合初始化爲空。
- 第二步:通過輸入文件中的每一個整數來建立集合,將每個對應的位都置爲1。
- 第三步:檢驗每一位,如果該位爲1,就輸出對應的整數。
經過以上三步後,就能產生有序的輸出文件。令n爲位圖向量中的位數(本例中爲1000 0000),程序可以用僞代碼表示如下:
<span style="font-size:18px;">//磁盤文件排序位圖方案的僞代碼
//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</span>
完整代碼如下:
<span style="font-size:18px;">//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;
}</span>
其中產生大數據量(1000W)的程序(生成的數據量的數據文件統一名稱爲“data.txt”)如下:
<span style="font-size:18px;">/*
#define _CRT_SECURE_NO_WARNINGS
//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()
{
clock_t begin = clock();
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]);
clock_t end = clock();
cout << "產生隨機大數據集耗時:" << endl;
cout << (end - begin) / CLK_TCK << "s" << endl;
fclose(fp);
return 0;
}
//*/</span>
而後測試了一下上述程序的運行時間,採取位圖方案耗時14s,即14000ms:
在July的博文中原來是直接用10000000的最大數組來進行操作的,很明顯,100000008=1250000>1048576(1M),已經不符合題目要求,所以,經過修改之後,將數組大小改成5000000,並且分兩次進行排序,內存使用這才符合了要求。
1. 第一次,只處理1-4999999之間的數據,這些數都是小於5000000的,對這些數進行位圖排序,只需要約50000008=625000Byte,也就是0.625MB,排序後輸出。
2. 第二次,掃描輸入文件時,只處理4999999-10000000的數據項,也只需要0.625MB(可以使用第一次處理申請的內存)。
因此,總共也只需要0.625MB。
位圖的方法有必要強調一下,位圖的適用範圍爲針對不重複的數據進行排序,若數據有重複,位圖方案就不適用了。
多路歸併算法的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實現):
<span style="font-size:18px;">//有誤,未改正
//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);
}</span>
磁盤文件排序的編程實現
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的繪製)。
然後,編寫的完整代碼如下:
<span style="font-size:18px;">//data1顯示有問題,過程尚未仔細看!!!!
//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;
}</span>
最終的運行結果,如下,單位統一爲ms:
由上觀之,我們發現,第一節的位圖方案的程序效率是最快的,約爲14s,而採用上述的多路歸併算法的程序運行時間約爲25s。時間主要浪費在讀寫磁盤IO上,且程序中用的庫函數qsort也耗費了不少時間。所以,總的來說,採取位圖方案是最佳方案。
多路歸併算法的c實現
<span style="font-size:18px;">//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;
}</span>
程序測試:
在此,我們先測試下對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(kn/klogn/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。
參考鏈接
- 主要參考v_JULY_v的博文“ 程序員編程藝術:第十章、如何給10^7個數據量的磁盤文件排序”,本文乃其拜讀筆記,相同之處,版權歸大牛所有。