C语言版的磁盘文件分片归并排序函数(转)

介绍了一种用于磁盘文件排序的老式C函数,适用于大文件的分片归并排序,包括算法原理及其实现代码。展示了如何在有限内存条件下对磁盘文件进行高效排序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      这是一个很老的的C函数,用来实现大的磁盘文件排序。在以前DOS操作系统下,对磁盘文件的排序一般有3种方法:1、将磁盘文件装入内存排序,将排序结果保存到新的文件,这适用于很小的(64K以内)、不需要经常索引的文件;2、对磁盘文件按关键字进行分块排序后,形成一个索引文件。块的大小一般为512K,常采用B+树或者B-数算法,这种方法适用于需要经常索引的磁盘文件,如DBF文件;3、把磁盘文件分片排序后,形成很多排序片文件,然后将这些排序片文件合并起来,输出为一个排序文件,这种方法适用于很大的、但又不需要经常索引的磁盘文件。

        可见,在DOS有限的内存条件下,磁盘文件分片归并排序是使用比较广泛的一种外存储器排序算法。现在计算机的物理内存一般足够大(最小的也有256MB吧),Windows的虚拟内存更是多达4个GB(对每一个应用程序而言),这对于很多磁盘文件的内存排序应该是足够了,况且现在的记录文件都放在各种数据库中,所以磁盘文件分片归并排序算法可能没有市场了(不过内存多路归并排序还是有市场的)。作为怀旧,把代码贴在这里,以免“失传”!


/**************************************************************************
*  文  件  名 : MERGE.H                                                   *
*  编  制  人 : 湖北省公安县统计局  毛 泽 发                              *
*  日      期 : 1991.8                                                    *
**************************************************************************/

#define S_IREAD         0x0100
#define S_IWRITE        0x0080

#if defined(__TINY__) || defined(__SMALL__) || defined(__MENIUM__)
#define SSIZE     25600    /* 排序缓冲区字节 */
#define NULL      0
#else
#define SSIZE     65024    /* 排序缓冲区字节 */
#define NULL      0L
#endif
#define MAXMERGE  4        /* 排序合并每趟每次最大片 */
#define MAXMEREC  (SSIZE / (MAXMERGE + 1)) /* 文件最大记录长 */

typedef int cdecl mercmpf(const void *, const void *);

/* 通用排序函数.
     参  数:排序文件名;原文件名;原文件头字节数;文件记录长;用户提供的比较函数.
     返回值:成功 > 0;内存不够.记录超长返回 0;文件操作出错 -1 */
int fmerge(char *foname, char *finame, int ftops, int lrd, mercmpf *cmpf);

/**************************************************************************
*  文  件  名 : MERGE.C                                                   *
*  编  制  人 : 湖北省公安县统计局  毛 泽 发                              *
*  日      期 : 1991.8                                                    *
**************************************************************************/

#include <io.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include "merge.h"

static mercmpf *mercmp = NULL;   /* 比较函数 */

static char *merbuf = NULL;  /* 排序动态缓冲区 */
static char *filetop = NULL; /* 原文件文件头存放动态缓冲区 */
static int filetopchs;       /* 原文件文件头长 */
static int merlrd;           /* 文件记录长 */

static int outfile(char *fname, unsigned size, int flag);
static int formerge(char *foname, char *finame, char *tmp, unsigned m);
static domerge(char *foname, char *tmp1, char *tmp2, int irun);
static void smerge(int *md, int m, char *buf[], int outf, char *outbuf, int size);
static int dopass(char *name1, char *name2, int irun);

/* 通用排序函数.
     参  数:排序文件名;原文件名;原文件头字节数;文件记录长;用户提供的比较函数.
     返回值:成功 > 0;内存不够.记录超长返回 0;文件操作出错 -1 */
int fmerge(char *foname, char *finame, int ftops, int lrd, mercmpf *cmpf)
{
  char tmp1[68], tmp2[68];
  int irun;
  unsigned size;
  if(lrd > MAXMEREC) return 0;   /* 记录超长 */
  merlrd = lrd;
  size = (SSIZE / lrd) * lrd;    /* 排序缓冲区实际长 */
  if((merbuf = (char *)malloc(size)) == NULL) return 0; /* 分配动态缓冲区 */
  if(ftops && (filetop = (char *)malloc(ftops)) == NULL) return 0;
  filetopchs = ftops;
  mercmp = cmpf;
  strcpy(tmp1, "&&&1");   /* 临时文件名 */
  strcpy(tmp2, "&&&2");
  irun = formerge(foname, finame, tmp1, size); /* 分片排序 */
  if(irun > 1)                                 /* 如果排序片大于 1 */
    irun = domerge(foname, tmp1, tmp2, irun);  /* 合并排序片 */
  free(merbuf);
  if(filetopchs) free(filetop);
  return irun;
}
/* 写一排序片文件 */
static int outfile(char *fname, unsigned size, int flag)
{
  int h, c;
  if((h = open(fname, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IWRITE)) == -1)
    return -1;
  if(flag && filetopchs) /* 如果是最终文件同时原文件有文件头 */
    write(h, filetop, filetopchs); /* 写入文件头内容 */
  c = write(h, merbuf, size);      /* 写排序片到文件 */
  close(h);
  return c;
}
/* 分片排序 */
static int formerge(char *foname, char *finame, char *tmp, unsigned m)
{
  unsigned irun, ret;
  int f, flag = 0;
  char tmpname[68];
  if((f = open(finame, O_RDONLY | O_BINARY)) == -1) return -1;/* 打开原文件 */
  if(filetopchs)   /* 如有文件头,保存其内容到缓冲区 */
    read(f, filetop, filetopchs);
  irun = 0;
  do{
    ret = read(f, merbuf, m);  /* 读一排序片到排序缓冲区 */
    if(ret == 0 || ret == 0xffff) break; /* 原文件结束或出错,退出 */
    qsort(merbuf, ret / merlrd, merlrd, mercmp); /* 排序 */
    if(ret == m || irun > 0)   /* 如原文件长大于或等于一排序片长 */
      sprintf(tmpname, "%s.%03d", tmp, irun); /* 采用临时文件名 */
    else{                      /* 否则,直接用排序文件名 */
      strcpy(tmpname, foname);
      flag = 1;                /* 最终文件标记 */
    }
    ret = outfile(tmpname, ret, flag); /* 写排序片 */
    irun ++;
  }while(ret == m);
  close(f);
  if(ret == 0xffff) return ret;  /* 出错返回 -1 */
  return irun;                   /* 返回排序片数 */
}
/* 分配每一合并趟不同临时文件名;控制合并趟数 */
static domerge(char *foname, char *tmp1, char *tmp2, int irun)
{
  char *p;
  while(irun > 1){
    if(irun <= MAXMERGE) strcpy(tmp2, foname);
    irun = dopass(tmp1, tmp2, irun);
    p = tmp1;
    tmp1 = tmp2;
    tmp2 = p;
  }
  return irun;
}
/* 执行合并趟,计算.分配每次合并所需文件数,缓冲区大小,控制每次合并的执行 */
static int dopass(char *name1, char *name2, int irun)
{
  int fi, i, nrun, m, size;
  char oname[68], inname[68], *p[MAXMERGE], *q;
  int md[MAXMERGE], fo;
  size = SSIZE / merlrd;  /* 合并缓冲区容纳记录数 */
  nrun = 0;
  for(fi = 0; fi < irun; fi += MAXMERGE){
    m = irun - fi;    /* 每次合并实际排序片数 */
    if(m > MAXMERGE) m = MAXMERGE;
    for(i = 0; i < m; i ++) p[i] = merbuf + (i * merlrd); /* 分配读缓冲区 */
    if(irun <= MAXMERGE) strcpy(oname, name2); /* 最终合并形成排序文件 */
    else sprintf(oname, "%s.%03d", name2, nrun);/* 中间合并采用临时文件 */
    if((fo = open(oname, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IWRITE)) == -1)
      break;  /* 打开写文件 */
    i = 0;
    do{ /* 分别打开读文件 */
      sprintf(inname, "%s.%03d", name1, fi + i);
      md[i] = open(inname, O_RDONLY | O_BINARY);
    }while(md[i ++] != -1 && i < m);
    if(i != m){
      close(fo);
      for(fi = 0; fi < i; fi ++) close(md[fi]);
      break;
    }
    if(irun <= MAXMERGE && filetopchs) /* 最终合并写文件头(如有) */
      write(fo, filetop, filetopchs);
    q = merbuf + (m * merlrd);        /* 分配写缓冲区 */
    smerge(md, m, p, fo, q, size - m); /* 合并 */
    for(i = 0; i < m; i ++){  /* 删除各排序片文件 */
      close(md[i]);
      sprintf(inname, "%s.%03d", name1, fi + i);
      unlink(inname);
    }
    close(fo);
    nrun ++;
  }
  if(nrun != (irun + MAXMERGE - 1) / MAXMERGE) return -1;
  return nrun;
}
/* 执行实际排序片合并 */
static void smerge(int *md, int m, char *buf[], int outf, char *outbuf, int size)
{
  int i, j, n = merlrd, w = merlrd * size;
  char *s = buf[0], *p, *q = outbuf, *end = q + w;
  for(i = 0; i < m; i ++)   /* 从各片文件中读第一条记录 */
    read(md[i], buf[i], n);
  while(1){
    if(n == merlrd){        /* 如各片文件均有记录,各片记录反向插入排序 */
      for(i = 1; i < m; i ++){
    for(p = buf[i], j = i - 1; j >= 0 && mercmp(p, buf[j]) > 0; j --)
      buf[j + 1] = buf[j];
    buf[j + 1] = p;
      }
    }
    else m --;  /* 一片文件内容结束 */
    if(!m){     /* 如所有片文件结束,写缓冲区残余记录,退出 */
      if(q != outbuf) write(outf, outbuf, q - outbuf);
      break;
    }
    if(q == end){  /* 刷新一次写缓冲区到文件 */
      if(write(outf, outbuf, end - outbuf) != w) break;
      q = outbuf;
    }
    i = m - 1;
    j = (buf[i] - s) / merlrd;
    memmove(q, buf[i], merlrd); /* 将各片记录中值最小(大)者移入写缓冲区 */
    q += merlrd;
    n = read(md[j], buf[i], merlrd); /* 从该片中读下一记录,继续 */
  }
}

        可以看到,上面2个文件时间是1991年的,真是老古董了,如MERGE.H文件开头就没有什么诸如#ifndef        __MERGE_H......的代码,我记得那个时候好像没这个写法的。函数里面当初也作了很详细的注释,所以算法就不再讲了(要讲我还得先分析代码,早忘记了 ^_^ )。

        为了示范该函数的使用方法,我还是用BCB6写了一个简单的演示程序,如果你想试一下老古董,不妨也写一个?可以将MERGE.H文件中的排序缓冲区加大一些,可提高排序速度。

//---------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include "merge.h"

#pragma hdrstop

#define TOPSTRING       "湖北省公安县统计局  毛 泽 发"
#define TOP_SIZE        30
#define RECORD_SIZE     53
#define RECORD_COUNT    10000

//---------------------------------------------------------------------------
/* 为了方便观察,随机生成了一个RECORD_COUNT行的文本文件 */
void MakeFile(char *filename)
{
    int i, j;
    long v[4];
    FILE *f;
    f = fopen(filename, "w");
    fprintf(f, "%s ", TOPSTRING);
    randomize();
    for (i = 0; i < RECORD_COUNT; i ++)
    {
        for (j = 0; j < 4; j ++)
            v[j] = random(0x7fffffff);
        fprintf(f, "%12ld %12ld %12ld %12ld ", v[0], v[1], v[2], v[3]);
    }
    fclose(f);
}


int cdecl CompRecord(const void *ra, const void *rb)
{
    int a[4], b[4];
    int i, n;
    sscanf((char*)ra, "%ld%ld%ld%ld", &a[0], &a[1], &a[2], &a[3]);
    sscanf((char*)rb, "%ld%ld%ld%ld", &b[0], &b[1], &b[2], &b[3]);
    for (n = 0, i = 0; i < 4 && n == 0; i ++)
        n = a[i] - b[i];
    return n;
}

#pragma argsused
int main(int argc, char* argv[])
{
    printf("正在随机制造一个文本文件d:/test.txt... ");
    MakeFile("d:/test.txt");
    printf("正在进行磁盘文件排序,排序文件d:/sort.text... ");
    fmerge("d:/sort.txt", "d:/test.txt", TOP_SIZE, RECORD_SIZE, CompRecord);
    printf("磁盘文件排序完毕! ");
    system("pause");
    return 0;
}
//---------------------------------------------------------------------------
       如有错误,或者你有什么好的建议请来信:maozefa@hotmail.com

        发现代码贴上去总是走样,文件路径‘//’也成了‘/’,‘/n’也没了,MakeFile的2句写记录语句应该分别是,不然,测试会出问题:

fprintf(f, "%s/n", TOPSTRING);

fprintf(f, "%12ld %12ld %12ld %12ld/n", v[0], v[1], v[2], v[3]);

 

本文来自优快云博客,转载请标明出处:http://blog.youkuaiyun.com/maozefa/archive/2008/01/03/2023395.aspx

以下是将提供的内容换为Markdown格式的文件: ```markdown # Hadoop-MapReduce 文档 ## 1. MapReduce 设计思想 ### 分而治之 - **对付大数据并行处理**:将大的数据切分成多个小数据,交给更多节点参与运算。 - 注意:不可拆分的计算任务或相互间有依赖关系的数据无法进行并行计算。 ### 抽象模型 - Input:读取数据。 - Split:对数据进行粗粒度切分。 - Map:对数据进行细粒度切分。 - Shuffle:洗牌,将各个 `MapTask` 结果合并输出到 `Reduce`。 - Reduce:对 `Shuffle` 进行汇总并输出到指定存储。 - Output:如 HDFS、Hive、Spark、Flume 等。 ### 统一架构 - 提供统一的计算框架,隐藏大多数系统层面处理细节,减轻程序员开发负担。 ### 特征 - **离线框架**:适合PB级别海量数据的离线处理。 - **不擅长实时计算**:不适合毫秒或秒级返回结果。 - **不擅长流式计算**:输入数据是动态的,而 MapReduce 输入数据集是静态的。 - **不擅长 DAG(有向图)计算**:多应用程序存在依赖关系时,性能低下。 ### 计算向数据靠拢 - 在数据节点上进行工作。 ### 顺序处理数据 - 规避随机访问数据,提升性能。 ### 失效被认为是常态 - 使用大量低端服务器,节点失效频繁。设计良好的并行计算系统应具备健壮性和容错性。 --- ## 2. 常用排序算法 ### 分类 #### 不值钱 - 学过计算机的基本都会的算法。 - 包括:冒泡排序、选择排序、插入排序。 #### 进阶型 - 更高效的算法。 - 包括:希尔排序(高级插入)、堆排序(高级选择)。 #### 常用型 - 实际应用广泛的算法。 - 包括:快速排序、归并排序。 #### 偏方型 - 在特定场景下有奇效。 - 包括:计数排序、桶排序、基数排序。 ### 快速排序 - **从冒泡排序演变而来**,实际上是递归分治法。 - **实现方式**:Hoare法(左右指针法)、前后指针法、挖坑法。 #### 左右指针法(Hoare法) - 第一个元素为 `Key`,`Begin` 和 `End` 分别指向第一个和最后一个元素。 - 操作直至 `Begin >= End`,交换 `Key` 和 `End` 的值,重复操作左右两边。 #### 挖坑法 - 挑选基准元素,所有小于基准值的元素置于左侧,大于的置于右侧。 - 示例:`arr = [5, 2, 4, 6, 1, 7, 8, 3]`,逐步调整,最终基准位于正确位置,再递归处理两侧子序列。 #### 前后指针法 - 初始设置 `Cur` 和 `Prev` 标志指针。 - `Cur` 前进,遇到小于基准值时交换 `Prev` 和 `Cur` 的值,保证 `Cur` 与 `Prev` 间元素均大于基准值。 #### 优化选 Key - 最佳情况:每次选取中间值。 - 引入“三数取中”,取首中尾三个元素的中间值作为基准,解决有序数组排序效率低下的问题。 ### 归并排序 - **基于归并操作**的有效排序算法。 - **基本思想**:将原序列不断拆分至单个元素,再按插入方式归并。 - 时间复杂度始终为 `O(n log n)`,但需要额外内存空间。 --- ## 3. MapReduce 计算流程 ### 计算1T数据中每个单词出现次数 -> wordcount #### 原始数据File - 数据被切分成块存放在 HDFS 上,每块 128M。 #### 数据块Block - 数据存储单元,同一文件中块大小相同。 - 可能块数量与集群计算能力不匹配,需动态调整参与计算的节点数量。 #### 切片Split - 控制参与计算的节点数目,切片大小为块的整数倍(2 或 1/2)。 - 默认情况下,切片大小等于块大小(默认 128M),一个切片对应一个 `MapTask`。 #### MapTask - 默认从所属切片读取数据,每次读取一行到内存。 - 计算每个单词出现次数,产生 `(Map<String, Integer>)` 临时数据,存放在内存中。 - 为了避免 `OOM` 和低效率,数据会周期性写入硬盘。 #### 环形数据缓冲区 - 循环利用内存区域,减少数据溢写时 `Map` 的停止时间。 - 每个 `Map` 单独占用一块内存区域,默认大小为 100M。 - 当缓冲区达到 80% 开始溢写,留出 20% 空间,确保效率不减缓。 #### 分区 Partation - 根据 `Key` 计算对应的 `Reduce`。 - 分区数量与 `Reduce` 数量相等,默认分区算法为哈希取余。 #### 排序 Sort - 对溢写数据进行排序(`QuickSort`),按 `Partation` 和 `Key` 排序,确保相同分区和 `Key` 在一起。 #### 溢写 Spill - 将内存数据写入硬盘,避免 `OOM` 问题。 - 每次产生一个 80M 文件,数据较多时可能多次溢写。 #### 合并 Merge - 将溢写产生的多个小文件合并成一个大文件,便于后续拉取数据。 - 合并过程中仍进行排序(归并排序),生成有序大文件。 #### 组合器 Combiner - 减少 `Map` 和 `Reduce` 间的数据传输。 - 自定义 `combiner` 函数,类似 `reduce` 函数,确保局部汇总结果不影响最终结果。 #### 拉取 Fetch - 将 `Map` 的临时结果拉取到 `Reduce` 节点。 - 确保相同 `Key` 拉取到同一个 `Reduce` 节点。 #### 归并 Reduce - 读取文件数据到内存,将相同 `Key` 全部读取并得出最终结果。 #### 写出 Output - 每个 `Reduce` 将最终结果存放到 HDFS 上。 --- ## 4. Hadoop-YARN 架构 ### 基本概念 - **Yarn**:另一种资源协调者,用于统一管理资源。 - **ResourceManager**:资源协调框架的管理者,分为主节点和备用节点。 - **NodeManager**:资源协调框架的执行者,每个 `DataNode` 上默认有一个 `NodeManager`。 - **ApplicationMaster**:负责调度被分配的资源 `Container`,任务完成后通知 `ResourceManager` 销毁资源。 - **Task**:开始按照 `MR` 流程执行业务。 ### 工作流程 - 根据 `mapreduce.framework.name` 变量配置选择运行时框架。 - `Client` 发起提交作业申请,`ResouceManager` 返回 `JobID` 并保存数据资源。 - `Client` 计算分片并将资源拷贝到 HDFS,最后提交 `Job` 给 `ResouceManager`。 - `ApplicationManager` 接收提交的 `Job` 并交由 `ResourceScheduler` 处理。 - `ApplicationMaster` 注册并与 `ResourceManager` 交互,申请资源并调度任务。 --- ## 5. Hadoop-YARN 环境搭建 ### 目标环境 - 基于 HA 环境搭建。 ### 修改配置文件 - 修改 `hadoop-env.sh`、`mapred-site.xml` 和 `yarn-site.xml` 文件。 - 添加必要的环境变量和配置项。 ### 拷贝至其他节点 - 将配置好的 YARN 拷贝至其他节点。 ### 启动 - 启动 ZooKeeper、HDFS 和 YARN。 - 启动 JobHistory。 ### 关闭 - 关闭 Hadoop 和 JobHistory,再关闭 ZooKeeper。 --- ## 6. MapReduce 案例 - WordCount ### Java代码实现 - 主要涉及 `WordCountJob` 类、`WordCountMapper` 类和 `WordCountReducer` 类。 - 设置 `Map` 输出的 `Key` 和 `Value` 类型,配置 `Map` 和 `Reduce` 的处理类。 ### 提交方式 - Linux端:`hadoop jar wordcount.jar com.yjxxt.mapred.wordcount.WordCountJob` ### YARN常用命令 - 查看节点:`yarn node-list-all` - 查看所有 Application:`yarn application-list` - 杀死 Application:`yarn application-kill <application_id>` - 查看 Application 日志:`yarn logs -applicationId <application_id>` --- ## 7. MapReduce 案例 - 充值记录 ### Mock数据 - 自动生成一万条充值记录,模拟用户行为。 - 按照性别分类统计男女性用户的充值总额。 --- ## 8. MapReduce 案例 - 天气信息 ### 需求 - 每个地区的每日最高温及最低温。 - 每月温度最高的三天。 --- ## 9. MapReduce 案例 - 好友推荐 ### 需求 - 根据互为好友的关系,进行好友推荐。 --- ## 10. MapReduce 压缩 ### 概述 - 压缩技术能减少存储读写字节数,提高网络带宽和磁盘空间效率。 - 压缩在大数据环境中尤为重要,特别是在 I/O 密集型作业中。 ### 压缩实践 - 支持的压缩格式及其特点见表格。 - 压缩应用场景的选择依据。 --- ## 11. MapReduce 源码分析 ### Split - 创建切片文件,设置切片数量。 ### MapTask - 初始化 `MapTask`,读取输入并执行 `Mapper` 方法。 ### KvBuffer - 初始化环形缓冲区,设置溢写阈值。 ### Spill - 将缓冲区数据溢写到磁盘。 ### Merge - 合并溢写文件,减少磁盘 I/O 次数。 ### ReduceTask - 获取 `Key` 和 `Value` 的迭代器,执行 `Reducer` 方法。 --- ## 12. MapReduce 优化 ### 概述 - 明确 Hadoop 的优势和适用场景,了解 MapReduce 执行流程。 ### 小文件优化 - 使用 `CombineFileInputFormat` 读取数据时合并小文件。 ### 数据倾斜 - 解决方法包括自定义分区、增加 Reduce 的资源或调整 Reduce 数量。 ### 推测执行 - 开启推测执行,空间换时间,提高作业性能。 ### MapReduce 执行流程优化 - **Map** - **临时文件**:合理设置文件副本数量。 - **分片**:调整分片大小以动态设置 Map 数量。 - **资源**:根据业务需求调整 Map 的资源。 - **环形缓冲区 & 溢写**:调整缓冲区大小和溢写百分比。 - **合并**:增加合并因子,减少合并次数。 - **输出**:使用组合器和压缩减少数据传输。 - **Reduce** - **资源**:合理设置 Reduce 数量和资源。 - **拉取**:调整拉取并行度和超时时间。 - **缓冲区 & 溢写**:基于 JVM Heap Size 设置缓冲区大小。 - **合并**:增加合并因子,减少磁盘操作。 - **读缓存**:启用读缓存以减少磁盘 I/O。 --- ``` 此 Markdown 文件包含了原文档的主要内容,并遵循 Markdown 语法进行了适当的格式化,使其更易于阅读和理解。希望这对您有所帮助!如果您有任何进一步的需求,请告知我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值