外排序多路歸併+敗者樹-算法學習筆記十五

問題:一個文件有大量的數,現要對文件排序,但內存無法一次讀取完全,而磁盤空間足夠,要如何排序。

學習了幾篇博客:
1. july大神的海量數據排序(他的其他博客都很值得看)
2. 對july大神的算法進行改進不用選擇法而是敗者樹的博客
3. 以及另一篇但不知道是否爲原創的博客
4. 還有生成不重複亂序m-n的數的博客(先生成m-n的數,然後洗牌算法)


以上幾篇博客寫得很完全了,看懂了思路,自己臨摹寫一個簡單的測試 ….

先用生成隨機數的代碼生成data.txt待排序大文件:
(照搬的前面貼的博客的,加了個取低31位,因爲我的跑出來老是段錯誤,gdb調試生成的隨機索引爲負數,太困了不想深究原因了 …)

//生成隨機的不重複的測試數據
#include <iostream>
#include <stdio.h>
#include <time.h>
#include <assert.h>
#include <stdlib.h>  // RAND_MAX
using namespace std;
//產生[i,u]區間的隨機數
int randint(int l, int u)
{
    int a = RAND_MAX * rand();
    int b = rand();
    //取低31位
    int c = ( a + b ) & (0x7fffffff) % ( u - l + 1 );
    int d = l + c;
    return d;
}

const int size = 10000000;
// const int size = 10;
int num[size];
int main()
{
    srand((int)time(NULL));
    int i, j;
    FILE *fp = fopen("data.txt", "w");
    assert(fp);
    for (i = 0; i < size; i++)
        num[i] = i+1;
    // printf("rand_max:%d\n", RAND_MAX);
    for (i = 0; i < size; i++)
    {
        j = randint(i, size-1);
        // printf("%d ", j);
        fflush(stdout);
        int t = num[i]; num[i] = num[j]; num[j] = t;
        //swap(num[i], num[j]);
    }
    // printf("\n");
    for (i = 0; i < size; i++)
        fprintf(fp, "%d\n", num[i]);
    fclose(fp);
    return 0;
}

對data.txt文件開始外排序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

#define TEMP_PREFIX "ftemp_"
#define OUTPUT_FILE "out_data.txt"
#define MAX_WAYS  100
// 無窮大,用於某一路文件或緩衝區讀到尾了
// 敗者樹產生一個註定失敗的結點
#define INFINITY 1000000000

int *buf;
int lst[MAX_WAYS];

void read_data(FILE *fp, int *buf)
{
    if ( fscanf(fp, "%d ", buf) == EOF )
        *buf = INFINITY;
}
int partition( int *arr, int p, int r )
{
    int x = arr[r];
    int i = p - 1;
    int j = 0, temp;

    for ( j = p; j <= r - 1; j++ ) {
        if ( arr[j] <= x ) {
            i += 1;
            temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    temp = arr[i + 1];
    arr[i + 1] = arr[r];
    arr[r] = temp;

    return i + 1;
}
void quick_sort( int *arr, int p, int r )
{
    if ( p < r ) {
        int q = partition( arr, p, r );
        quick_sort( arr, p, q - 1 );
        quick_sort( arr, q + 1, r );
    }
}
void adjust( int k, int s )
{
    // t爲結點s在敗者樹數組的父結點,
    // 例如64路歸併,輸入63/62,他們
    // 的父結點均爲 63
    int t = ( k + s ) / 2;

    while ( t > 0 ) {
        if ( s == -1 ) {
            break;
        }

        // 第一趟,輸入一個葉子結點s,讓s與父結點的值比較
        // (這裏父結點一定保存上一次比較的較大者),如果s
        // 大於父結點的值,表示s爲新的敗者,將表示s結點的
        // 索引放到父結點處,勝者(父結點)繼續到更上層的
        // 父結點進行比較,這樣比較完後,頂點一定放置的最
        // 小的值

        // 將敗者樹想象爲一場淘汰賽,假如有8個參賽者入口,
        // 8個參賽者編號1-8,第一輪(即初始化敗者樹),假
        // 設產生了2/4/6/8四強敗者,放置到8個入口上一層(
        // 即父結點),然後再產生兩強敗者5/7,放置到更上一
        // 層的結點,然後產生最後的失敗者1,而剩餘的8就是最
        // 終的勝者,這樣一棵敗者樹就初始化好了,

        // 8個入口,之後我們可以隨便哪個入口加入一個參賽者,
        // 這個參賽者只需要與父結點進行比較,敗者留下,勝者
        // 可以往更高的父結點去參加比賽....這樣每一輪進入一
        // 個參賽者,每次能得到一個新的冠軍(最小值),然後寫入
        // 文件末尾

        // 這裏的8其實就是8路歸併,這個入口的參賽者每次就去
        // 讀取8個排好序的文件或緩衝區
        if ( buf[s] > buf[lst[t]] ) {
            int temp = s;
            s = lst[t];
            lst[t] = temp;
        }
        t >>= 1;
    }
    // 以2^n次方來算,頂層敗者編號爲1,所以敗者樹數組lst[0]一定
    // 沒存東西,可以用來存放最後的冠軍
    lst[0] = s;
}
void create_loser_tree(int k)
{
    int i = 0;
    for( i; i < k; i++ ) {
        lst[i] = -1;
    }

    for( i = k - 1; i >= 0; i-- ) {
        adjust(k, i);
    }
}
void k_merge(
    int k )
{
    int i = 0;
    FILE **ftemp = ( FILE * )malloc( sizeof(FILE *) * k );
    FILE *fout = NULL;

    // 歸併路數大小的數組,每個數組值存放每一個歸併路文件讀取的
    // 一個值,某一個索引的值寫入輸出文件,又讀取對應文件下一個
    // 值補充
    buf = ( int * )malloc( sizeof(int) * k );

    fout = fopen( OUTPUT_FILE, "w+" );

    for ( i; i < k; i++ ) {
        char file_name[20] = {0};
        snprintf( file_name, sizeof(file_name), TEMP_PREFIX"%d", i );
        ftemp[i] = fopen( file_name, "r" );
        // 讀取每個排序好的臨時文件第一個數
        fscanf( ( FILE * )ftemp[i], "%d ", buf + i );
    }

    // 以排好序文件第一個數的數組來創建敗者樹,
    // 樹結點產生敗者,這樣以後的每輪比較只需要
    // 去文件或緩衝區讀取下一個值加入敗者樹入口即可
    create_loser_tree( k );

    // 開始歸併, 哪一個入口產生的冠軍,先把冠軍寫入輸出文件,
    // 然後冠軍所屬的文件或緩衝區再讀入一個數進行比賽,如果某一路
    // 文件或緩衝區讀到尾了,那麼這個入口的參賽者爲無限大,這樣
    // 與之共有一個父結點的兄弟結點每次讀取的值都能成爲勝者,參加
    // 父結點以上的比較,到所有節點都讀完時,最終敗者結點,即lst[1]
    // 爲無窮大,再加入一個參賽者,lst[0]也爲無窮大了,
    while ( buf[lst[0]] != INFINITY ) {
        // 讀取冠軍的值
        int q = lst[0];

        // 將冠軍寫入輸出文件
        fprintf(fout, "%d\n", buf[q]);

        // 讀取冠軍所屬隊列(文件或緩衝區)的下一個值
        read_data(ftemp[q], &buf[q]);

        // 加入了一個新參賽者,調整敗者樹
        adjust(k, q);
    }

    // 清理
    free( buf );

    for ( i = 0; i < k; i++ ) {
        fclose(ftemp[i]);
    }
}

void memory_sort_small_file(
    FILE *fp,
    int num, // 待排序數的數量
    int k )
{
    int i = 0;
    int num_per_ways = num / k; // 每一路多少個數
    int *buf = NULL;
    FILE **ftemp = ( FILE * )malloc( sizeof(FILE *) * k );

    buf = ( int * )malloc( sizeof(int) * num_per_ways + 1000 );

    // for ( i = 0; i < k; i++ ) {
    //     char temp_buf[20] = {0};
    //     snprintf( temp_buf, sizeof(temp_buf), TEMP_PREFIX"%d", i);
    //     ftemp[i] = fopen( temp_buf, "w+" );
    //     if ( ftemp[i] == NULL ) {
    //         printf("[%s:%d],error occured!!(%s)\n", __func__, __LINE__, strerror(errno));
    //         exit( 0 );
    //     }
    // }

    // 先不處理最後一個,可能總數/k路帶餘數,多餘的
    // 留到最後一個文件處理
    k--;
    while ( k > 0 ) {

        char temp_buf[20] = {0};
        snprintf( temp_buf, sizeof(temp_buf), TEMP_PREFIX"%d", k);
        ftemp[k] = fopen( temp_buf, "w+" );
        if ( ftemp[k] == NULL ) {
            printf("[%s:%d],error occured!!(%s)\n", __func__, __LINE__, strerror(errno));
            exit( 0 );
        }

        i = 0;
        memset( buf, 0, sizeof(buf) );

        for ( i; i < num_per_ways; i++ ) {
            fscanf(fp, "%d ", &buf[i]);
        }
        printf("%s:%d, K:%d\n", __func__, __LINE__, k);
        quick_sort( buf, 0, num_per_ways - 1 );
        for ( i = 0; i < num_per_ways; i++ ) {
            fprintf(ftemp[k], "%d ", buf[i]);
        }
        fclose( ftemp[k] );
        k--;
    }

    // 處理剩餘的最後一個待排序文件
    char temp_buf[20] = {0};
    snprintf( temp_buf, sizeof(temp_buf), TEMP_PREFIX"%d", 0);
    ftemp[0] = fopen( temp_buf, "w+" );
    if ( ftemp[0] == NULL ) {
        printf("[%s:%d],error occured!!(%s)\n", __func__, __LINE__, strerror(errno));
        exit( 0 );
    }

    i = 0;
    while ( fscanf(fp, "%d ", &buf[i]) != EOF ) i++;
    printf("%s:%d, K:%d\n", __func__, __LINE__, 0);
    quick_sort( buf, 0, i );

    int j = 0;
    for ( j = 0; j <= i; j++ ) {
        fprintf(ftemp[0], "%d ", buf[j]);
    }
    free( buf );
    fclose( ftemp[0] );
}
int main(
    int argc,
    char **argv )
{
    if ( argc != 3 ) {
        printf("usage:\n\t./xxx file_name k ways to merge\n");
        exit( 0 );
    }



    int k = atoi( argv[2] );
    char *file_name = argv[1];

    FILE *fp = fopen(file_name, "r");
    if ( fp == NULL ) {
        printf("[%s:%d],error occured!!(%s)\n", __func__, __LINE__, strerror(errno));
        exit( 0 );
    }

    time_t t1 = time(NULL), t2, t3;
    memory_sort_small_file( fp, 10000000, k );

    t2 = time(NULL);

    k_merge( k );

    t3 = time(NULL);

    printf("---------------------------finish-----------------------------\n");

    printf("\tmemory sort & ouput to temp file cost:  %ds\n", (int)(t2 - t1));

    printf("\tk_merge & ouput to file cost:  %ds\n", (int)(t3 - t2));

    printf("\ttotal cost time:  %ds\n", (int)(t3 - t1));

    printf("--------------------------------------------------------------\n");

    fclose( fp );

    return 0;
}

64路歸併排序1000w個數用時:
這裏寫圖片描述

生成文件:
這裏寫圖片描述

排序後的文件頭和尾:
這裏寫圖片描述
這裏寫圖片描述

代碼註釋寫了很多了,以後忘了回頭看看也能記起來 …..


好睏 ________________________

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