问题描述
应用竞赛树结构模拟实现外排序。
基本要求
(1)设计并实现最小输者树结构ADT,ADT中应包括初始化、返回赢者,重构等基本操作。
(2)应用最小输者树设计实现外排序,外部排序中的生成最初归并串以及K路归并都应用竞赛树结构实现;
(3)随机创建一个较长的文件作为外排序的初始数据;设置归并路数以及缓冲区的大小;获得外排序的访问磁盘的次数并进行分析。可采用小文件来模拟磁盘块。
数据结构与算法描述
构造输者树的节点包括优先级和数值,重载小于号为先比较优先级大小,优先级相同的再比较数值。
struct Node {//设置Node节点,用来进行初始归并段的生成
int priority;//优先级
int number;
bool operator<(const Node& a)const{
if (priority != a.priority) {//如果优先级不同那么就先比较优先级,如果优先级相同,则比较数值
return priority < a.priority;
}
else {
return number < a.number;
}
}
};
构造输者树,确定输者树大小,选手为外节点,所以要想形成树总的选手数组应该为2倍的外节点个数的大小。将要形成树的数组赋值到选手数组的后半部分,初始化赢者下标数组。然后从最后两个数值开始,两两相比较,小的为赢者赋值到选手数组前半部分,也就是说选手数组最终会形成一个赢者树。较大的数值放到输者树中,然后更新赢者下标数组。最后把最终的赢者存在输者树数组0的位置。
template<class T>
class losertree
{
public:
losertree(T* a, int num);
T winner(){ return loser[0]; }//loser[0]中存储的是最小值
T winner_index() { return winnerindex[1]; }//最小值的下标
void reconfiguration(T element);//重构
private:
int size;//输者树大小
T* loser;//输者树
int* winnerindex;//记录每次的赢者下标
T* player;//选手数组->赢者树
};
template<class T>
losertree<T>::losertree(T* a, int num){//构造输者树
size = num;
player = new T[2 * num];//选手数组
winnerindex = new int[num * 2];
loser = new T[num];
for (int i = size; i < 2 * size; i++){
player[i] = a[i - size];//初始化选手数组
winnerindex[i] = i;//初始化赢者的下标
}
for (int i = size - 1; i > 0; i--){//两两比较
if (player[2 * i] < player[2 * i + 1]){
player[i] = player[2 * i];//交小的赢
loser[i] = player[2 * i + 1];//较大的输
winnerindex[i] = winnerindex[2 * i];
}
else{
player[i] = player[2 * i + 1];
loser[i] = player[2 * i];
winnerindex[i] = winnerindex[2 * i + 1];
}
}
loser[0] = player[winnerindex[1]];//将最终的赢者也就是最小值存到输者树的顶端
}
重构输者树,进行重赛。将原来的最小值处更换成新加的数值,然后从这个节点开始向父亲节点遍历,一次重新比赛,更新输者树数组,赢者下标数组,以及选手数组。
template<class T>
void losertree<T>::reconfiguration(T element){//重构
player[winnerindex[1]] = element;//将原来的最小值替换掉
for (int i = winnerindex[1] / 2; i > 0; i = i / 2) {//从最小值的父节点开始向上比较,直到根节点为止
if (player[2 * i] < player[2 * i + 1]){
player[i] = player[2 * i];//较小的赢
loser[i] = player[2 * i + 1];
winnerindex[i] = winnerindex[2 * i];
}
else{
player[i] = player[2 * i + 1];
loser[i] = player[2 * i];
winnerindex[i] = winnerindex[2 * i + 1];
}
}
loser[0] = player[winnerindex[1]];//更新新的最小值
}
归并排序的实现,再归并排序类中,私有成员要包括输者树大小,归并路数,归并段个数,生成归并段的名称,最终输出的文件。
template<class T>
class Merge_sort{//归并排序
public:
Merge_sort(int _Size, int _NumberOfCombine);
void initialize(string name);//初始归并段的生成
void combine();//归并段的合并
private:
int Size;//输者树的大小
int number_way;//记录归并路数
int number_file;//记录每次产生的归并段个数
string filename, filetype;//产生归并段的名称
string finalname; //最终输出的文件的名称
};
template<class T>
Merge_sort<T>::Merge_sort(int size, int waynumber){
Size = size;
number_way = waynumber;
number_file = 0;
filetype = ".txt";
finalname = "output.txt";
filename = "Merging_segment";//归并段名称
}
初始归并段的生成函数,打开输入数据的文件,进行输入操作,输者数的大小为Size,将文件中前Size个数据,优先级设为1,存入输者树中。然后用while循环直到文件末尾停止,取出文件中的数据,将数据与输者树中的最小值比较,如果大于则把此树的优先级设置的和输者树中的最小值一样,否则优先级为输者树最小值的优先级加一,同时把最小值放到初始归并段的文件中,初始归并段个数为优先级的最大值。然后再用一个循环,将最后输者树中剩余的诗句输到对应的初始归并段中。
template<class T>
void Merge_sort<T>::initialize(string name) {//初始归并段的生成
ifstream inputfile(name, ios::in); //打开名为name的文件,进行输入操作
times++;
Node* data = new Node[Size];//建立一个大小为输者树大小的数组
for (int i = 0; i < Size; i++){
inputfile >> data[i].number;//文件前Size个数输入到数组中
data[i].priority = 1;//初始优先级均为1
}
losertree<Node> loser_tree1(data, Size);//将该数组构造成输者树
int num;
while (!inputfile.eof()){// 到文件末尾则退出循环
inputfile >> num;
Node minwin = loser_tree1.winner();
Node p;
p.number = num;
if (num > minwin.number) {//如果num比最小输者树的最小值大,则优先级相同,反之则优先级+1
p.priority = minwin.priority;
}
else{
p.priority = minwin.priority + 1;//优先级加一,存到新的归并段
number_file = max(number_file, p.priority);//初始归并段的个数,即最大优先级
}
loser_tree1.reconfiguration(p);//将新数据放入输者树中重构
string initialname = filename + "0_" + to_string(minwin.priority) + filetype;
ofstream outfile(initialname, ios::app);//输出文件流,ios::app在文件末位接着输出
times++;//由于打开了initialname文件因此,寻盘次数加一
outfile << minwin.number << ' ';
outfile.close();
}
while (loser_tree1.winner().number != INT_MAX){//将输者树中剩余的数输出
Node minwin = loser_tree1.winner();
Node p;//p的优先级得是最低,并且值是最大,方便将数据利用输者树的赢者函数输出
p.priority = INT_MAX;
p.number = INT_MAX;
loser_tree1.reconfiguration(p);
string initialname = filename + "0_" + to_string(minwin.priority) + filetype;
ofstream outfile(initialname, ios::app);
times++;
outfile << minwin.number << ' ';
outfile.close();
}
}
归并段的合并函数。最大的循环统计归并的轮数h,直到归并段总数为1停止循环。先打开上一轮归并产生的归并段的文件,然后对这些归并段进行归并操作,次数为归并段个数除以归并路数,向上取整。如果归并段数小于等于归并路数,则这是最后一轮归并操作文件名为最终输出文件名,否则是h+1轮的归并的第几个归并段。用一个和归并路数一样大小的数组把这次归并的归并段的每个归并段的最小值存入数组,将数组构造成输者树,取出最小值放入新归并段文件中,然后找到最小值所在的上一轮归并段文件,再从中取下一个最小值,把它放到输者树中重赛,一直到这几个归并段的文件都被读取完,都存到新的归并段。
template<class T>
void Merge_sort<T>::combine(){//归并段的合并
for (int h = 0; number_file != 1; h++){//h为归并轮数
ifstream* prefile = new ifstream[number_file + 1];//存前一次归并得到的归并段
for (int i = 1; i <= number_file; i++){
prefile[i].open(filename + to_string(h) + "_" + to_string(i) + filetype, ios::in);//第h次(上一轮)归并的第i个归并段
times++;
}
int j = 0;
while (j < ceil((double)number_file / (double)number_way)){//将所有的文件进行k路归并,j为对number_file个归并段k路归并,需要归并的次数,也是下一次归并的归并段数
string out;
if (number_file <= number_way){//判断产生的文件是否是最终的归并结果,对输出的文件名进行定义
out = finalname;//如果归并路数大则说明是最后一轮归并操作
}
else{
out = filename + to_string(h + 1) + "_" + to_string(j + 1) + filetype;//这一轮归并得到的归并段文件名
}
ofstream outfile(out, ios::out);//ios::out重新在文件中输出
times++;
int* he = new int[number_way];//存上一轮归并得到的归并段中的最小值
for (int i = 1; i <= number_way; i++){
if (j * number_way + i > number_file ){//判断是否有足够的归并段凑够归并路数个,如果没有用最大值放入。
he[i - 1] = INT_MAX;
}
else {
prefile[j * number_way + i] >> he[i - 1];
}
}
losertree<int> L(he, number_way);
while (L.winner() != INT_MAX){
outfile << L.winner() << ' ';
for (int f = 0; f < number_way; f++){
if (L.winner_index() == f + number_way && j * number_way + f + 1 <= number_file ) {//得到最小输者树的最小值下标是具体的那个文件,此时还要注意有可能存在剩余的文件个数小于归并路数的情况
int u = 0;
prefile[j * number_way + 1 + f] >> u;//将该文件新的最小值取出
if (prefile[j * number_way + 1 + f].eof()) u = INT_MAX;//如果到文件末位则将最大值输入到输者树中
L.reconfiguration(u);//对输者树重构
break;
}
}
}
outfile.close();
j++;
}
for (int i = 1; i <= number_file; i++) prefile[i].close();
number_file = j;//更新归并段数
}
}
主函数中要输入生成的随机数个数,最大值,输者树大小、归并路数。输出访问磁盘次数( 用一个全局变量,在每一打开文件的操作后加一)。
int generateUniformRandomInt(int min, int max) {//约束随机数的大小
return min + rand() % (max - min + 1);
}
int main(){
cout << "请输入要生成的随机数个数:";
int n, max;
cin >> n;
cout << "请输入随机数的最大值:";
cin >> max;
ofstream inputfile("input.txt");
for (int i = 0; i < n; ++i) {
int randomNum = generateUniformRandomInt(0, max);
inputfile << randomNum << " ";
}
inputfile.close();
cout << "请输入输者树的大小:";
int Size;
cin >> Size;
cout << "请输入要实现归并路数:";
int Several;
cin >> Several;
Merge_sort<int> M(Size, Several);
M.initialize("input.txt");//要外排序的文件
M.combine();
cout << "磁盘访问次数为:"<< times << endl;
return 0;
}
测试结果
完整代码
#include<iostream>
#include<string>
#include<fstream>
#include<cstdlib>
#include<climits>
using namespace std;
int times = 0;//记录磁盘的读取次数
struct Node {//设置Node节点,用来进行初始归并段的生成
int priority;//优先级
int number;
bool operator<(const Node& a)const{
if (priority != a.priority) {//如果优先级不同那么就先比较优先级,如果优先级相同,则比较数值
return priority < a.priority;
}
else {
return number < a.number;
}
}
};
template<class T>
class losertree
{
public:
losertree(T* a, int num);
T winner(){ return loser[0]; }//loser[0]中存储的是最小值
T winner_index() { return winnerindex[1]; }//最小值的下标
void reconfiguration(T element);//重构
private:
int size;//输者树大小
T* loser;//输者树
int* winnerindex;//记录每次的赢者下标
T* player;//选手数组->赢者树
};
template<class T>
losertree<T>::losertree(T* a, int num){//构造输者树
size = num;
player = new T[2 * num];//选手数组
winnerindex = new int[num * 2];
loser = new T[num];
for (int i = size; i < 2 * size; i++){
player[i] = a[i - size];//初始化选手数组
winnerindex[i] = i;//初始化赢者的下标
}
for (int i = size - 1; i > 0; i--){//两两比较
if (player[2 * i] < player[2 * i + 1]){
player[i] = player[2 * i];//交小的赢
loser[i] = player[2 * i + 1];//较大的输
winnerindex[i] = winnerindex[2 * i];
}
else{
player[i] = player[2 * i + 1];
loser[i] = player[2 * i];
winnerindex[i] = winnerindex[2 * i + 1];
}
}
loser[0] = player[winnerindex[1]];//将最终的赢者也就是最小值存到输者树的顶端
}
template<class T>
void losertree<T>::reconfiguration(T element){//重构
player[winnerindex[1]] = element;//将原来的最小值替换掉
for (int i = winnerindex[1] / 2; i > 0; i = i / 2) {//从最小值的父节点开始向上比较,直到根节点为止
if (player[2 * i] < player[2 * i + 1]){
player[i] = player[2 * i];//较小的赢
loser[i] = player[2 * i + 1];
winnerindex[i] = winnerindex[2 * i];
}
else{
player[i] = player[2 * i + 1];
loser[i] = player[2 * i];
winnerindex[i] = winnerindex[2 * i + 1];
}
}
loser[0] = player[winnerindex[1]];//更新新的最小值
}
template<class T>
class Merge_sort{//归并排序
public:
Merge_sort(int _Size, int _NumberOfCombine);
void initialize(string name);//初始归并段的生成
void combine();//归并段的合并
private:
int Size;//输者树的大小
int number_way;//记录归并路数
int number_file;//记录每次产生的归并段个数
string filename, filetype;//产生归并段的名称
string finalname; //最终输出的文件的名称
};
template<class T>
Merge_sort<T>::Merge_sort(int size, int waynumber){
Size = size;
number_way = waynumber;
number_file = 0;
filetype = ".txt";
finalname = "output.txt";
filename = "Merging_segment";//归并段名称
}
template<class T>
void Merge_sort<T>::initialize(string name) {//初始归并段的生成
ifstream inputfile(name, ios::in); //打开名为name的文件,进行输入操作
times++;
Node* data = new Node[Size];//建立一个大小为输者树大小的数组
for (int i = 0; i < Size; i++){
inputfile >> data[i].number;//文件前Size个数输入到数组中
data[i].priority = 1;//初始优先级均为1
}
losertree<Node> loser_tree1(data, Size);//将该数组构造成输者树
int num;
while (!inputfile.eof()){// 到文件末尾则退出循环
inputfile >> num;
Node minwin = loser_tree1.winner();
Node p;
p.number = num;
if (num > minwin.number) {//如果num比最小输者树的最小值大,则优先级相同,反之则优先级+1
p.priority = minwin.priority;
}
else{
p.priority = minwin.priority + 1;//优先级加一,存到新的归并段
number_file = max(number_file, p.priority);//初始归并段的个数,即最大优先级
}
loser_tree1.reconfiguration(p);//将新数据放入输者树中重构
string initialname = filename + "0_" + to_string(minwin.priority) + filetype;
ofstream outfile(initialname, ios::app);//输出文件流,ios::app在文件末位接着输出
times++;//由于打开了initialname文件因此,寻盘次数加一
outfile << minwin.number << ' ';
outfile.close();
}
while (loser_tree1.winner().number != INT_MAX){//将输者树中剩余的数输出
Node minwin = loser_tree1.winner();
Node p;//p的优先级得是最低,并且值是最大,方便将数据利用输者树的赢者函数输出
p.priority = INT_MAX;
p.number = INT_MAX;
loser_tree1.reconfiguration(p);
string initialname = filename + "0_" + to_string(minwin.priority) + filetype;
ofstream outfile(initialname, ios::app);
times++;
outfile << minwin.number << ' ';
outfile.close();
}
}
template<class T>
void Merge_sort<T>::combine(){//归并段的合并
for (int h = 0; number_file != 1; h++){//h为归并轮数
ifstream* prefile = new ifstream[number_file + 1];//存前一次归并得到的归并段
for (int i = 1; i <= number_file; i++){
prefile[i].open(filename + to_string(h) + "_" + to_string(i) + filetype, ios::in);//第h次(上一轮)归并的第i个归并段
times++;
}
int j = 0;
while (j < ceil((double)number_file / (double)number_way)){//将所有的文件进行k路归并,j为对number_file个归并段k路归并,需要归并的次数,也是下一次归并的归并段数
string out;
if (number_file <= number_way){//判断产生的文件是否是最终的归并结果,对输出的文件名进行定义
out = finalname;//如果归并路数大则说明是最后一轮归并操作
}
else{
out = filename + to_string(h + 1) + "_" + to_string(j + 1) + filetype;//这一轮归并得到的归并段文件名
}
ofstream outfile(out, ios::out);//ios::out重新在文件中输出
times++;
int* he = new int[number_way];//存上一轮归并得到的归并段中的最小值
for (int i = 1; i <= number_way; i++){
if (j * number_way + i > number_file ){//判断是否有足够的归并段凑够归并路数个,如果没有用最大值放入。
he[i - 1] = INT_MAX;
}
else {
prefile[j * number_way + i] >> he[i - 1];
}
}
losertree<int> L(he, number_way);
while (L.winner() != INT_MAX){
outfile << L.winner() << ' ';
for (int f = 0; f < number_way; f++){
if (L.winner_index() == f + number_way && j * number_way + f + 1 <= number_file ) {//得到最小输者树的最小值下标是具体的那个文件,此时还要注意有可能存在剩余的文件个数小于归并路数的情况
int u = 0;
prefile[j * number_way + 1 + f] >> u;//将该文件新的最小值取出
if (prefile[j * number_way + 1 + f].eof()) u = INT_MAX;//如果到文件末位则将最大值输入到输者树中
L.reconfiguration(u);//对输者树重构
break;
}
}
}
outfile.close();
j++;
}
for (int i = 1; i <= number_file; i++) prefile[i].close();
number_file = j;//更新归并段数
}
}
int generateUniformRandomInt(int min, int max) {//约束随机数的大小
return min + rand() % (max - min + 1);
}
int main(){
cout << "请输入要生成的随机数个数:";
int n, max;
cin >> n;
cout << "请输入随机数的最大值:";
cin >> max;
ofstream inputfile("input.txt");
for (int i = 0; i < n; ++i) {
int randomNum = generateUniformRandomInt(0, max);
inputfile << randomNum << " ";
}
inputfile.close();
cout << "请输入输者树的大小:";
int Size;
cin >> Size;
cout << "请输入要实现归并路数:";
int Several;
cin >> Several;
Merge_sort<int> M(Size, Several);
M.initialize("input.txt");//要外排序的文件
M.combine();
cout << "磁盘访问次数为:"<< times << endl;
return 0;
}
如能打赏,不胜感激[叩谢]。