外排序(最小输者树实现)

问题描述

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

基本要求

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

解题

输者树介绍

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

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

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

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

  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. 顺串号相同,较小元素值的元素获胜。

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

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

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

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#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;
}





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