算法學習(七)有內存限制的海量數據排序

磁盤文件排序

問題描述:
輸入:給定一個文件,裏面最多含有n個不重複的正整數(也就是說可能含有少於n個不重複正整數),且其中每個數都小於等於n,n = 10^7。
輸出:得到按從小到大升序排列的包含所有輸入的整數的列表。
條件:最多有大約1MB的內存空間可用,但磁盤空間足夠。且要求運行時間在五分鐘以下,10秒爲最佳結果。
分析:
首先注意的是它的內存要求,基本上否決了大多數的排序算法。
文件裏面的數據是不重複的,這個很關鍵,因爲下面的方案前提就是數據不重複。

位圖方案

以位爲最小單位,對應位爲1,代表位數值,可以用20位長的字符串來表示一個所有元素都小於20的簡單的非負數集合,邊框用如下字符串來表示集合{1,2,3,5,8,13}:
01110100100001000000
對應位置則置1,沒用對應數的位置則置0,開始扳手指頭數數吧。我們可以採用一個具有1000萬個位的字符串來表示這個文件,其中,當整數i在文件中時,第i位爲1。
第一步,所有位置0,將集合初始化。
第二步,通過讀入文件中的每個整數來建立集合,將對應的位都置1.
第三步,檢驗每一位,如果該位爲1,就輸出對應的整數。
這裏不得不提一下c++庫中的一個容器bitset,發現這個在處理位的時候簡直牛,先來瞅瞅。

#include <iostream>
#include <bitset>
using namespace std;
int main()
{
    const int max = 20;
    int arr[10] = {2,9,3,5,4,0,18,11,12,15};
    bitset<max> bit_map;  //一種類模板,放入長度,定義了一個max位,每位都爲0
    bit_map.reset();  //所有位置0
    int i;
    for(i = 0;i< 10;i++)
    {
        bit_map.set(arr[i]);  //將對應位置1
    }
    for(i = 0;i< max;i++)
    {
        if(bit_map.test(i))  //判斷第i位,如果爲1,返回true
            cout << i << " ";
    }
    cout << endl;
    return 0;
}

再加上文件操作,就是針對海量數據排序。

#include <iostream>
#include <bitset>
#include <assert.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

const int MAX = 5000000;
int main()
{
    clock_t begin = clock();
    bitset<MAX> bit_map;
    bit_map.reset();

    FILE * fp_unsort_file = fopen("data.txt","r");
    assert(fp_unsort_file);
    int num;
    while(fscanf(fp_unsort_file,"%d ",&num)!= EOF)
    {
        if(num < MAX)   //先對1-4999999的數進行排序
            bit_map.set(num);
    }
    FILE * fp_sort_file = fopen("sort.txt","w");
    assert(fp_sort_file);
    int i;
    for(i= 0;i < MAX;i++)
    {
        if(bit_map.test(i))
            fprintf(fp_sort_file,"%d ",i);
    }
    int result = fseek(fp_sort_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 && num < 10000000)  //再對5000000-10000000的數排序
            {
                num -=MAX;
                bit_map.set(num);
            }
        }
        for(i = 0;i<MAX;i++)
        {
            if(bit_map.test(i))
                fprintf(fp_sort_file,"%d ",i+MAX);
        }
    }
    clock_t end = clock();
    cout << "time is:" << endl;
    cout << (end-begin)/CLOCKS_PER_SEC << "s" << endl;
    fclose(fp_sort_file);
    fclose(fp_unsort_file);
    return 0;
}

第一次只處理1-4999999之間的數據,對這些數進行位圖排序,只需要約5000000/8 = 625000byte,就是0.625M,排序後輸出。
第二次,掃描輸入文件,只處理4999999-10000000的數據項,只需要0.625M,因此只需要0.625M。

多路歸併

我們先了解一下,歸併排序的過程,歸併排序就是2路歸併
歸併排序的過程:
1,把無序表的每一個元素看做是一個有序表,則有n個有序子表;
2,把n個有序表按相鄰位置分成若干對,每對中的兩個表進行歸併,歸併後子表數減少一半。
3,反覆進行這一過程,直到歸併爲一個有序表爲止。
是採用分治法的典型應用
首先考慮下如何將二個有序數列合併,比較二個數列第一個數,誰小就放入到臨時數組中,然後再進行比較,如果一個數列到達結尾,直接將另一個數列放入就臨時數組中。

oid MemeryArray(int a[],int n,int b[],int m,int c[])
{
    int i,j,k;
    i = j= k = 0;
    while(i < n && j< m)
    {
        if(a[i] < b[j])
            c[k++] = a[i++];
        else
            c[k++] = b[j++];
    }
    while(i < n)
        c[k++] = a[i++];
    while(j < n)
        c[k++] = b[j++];
}

下一步考慮如何讓兩個A,B數組有序,可以將A,B各自再分成二組,依次類推,當分出來的小組只有一個元素時,這個小組內已經達到有序了,然後再合併相鄰的二個小組就可以了,先遞歸分解,再合併數列就完成了歸併排序。

//將二個有序數列a[first...mid] 和a[mid+1...last]合併
void MemeryArray(int a[],int first,int mid,int last,int temp[])
{
    int i  = first;
    int j = mid+1;
    int m = mid;
    int n = last;
    int k = 0;
    while(i <= m && j< =n)
    {
        if(a[i] < b[j])
            temp[k++] = a[i++];
        else
            temp[k++] = a[j++];
    }
    while(i <= m)
        temp[k++] = a[i++];
    while(j <=n)
        temp[k++] = a[j++];
    for(i = 0;i<k;i++)
        a[first+i] = temp[i];
}
void mergesort(int a[],int first,int last,int temp[])
{
    if(first < last)
    {
        int mid = (first + last)/2;
        mergesort(a,first,mid,tmep);
        mergesort(a,mid+1,last,temp);
        MemeryArray(a,first,mid ,last,temp);
    }
}

歸併排序是一種穩定排序,時間複雜度最好情況下和最壞情況下均是O(nlogn)
多路歸併就是從多個有序數列中歸併。
比如將10000000的數據,分成40個有序文件,分別在內存中排序,然後對這40個有序文件進行歸併排序。
1,讀取每個文件中第一個數(每個文件的最小數),存放在一個大小爲40的data數組中,
2,選擇data數組中最小的數min_data,及其相應的文件索引(來自哪個文件)index
3,將min_data寫入到文件result,然後更新數組data(根據index,讀取該文件的下一個數代替min_data)
4,判讀是否所有數據都讀取完畢,否則返回到2步。

#include <iostream>
#include <string>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

using namespace std;

int sort_num = 10000000;
int memory_size = 250000;  //每次排序250000個數據

int read_data(FILE *fp,int *space)
{
    int index = 0;
    while(index < memory && 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++;
    }
}
void check_fp(FILE *fp)
{
    if(fp == NULL)
    {
        cout << "the file not open" << endl;
        exit(1);
    }
}
int compare(const void * a,const void * b)
{
    return *(int *)a-*(int *)b;
}
string new_filename(int n)
{
    char file_name[20];
    sprintf(file_name,"data%d.txt",n);
    return file_name;
}
//將原文件分割成多個小文件,並在內存中排序
int memroy_sort()
{
    FILE * fp_in_file = fopen("data.txt","r");
    check_fp(fp_in_file);
    int counter = 0;
    while(true)
    {
        int * space = new int(memory_size);
        int num = read_data(fp_in_file,space);
        if(num == 0)
            break;
        qsort(space,num,sizeof(int),compare);
        string file_name = new_filename(++counter);
        FILE *fp_aux_file = fopen(file_name,"w");
        check_fp(fp_aux_file);
        write_data(fp_aux_file,space,num);
        fclose(fp_aux_file);
        delete []space;
    }
    fclose(fp_in_file);
    return counter;
}
//將n個小文件歸併爲一個文件,多路歸併
void merge_sort(int file_num)
{
    if(file_num < 0)
        return;
    FILE *fp_out_file = fopen("result.txt","w");
    check_fp(fp_out_file);
    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,"r");
        check_fp(fp_array[i]);
    }
    int * first_data  = new int [file_num];
    bool * finish  = new bool[file_num];  //文件是否讀取到結尾
    memset(finish,false,sizeof(bool) * file_num);

    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++;
        if(index >=file_name)
            break;
        int min_data = first_data[index];
        for(i = index+1;i<file_name,i++)
        {
            if(min_data > first_data[i] && !finish[index])
            {
                min_data = first_data[i];
                index = i;
            }
        }
        fprintf(fp_out_file ,"%d ",min_data);  //將最小的輸出
        if(fscanf(fp_array[index],"%d ",&first_data[index]) == EOF) //根據index,找到該文件的下一個數,放入到first_data中。如果這個文件已經讀取到結尾,則將標誌置爲true。
            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 = clock();
    int aux_file_num = memory_sort();
    clock_t memory_end = clock();
    cout << "time need in memory sort" << memory_end-start << endl;
    merge_sort(aux_file_num);
    clock_t end = clock();
    cout << "the time need in merge sort" << end - memory_end << endl;
    return 0;
}
qsort()用法

int cmp(const void* a,const void * b);
void qsort(char *,int n,int m,cmp);
包含在stdlib.h中,函數四個參數,
參與排序的數組,元素個數,單個元素到小(sizeof),比較函數。
再說比較函數,返回正數,a要放在b後面,返回負數,a要放在b前面,返回0,相等。

解決問題的關鍵是在於熟悉一個算法,而不是某一問題,
通過這個問題,我們學到了兩種方法:
一種:位圖法,
針對與不重複數據排序,進行數據的快速查找,判重,刪除。
二中:多路歸併
海量數據,內存有限的情況下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章