外排序(最小输者树实现)-程序员宅基地

技术标签: 算法  数据结构  排序算法  

问题描述

应用竞赛树结构模拟实现外排序。

基本要求

(1) 设计实现最小输者树结构ADT,ADT中应包括初始化、返回赢者,重构等基本操作。
(2) 设计实现外排序,外部排序中的生成最初归并串以及K路归并都应用最小输者树结构实现;
(3) 随机创建一个较长的文件;设计归并路数以及缓冲区的大小;获得外排序的访问磁盘的次数并进行分析。可采用小文件来模拟磁盘块。

解题

输者树介绍

对于输者树的构建过程,数据结构课本上并没有多说,仅仅讲解了赢者树,如下图所示,赢者上升进入下一次比赛。
最小赢者树
哦,原来这就是赢者树,那么这个最小赢者树是不是就是最大输者树了呢?

想多了,课本上有一个最小输者树的图如下图所示:
最小输者树
说实话,第一次见到这个图我是一脸懵逼的(可能我当时上课讲的忘了),中间结点加上上面的输者结点,包含 a ∼ f a\sim f af的所有结点,根本就不是我开始想象的输者树就是每个结点记录当前的输者。

后来在网上看了一些博客才懂什么意思,我们下面看一下课本上这个图是怎么建立起来的:
在这里插入图片描述
这棵树上,边的值才是比赛晋级的选手,而不是赢者树中结点的才是晋级。

我们按照先从右孩子开始的顺序来构建这棵最小输者树。

  1. a与b比较,b输,a晋级
  2. c与d比较,d输,c晋级
  3. e与f比较,e输,f晋级
  4. g与h比较,h输,g晋级
  5. a与c比较,c输,a晋级
  6. f与g比较,g输,f晋级
  7. a与f比较,a输,f晋级

所以说,每个结点都保存了当前的输者没有错,只是图上没有表现出来晋级的选手而已。每次比赛的选手都是晋级的选手(我就说嘛,什么比赛最后要输的一方)。

外排序

这个外排序的思路是先利用内排序构造顺串,然后这些顺串在进行k路归并,合并成最终结果。

我们在这里使用最小输者树来构造顺串,这样可以减少初始顺串的个数。

在利用最小输者树构造顺串的时候,每个选手都对应着输入集合中的一个元素。另外,每一个选手还有一个顺串号。

赢者的规则是:

  1. 具有较小顺串号的元素获胜。
  2. 顺串号相同,较小元素值的元素获胜。

那这些元素的顺串号又怎么确定呢?伪代码如下:

建立前p个选手的最小赢者树(p是内存的大小)
while(还有元素没有输出到对应的顺串中)
{
    
	将最终赢者w移入它的顺串号所对应的顺串中;
	if(输入集合中又下一个元素)
		n=下一个元素;
	else {
    
		n=INT_MIN;
		n的顺串号=INT_MAX;//这样就不会输入顺串中
	}
	if(n的元素值>=w的值) n的顺串号=w的顺串号;
	else n的顺串号=w的顺串号+1;
	n代替w的位置,重构赢者树;
}

建立了这些顺串后,再利用一次最小输者树就可以将他们归并,并且存到对应的磁盘文件中。

完整代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <string>
#include <climits>
#include <algorithm>
#include <queue>
#include <vector>
#include <fstream>
#include <ctime>
#include <random>
using namespace std;


struct Variable//变量结构体
{
    
    Variable(){
    
        visitDiskTime=0;
        n=0; m=0;
        numberOfDisk=0;
    }
    string path;//磁盘的位置,可以根据自己的电脑修改
    string fileName="Disk.txt";//磁盘文件名称
    string file;//最终完整路径
    int visitDiskTime,n,m,numberOfDisk;
    char ch1,ch;
};
struct SequentialStringPlayer//用于构建顺串
{
    
    int id,key;
    bool operator<=(SequentialStringPlayer &p){
    
        return (id!=p.id) ? (id<p.id) : (key<p.key);
    }
};
template <class T>
class loserTree
{
    
public:
    virtual ~loserTree(){
    }
    virtual void initialize(T* thePlayer,int number)=0;
    virtual int getTheWinner() const =0;
    virtual void rePlay(int thePLayer,T newvalue)=0;
};
template <class T>
class MinimumLoserTree : public loserTree<T>
{
    
public:
    MinimumLoserTree(T* thePlayer=nullptr,int theNumberOfPlayers=0){
    
        tree=nullptr; advance=nullptr;
        initialize(thePlayer,theNumberOfPlayers);
    }
    ~MinimumLoserTree(){
    delete[] tree;delete[] advance;}
    void initialize(T* thePlayer,int theNumberOfPlayers);//初始化
    int getTheWinner() const {
    return tree[0];};//输出当前的赢者
    void rePlay(int thePlayer,T newvalue);//重构
private:
    int numberOfPlayers;
    int* tree;//记录内部结点,tree[0]是最终的赢者下标,不使用二叉树结点,因为父子关系都是通过计算得出
    int* advance;//记录比赛晋级的成员
    T* player;//参与比赛的元素
    int lowExt;//最底层外部结点的个数,2*(n-s)
    int offset;//2*s-1
    void play(int,int,int);
    int winner(int x,int y){
    return player[x]<=player[y]?x:y;};//返回更小的元素下标
    int loser(int x,int y){
    return player[x]<=player[y]?y:x;};//返回更大的元素下标
};
template <class T>
void MinimumLoserTree<T>::initialize(T* thePlayer,int theNumberOfPlayers)
{
    
    int n=theNumberOfPlayers;
    if(n<2) {
    
        cout<<"Error! the number must >= 2"<<endl;
        return;
    }
    player=thePlayer;
    numberOfPlayers=n;
    delete[] tree; delete[] advance;
    tree=new int[n+1];
    advance=new int[n+1];

    int s; for (s=1; 2*s<=n-1; s+=s);//s=2^log(n-1)-1(常数优化速度更快),s是最底层最左端的内部结点

    lowExt=2*(n-s); offset=2*s-1;

    for (int i=2; i<=lowExt; i+=2)//最下面一层开始比赛
        play((i+offset)/2,i-1,i);//父结点计算公式第一条

    int temp=0;
    if(n%2==1){
    //如果有奇数个结点,处理下面晋级的一个和这个单独的结点
        play(n/2,advance[n-1],lowExt+1);
        temp=lowExt+3;
    }
    else temp=lowExt+2;//偶数个结点,直接处理次下层

    for (int i=temp; i<=n; i+=2)//经过这个循环,所有的外部结点都处理完毕
        play((i-lowExt+n-1)/2,i-1,i);

    tree[0]=advance[1];//tree[0]是最终的赢者,也就是决赛的晋级者

}
template <class T>
void MinimumLoserTree<T>::play(int p, int leftChild, int rightChild)
{
    
    //tree结点存储相对较大的值,也就是这场比赛的输者
    tree[p]=loser(leftChild,rightChild);
    //advance结点存储相对较小的值,也就是这场比赛的晋级者
    advance[p]=winner(leftChild,rightChild);

    while(p % 2 == 1 && p > 1){
    
        tree[p/2]=loser(advance[p-1],advance[p]);
        advance[p/2]=winner(advance[p-1],advance[p]);
        p /= 2;//向上搜索
    }
}
template <class T>
void MinimumLoserTree<T>::rePlay(int thePlayer,T newvalue)
{
    
    int n=numberOfPlayers;
    if(thePlayer <= 0 || thePlayer > n){
    
        cout<<"Error! the number must >0 & <=n"<<endl;
        return;
    }

    player[thePlayer]=newvalue;

    int matchNode,//将要比赛的场次
        leftChild,//比赛结点的左儿子
        rightChild;//比赛结点的右儿子

    if(thePlayer<=lowExt){
    //如果要比赛的结点在最下层
        matchNode=(offset+thePlayer)/2;
        leftChild=2*matchNode-offset;
        rightChild=leftChild+1;
    }
    else {
    //要比赛的结点在次下层
        matchNode=(thePlayer-lowExt+n-1)/2;
        if(2*matchNode==n-1){
    //特殊情况,其中一方是晋级后的人
            leftChild=advance[2*matchNode];
            rightChild=thePlayer;
        }
        else{
    
            leftChild=2*matchNode-n+1+lowExt;//这个操作是因为上面matchNode计算中/2取整了
            rightChild=leftChild+1;
        }
    }
    //到目前位置,我们已经确定了要比赛的场次以及比赛的选手

    //下面进行比赛重构,也就是和赢者树最大的不同,分两种情况
    if(thePlayer==tree[0]){
    //当你要重构的点是上一场比赛的赢家的话,过程比赢者树要简化
        for (; matchNode>=1; matchNode/=2){
    
            int oldLoserNode=tree[matchNode];//上一场比赛的输者
            tree[matchNode]=loser(oldLoserNode,thePlayer);
            advance[matchNode]=winner(oldLoserNode,thePlayer);
            thePlayer=advance[matchNode];
        }
    }
    else {
    //其他情况重构和赢者树相同
        tree[matchNode]=loser(leftChild,rightChild);
        advance[matchNode]=winner(leftChild,rightChild);
        if(matchNode==n-1 && n%2==1){
    //特殊情况,比赛结点是最后一层的最后一场比赛
            //特殊在matchNode/2后,左儿子是晋级的选手,右儿子是一场都没有比过赛的选手
            matchNode/=2;
            tree[matchNode]=loser(advance[n-1],lowExt+1);
            advance[matchNode]=winner(advance[n-1],lowExt+1);
        }
        matchNode/=2;
        for (; matchNode>=1; matchNode/=2){
    
            tree[matchNode]=loser(advance[matchNode*2],advance[matchNode*2+1]);
            advance[matchNode]=winner(advance[matchNode*2],advance[matchNode*2+1]);
        }
    }
    tree[0]=advance[1];//最终胜者
}
void init(Variable &va)
{
    
    cout<<"请输入您想要的模拟磁盘位置,接下来的操作都是在当前路径下进行,输入样例:/Users/longzhengtian/Desktop/text/"<<endl;
    cout<<"请输入:"; cin>>va.path; va.file=va.path+va.fileName;
    cout<<"请输入您想要在磁盘中初始化数字的个数,这些数字将用于模拟外排序过程:"; cin>>va.n;
    cout<<"请输入缓冲区大小(此处为缓冲区能存储数字的个数):"; cin>>va.m;
    cout<<"请问您是否想要将原始数据,顺串,最终数据显示在控制台中('y' or 'n'):"; cin>>va.ch1; cout<<endl;

    ofstream fout1(va.file);
    if(!fout1.is_open()){
    
        cout<<"无法打开指定磁盘文件,无法初始化磁盘"<<endl;
        exit(0);
    }
    if(va.ch1=='y') cout<<"原始磁盘文件有:"<<endl;
    for (int i=1; i<=va.n; i++){
    
        int x=random()%1000+1;
        fout1<<x<<' ';//random是C++11标准
        if(va.ch1=='y') cout<<x<<' ';
    }
    if(va.ch1=='y') cout<<endl<<endl;
    fout1.close();
}
void SequentialStringConstruction(Variable &va,SequentialStringPlayer* ssplayer)
{
    
    ifstream fin1(va.file);
    if(!fin1.is_open()){
    
        cout<<"无法打开指定磁盘文件,无法进行顺串构造"<<endl;
        exit(0);
    }
    for (int i=1; i<=va.m; i++){
    //将数据读入缓冲区,进行顺串构造
        fin1>>ssplayer[i].key;
        ssplayer[i].id=1;
        va.visitDiskTime++;//访存次数+1
    }
    MinimumLoserTree<SequentialStringPlayer> sstree(ssplayer,va.m);
    int num=0;
    for (int i=1; i<=va.n; i++){
    //依次从磁盘中取出元素,放入顺串生成树

        if(!(fin1>>num)) num=INT_MIN;
        else va.visitDiskTime++;

        SequentialStringPlayer tempwinner=ssplayer[sstree.getTheWinner()];
        SequentialStringPlayer tempnum; tempnum.key=num;

        if(num >= tempwinner.key) tempnum.id=tempwinner.id;
        else tempnum.id=num=(num==INT_MIN) ? INT_MAX :tempwinner.id+1;

        sstree.rePlay(sstree.getTheWinner(),tempnum);

        string smallfile=va.path+"smallfile"+to_string(tempwinner.id)+".txt";
        va.numberOfDisk=max(va.numberOfDisk,tempwinner.id);
        ofstream fout2(smallfile, ios::app);
        fout2<<tempwinner.key<<' ';

        fout2.close();
        va.visitDiskTime++;
    }
    fin1.close();
    cout<<"顺串分配完毕,生成"<<va.numberOfDisk<<"个顺串,顺串文件见您当前路径下出现的新文件"<<endl;
    if(va.ch1=='y'){
    
        cout<<"我们将这些数据在这里显示如下:"<<endl;
        for (int i=1; i<=va.numberOfDisk; i++)
        {
    
            string smallfile=va.path+"smallfile"+to_string(i)+".txt";
            ifstream finx(smallfile);
            int tempx=0;
            cout<<"smallfile"<<i<<".txt: ";
            while(finx>>tempx)
                cout<<tempx<<' ';
            cout<<endl;
            finx.close();
        }
    }
}
void GenerateTheFinalResult(Variable &va)
{
    
    cout<<endl<<"请问是否将最终排序结果放入原文件,如果否,则程序将新建一个Disk2.txt文件并放入此文件中(输入'y'或'n'代表'是'与'否'):"; cin>>va.ch;
    string newFile;
    if(va.ch=='y') newFile=va.file;
    else newFile=va.path+"Disk2.txt";

    ofstream fout3(newFile);

    if(va.numberOfDisk==1){
    //只生成了一个顺串文件,直接将其加入目标文件
        string smallfile=va.path+"smallfile"+to_string(1)+".txt";
        ifstream fin4(smallfile);
        int tempnumber;
        while(fin4>>tempnumber){
    
            fout3<<tempnumber<<' ';
            va.visitDiskTime+=2;
        }
        fout3.close();
        fin4.close();
        cout<<"由于只生成了1个顺串,直接将此结果放入目标文件中,磁盘访存次数为"<<va.visitDiskTime<<"次,原因是每个数据都读写4次"<<endl;
        if(va.ch1=='y'){
    
            cout<<"最终外排序结果如下:"<<endl;
            ifstream finy(newFile);
            int tempy;
            while(finy>>tempy)
                cout<<tempy<<' ';
            cout<<endl;
            finy.close();
        }
        exit(0);
    }

    int dplayer[va.numberOfDisk+10];//初始化队列
    int pointer[va.numberOfDisk+10];//各个小磁盘文件的指针
    for (int i=1; i<=va.numberOfDisk; i++){
    
        string smallfile=va.path+"smallfile"+to_string(i)+".txt";
        ifstream fin2(smallfile);
        fin2>>dplayer[i];
        pointer[i]=fin2.tellg();
        fin2.close();
    }
    MinimumLoserTree<int> dtree(dplayer,va.numberOfDisk);
    int cnt=0;
    while(cnt<va.n){
    
        cnt++;
        int temp=dtree.getTheWinner();
        int tempwinner=dplayer[temp];
        fout3<<tempwinner<<' ';
        va.visitDiskTime++;
        string smallfile=va.path+"smallfile"+to_string(temp)+".txt";
        ifstream fin3(smallfile);
        fin3.clear(); fin3.seekg(pointer[temp]+1);

        int tempnum;
        if(pointer[temp]+1==0) tempnum=INT_MAX;
        else {
    
            fin3>>tempnum;
            pointer[temp]=fin3.tellg();
            if(pointer[temp]+1==0) tempnum=INT_MAX;
        }
        va.visitDiskTime++;
        dtree.rePlay(temp,tempnum);
        fin3.close();
    }
    fout3.close();
    cout<<endl;
    cout<<"将这些文件进行"<<va.numberOfDisk<<"路归并,已经结果存入到目标文件中。磁盘访存次数为"<<va.visitDiskTime<<"次,原因是每个数据都读写4次"<<endl;
    if(va.ch1=='y'){
    
        cout<<"最终外排序结果如下:"<<endl;
        ifstream finy(newFile);
        int tempy;
        while(finy>>tempy)
            cout<<tempy<<' ';
        cout<<endl;
        finy.close();
    }
}
int main()
{
    
    srand(time(nullptr));
    Variable va;

    init(va);//初始化,生成随机磁盘文件

    SequentialStringPlayer ssplayer[va.n+10];

    SequentialStringConstruction(va,ssplayer);//构造顺串

    GenerateTheFinalResult(va);//生成最终结果

    return 0;
}

代码参考教材源码以及这篇博客

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43347376/article/details/105359191

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法