外部排序

620 阅读4分钟

多路平衡归并

多路平衡归并的思想是将大文件化成小文件,小文件要小到能加载到内存中进行内部排序。对各个小文件内部排序写入外存后,再将小文件归并排序。假设初始归并段数为r,执行k路平衡归并,则归并趟数S = logk(r)。

败者树

k路平衡归并需k-1次关键字比较,S趟归并需要的比较次数c为:S(n-1)(k-1) = logk(r)(n-1)(k-1) = log2(r)(n-1)(k-1)/log2(k)。记录数n和初始归并趟数r是固定的,c随k的增大而增大。为进一步减少比较次数,引入败者树。引入败者树后,S趟归并需要的比较次数c为: S(n-1)ceil(log2(k)) = ceil(log2(r))(n-1)。

为什么使用败者树,而不用胜者树或堆来实现排序。

如果要访问内存的值没有存在高速缓冲区,访存消耗的时钟周期将远大于ALU执行计算消耗的时钟周期

  • 比较次数: 堆 > 胜者树 = 败者树
  • 访存次数:堆 >= 胜者树 > 败者树

代码实现

快速排序

int partition(int *a, int low, int high){
	int pivot = a[low];
    while(low < high){
    	while(low < high && a[high] >= pivot) high --;
        a[low] = a[high];
        while(low < high && a[low] < pivot) low ++;
        a[high] = a[low];
    }
    
    a[low] = pivot;
    return low;
}

void QuickSort(int *a, int low, int high){
	if(low >= high) return;
    int pivot = partition(a, low, high);
    QuickSort(a, low, pivot - 1);
    QuickSort(a, pivot + 1, high);
}

编造数据

void MockData(FILE *fp){
    int i, tag = 1, count = 1, num;
    int data[LENGTH + 1];
    data[0] = LENGTH;
    srand((unsigned)time(NULL)); //播种
    for(i = 1; i <= LENGTH; i ++){
        do{
            tag = 0;
            num = rand() % (4 * LENGTH - 1) + 1; //随机数范围1 ~ 4 * LENGTH - 1
            for(int j = 0; j < count; j ++){ //去重
                if(num == data[j]){
                    tag = 1;
                    break;
                }
            }
            if(tag == 0){
                data[count] = num;
                count ++;
            }
        }while(tag == 1);
    }
    for(i = 0; i <= LENGTH; i ++){
        fprintf(fp, "%d,", data[i]);
    }
    fprintf(fp, "%d,", INT_MAX);
    fclose(fp);
}

分割数据并排序,排序后写入小文件

void CutSort(FILE *fp){
    int i, j, len, tmp;
    FILE *fptr;
    SqList L;
    char rePath[100], str[100];
    fscanf(fp, "%d", &len); //读取关键字长度
    int m = len / K; //假设内存一次最多可载入m个数据
    int n = K + (len % m != 0); //n为归并段数
    system("mkdir tmpfile"); //打开文件
    for(i = 0; i < n; i ++){
        strcpy(rePath, "tmpfile/");
        sprintf(str, "%d", i);
        strcat(rePath, strcat(str, ".txt"));
        fptr = fopen(rePath, "w+");
        if(fptr){
            L.len = 0;
            for(j = 0; j < m; j ++){
                if(fscanf(fp, ",%d", &tmp)){
                    L.data[L.len ++] = tmp;
                }else{
                    break;
                }
            }

            QuickSort(L.data, 0, L.len - 1); //内部排序
            for(j = 0; j < L.len; j ++){ //将数据写入文件
                fprintf(fptr, "%d,", L.data[j]);
            }
            fprintf(fptr, "%d,", INT_MAX); //写入哨兵
            fclose(fptr);
        }
    }
    fclose(fp);
}

败者树核心代码

void Adjust(LoserTree ls, External b, int s){
    int tmp;
    int f = (s + K) / 2; //指向s的父节点
    while(f > 0){
        if(b[s] > b[ls[f]]){ //s指向胜者, 小为胜
            tmp = ls[f];
            ls[f] = s;
            s = tmp;
        }
        f = f / 2;
    }
    ls[0] = s; //0指向最终的胜者
}

//创建败者树
void CreateLoserTree(LoserTree ls, External b){
    //初始化ls
    b[K] = INT_MIN; //哨兵
    for(int i = 0; i < K; i ++){
        ls[i] = K;
    }

    for(int i = K - 1; i >= 0; i --){
        Adjust(ls, b, i);
    }
}

void input(FILE *fp[K], int *x, int i){
    int tmp;
    if(!fp[i]){
        char file[100], str[100];
        strcpy(file, "tmpfile/");
        sprintf(str, "%d", i);
        strcat(file, strcat(str, ".txt"));
        fp[i] = fopen(file, "r");
    }
    if(fscanf(fp[i], "%d,", &tmp) != EOF){
        *x = tmp;
        if(tmp == INT_MAX){
            fclose(fp[i]);
        }
    }
}

void K_Merge(FILE *fp_out, LoserTree ls, External b){
    int i;
    FILE *fp_in[K] = {0};
    for(i = 0; i < K; i ++){ //从外部文件读入关键字到缓冲区b
        input(fp_in, &b[i], i);
    }

    CreateLoserTree(ls, b); //初始化败者树

    while(b[ls[0]] != INT_MAX){
        i = ls[0]; //最小值所在文件
        fprintf(fp_out, "%d,", b[i]); //将最小值写入文件
        input(fp_in, &b[i], i); //读入下一个关键字
        Adjust(ls, b, i); //调整败者树,选出最小的关键字
    }

    fprintf(fp_out, "%d,", b[ls[0]]); //将最大关键字写入文件
    fclose(fp_out);
}

全部代码

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

#define K 5 //K路归并
#define LENGTH 2500
#define MAXSIZE 1000

typedef int LoserTree[K]; //败者树是完全二叉树,可以用数组存放

typedef int key;

typedef key External[K + 1]; // 存放待排记录的关键字

typedef struct SqList {
    int data[MAXSIZE];
    int len;
} SqList;

int partition(int *a, int low, int high){
    int pivot = a[low];
    while(low < high){
        while(low < high && a[high] >= pivot) high --;
        a[low] = a[high];
        while(low < high && a[low] < pivot) low ++;
        a[high] = a[low];
    }

    a[low] = pivot;
    return low;
}

void QuickSort(int *a, int low, int high){
    if(low >= high) return;
    int pivot = partition(a, low, high);
    QuickSort(a, low, pivot - 1);
    QuickSort(a, pivot + 1, high);
}

#define LENGTH 2500 //关键字数
void MockData(FILE *fp){
    int i, tag = 1, count = 1, num;
    int data[LENGTH + 1];
    data[0] = LENGTH;
    srand((unsigned)time(NULL)); //播种
    for(i = 1; i <= LENGTH; i ++){
        do{
            tag = 0;
            num = rand() % (4 * LENGTH - 1) + 1; //随机数范围1 ~ 4 * LENGTH - 1
            for(int j = 0; j < count; j ++){
                if(num == data[j]){
                    tag = 1;
                    break;
                }
            }
            if(tag == 0){
                data[count] = num;
                count ++;
            }
        }while(tag == 1);
    }
    for(i = 0; i <= LENGTH; i ++){
        fprintf(fp, "%d,", data[i]);
    }
    fprintf(fp, "%d,", INT_MAX);
    fclose(fp);
}


void CutSort(FILE *fp){
    int i, j, len, tmp;
    FILE *fptr;
    SqList L;
    char rePath[100], str[100];
    fscanf(fp, "%d", &len); //读取关键字长度
    int m = len / K; //假设内存一次最多可载入m个数据
    int n = K + (len % m != 0); //n为归并段数
    system("mkdir tmpfile"); //打开文件
    for(i = 0; i < n; i ++){
        strcpy(rePath, "tmpfile/");
        sprintf(str, "%d", i);
        strcat(rePath, strcat(str, ".txt"));
        fptr = fopen(rePath, "w+");
        if(fptr){
            L.len = 0;
            for(j = 0; j < m; j ++){
                if(fscanf(fp, ",%d", &tmp)){
                    L.data[L.len ++] = tmp;
                }else{
                    break;
                }
            }

            QuickSort(L.data, 0, L.len - 1); //内部排序
            for(j = 0; j < L.len; j ++){ //将数据写入文件
                fprintf(fptr, "%d,", L.data[j]);
            }
            fprintf(fptr, "%d,", INT_MAX); //写入哨兵
            fclose(fptr);
        }
    }
    fclose(fp);
}

void Adjust(LoserTree ls, External b, int s){
    int tmp;
    int f = (s + K) / 2; //指向s的父节点
    while(f > 0){
        if(b[s] > b[ls[f]]){ //s指向胜者, 小为胜
            tmp = ls[f];
            ls[f] = s;
            s = tmp;
        }
        f = f / 2;
    }
    ls[0] = s; //0指向最终的胜者
}

//创建败者树
void CreateLoserTree(LoserTree ls, External b){
    //初始化ls
    b[K] = INT_MIN; //哨兵
    for(int i = 0; i < K; i ++){
        ls[i] = K;
    }

    for(int i = K - 1; i >= 0; i --){
        Adjust(ls, b, i);
    }
}

void input(FILE *fp[K], int *x, int i){
    int tmp;
    if(!fp[i]){
        char file[100], str[100];
        strcpy(file, "tmpfile/");
        sprintf(str, "%d", i);
        strcat(file, strcat(str, ".txt"));
        fp[i] = fopen(file, "r");
    }
    if(fscanf(fp[i], "%d,", &tmp) != EOF){
        *x = tmp;
        if(tmp == INT_MAX){
            fclose(fp[i]);
        }
    }
}

void K_Merge(FILE *fp_out, LoserTree ls, External b){
    int i;
    FILE *fp_in[K] = {0};
    for(i = 0; i < K; i ++){ //从外部文件读入关键字到缓冲区b
        input(fp_in, &b[i], i);
    }

    CreateLoserTree(ls, b); //初始化败者树

    while(b[ls[0]] != INT_MAX){
        i = ls[0]; //最小值所在文件
        fprintf(fp_out, "%d,", b[i]); //将最小值写入文件
        input(fp_in, &b[i], i); //读入下一个关键字
        Adjust(ls, b, i); //调整败者树,选出最小的关键字
    }

    fprintf(fp_out, "%d,", b[ls[0]]); //将最大关键字写入文件
    fclose(fp_out);
}

int main(int argc, const char *argv[]) {

    FILE *fp;
    LoserTree ls;
    External b;

    printf("▼6\n▲函数 MockData 测试...\n");						//6.函数RandomNum测试
    {
        printf("创建随机数表作为示例文件...\n");
        fp = fopen("TestData.txt", "w+");							//将随机数写入文件
        MockData(fp);
        printf("\n");
    }

    printf("▼7\n▲函数 CutSort 测试...\n");						//7.函数Cut_Sort测试
    {
        printf("分割随机数表,并分别排序后存入[0..k-1].txt...\n");
        fp = fopen("TestData.txt", "r");							//读取随机数文件
        CutSort(fp);												//分割fp为k个文件,并分别排序
        printf("\n");
    }

    printf("▼1、2、3、4、5\n▲函数 K_Merge等 测试...\n");			//1、2、3、4、5.函数K_Merge等测试
    {
        printf("将k个归并段[0..k-1].txt排序后写入文件Order.txt");
        fp = fopen("Order.txt", "w+");								//向输出归并段中写入排序后的关键字
        K_Merge(fp, ls, b);
        printf("\n\n");
    }

    return 0;
}

置换-选择排序

为了进一步减少比较次数,在记录数n不变的情况下,只能减少r,置换-选择排序算法能够减少数r,但是每段记录数不在均匀。 置换选择排序操作过程

  1. 从FI中输出w个记录到工作区WA
  2. 从WA中选出关键字最小的记录,记为miniMax
  3. 将miniMax记录到FO中去
  4. 若FI不为空,则输出FI中的下一个记录到WA
  5. 从WA中所有关键字比miniMax大的元素中选出最小关键字记录,作为新的MINIMAX记录
  6. 重复3 ~ 5,知道FI中选不出新的miniMAX记录为止,由此得到一个初始归并段,输入一个初始归并段的结束标记到FO中
  7. 重复2 ~ 6,知道WA为空,由此得到全部归并段。

代码

#define W 6 //工作区大小
typedef int LoserTree[W];
typedef struct {
	int key; //关键字
	int rnum; //段号
}KeyType;
typedef KeyType WorkArea[W + 1];

int rc, rmax;

void SelectMiniMax(LoserTree ls, WorkArea w, int s){
	int tmp, p; //s指向新的胜者
    for(t = (i + W) / 2, p = ls[t]; t > 0; t /= 2, p = ls[t]){ 
    	if(w[p].rnum < w[s].rnum || (w[p].rnum == w[s].rnum && w[p].key < w[s].key){
        	tmp = p;
            ls[t] = s;
            s = tmp;
        }	
    }
    ls[0] = s; 
}

void BuildLoserTree(FILE *fi, LoserTree ls, WorkArea w)
	int i, tmp;
    w[W].rnum = 0;
	w[W].key = INT_MIN; 
        
    for(i = 0; i < W; i ++){
    	w[i].rnum = 0;
        ls[i] = W;
        if(fscanf(fi, "%d,", &tmp) != EOF){
        	w[i].key = tmp;
        }else{
        	w[i].key = INT_MAX;
        }
    }
    for(i = W - 1; i >= 0; i--){
    	SelectMiniMax(ls, w, i);
    }
}

void getRun(FILE *fi, FILE *fo, ls, w){
	int tmp, miniMax;
   
	while(w[ls[0]].rnum == rc){
    	miniMax = w[ls[0]].key;
        if(miniMax == INT_MAX) return;
        fprintf(fo, "%d,", miniMax);
        if(fscanf(fi, "%d,", &tmp) != EOF){
        	w[s].key = tmp;
        	if(tmp < miniMax){ //tmp属于下一个段
            	rmax = rc + 1;
            	w[s].rnum = rmax;
            }else{ //tmp属于当前段
            	w[s].rnum = rc;
            }
        }else{
        	w[s].key = INT_MAX; 
            w[s].rnum = rmax + 1;
        }
        SelectMiniMax(ls, w, s);
    }
}

void ReplaceSelect(FILE *fi){
	LoserTree ls;
    WorkArea w;
    FILE *fo;
    char file[100], str[100];
    BuildLoserTree(fi, ls, w);
    rc = rmax = 0;
    system("mkdir tmpfile");
    while(rc <= rmax){
    	strcpy(file, "tmpfile/");
        sprintf(str, "%d", rc);
        strcat(file, strcat(str, ".txt"));
        fo = fopen(file, "W+");
        getRun(fi, fo, ls, w); //写入一个归并段
        fprintf(fi, "%d", INT_MAX);//写入结束标记
        fclose(fo);
        rc = w[ls[0]].rnum;
    }
}

int main(int argc, const char * argv[]) {
    FILE *fi = fopen("test/File", "r");
    Replace_Select(fi);
    return 0;
}

最佳归并树

经过置换-选择排序后,每段小文件的记录数不再均匀,简单的多路归并不能达到最优。如果把每段小文件看作叶结点,叶结点记录数看作权值,便可将归并树看作哈夫曼树,求解最小的WPL就得到了最优的归并策略。