多路平衡归并
多路平衡归并的思想是将大文件化成小文件,小文件要小到能加载到内存中进行内部排序。对各个小文件内部排序写入外存后,再将小文件归并排序。假设初始归并段数为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,但是每段记录数不在均匀。 置换选择排序操作过程
- 从FI中输出w个记录到工作区WA
- 从WA中选出关键字最小的记录,记为miniMax
- 将miniMax记录到FO中去
- 若FI不为空,则输出FI中的下一个记录到WA
- 从WA中所有关键字比miniMax大的元素中选出最小关键字记录,作为新的MINIMAX记录
- 重复3 ~ 5,知道FI中选不出新的miniMAX记录为止,由此得到一个初始归并段,输入一个初始归并段的结束标记到FO中
- 重复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就得到了最优的归并策略。