排序算法学习总结(c++)
概述
排序算法是数据结构与算法里极其重要的基础知识。分为内排序和外排序。
排序算法 | 内排序算法 | 外排序算法 |
---|---|---|
特点 | 待排序记录都放在主存(main memory)中 | 待排序的记录太多而无法放在主存中,需要把其中一些记录从磁盘中读取出来进行内排序,再将记录写回磁盘。 |
算法衡量标准 | 时间复杂度(比较和交换次数) | 磁盘I/O次数 |
默认的已排序成功标志:数组从小到大排列,将从小到大视为正序
1. 内排序
常见的内排序有10种,每种排序算法都有自己的优点和缺点,因此需要程序员在使用时进行时间和空间上的权衡,来决定需要用哪一个。
算法 | 稳定性 | 空间复杂度 | 时间复杂度(平均) | 时间复杂度(最好) | 时间复杂度(最差) |
---|---|---|---|---|---|
插入排序 | 稳定 | O(1) | O(n2) | O(n) | O(n2) |
冒泡排序 | 稳定 | O(1) | O(n2) | O(n) | O(n2) |
选择排序 | 不稳定 | O(1) | O(n2) | O(n2) | O(n2) |
希尔排序 | 不稳定 | O(1) | O(n1.5) | O(n) | O(n2) |
归并排序 | 稳定 | O(n) | O(nlogn) | O(nlogn) | O(nlogn) |
快速排序 | 不稳定 | O(logn) | O(nlogn) | O(nlogn) | O(n2) |
堆排序 | 不稳定 | O(1) | O(nlogn) | O(nlogn) | O(nlogn) |
计数排序 | 稳定 | O(n+k) | O(n+k) | O(n+k) | O(n+k) |
桶排序 | 稳定 | O(n+k) | O(n+k) | O(n+k) | O(n2) |
基数排序 | 稳定 | O(n+k) | O(n*k) | O(n*k) | O(n*k) |
排序算法的稳定性:稳定的排序算法是排序前后具有相同的关键字的记录的相对次序不发生改变。
- 不稳定的排序算法是选择排序、希尔排序、快速排序、堆排序,一个记忆的小窍门是:希尔选择快排队(堆)。
1.1 Insertion Sort
插入排序类似于打扑克牌时拿到牌进行排序的过程,即从左到右第2张牌开始依次与其左边所有的牌(从右到左)比较,插入到合适的位置上(即待排序的牌的左边全部是已经排好大小的牌)。
直观形象是这副牌左边排序好的队列越来越长,右边待排序的队列越来越短。
//插入排序
vector<int> Insertion_Sort(vector<int> nums) {
int n = nums.size();
for(int i=1;i<n;i++)
for (int j = i; j > 0; j--) {
if (nums[j] < nums[j - 1]) {
int temp = nums[j];
nums[j] = nums[j - 1];
nums[j - 1] = temp;
}
}
return nums;
}
复杂度分析:
- 最好的情况:原本的数组就是从小到大排好序的,因此每个节点刚进入内层循环就退出,相当于只有1层循环,时间复杂度为O(n)
- 最差的情况:原本的数组是从大到小排序的,则每个节点都要遍历移动到最左边,两层循环从头做到尾(不能偷懒),时间复杂度为O(n2)
- 平均情况:O(n2)
1.2 Bubble Sort
冒泡排序则是每一次循环都做:从最右边一个数开始比较相邻数字,如果是反序则交换,交换完后到下一个数继续比较。
//冒泡排序一,将小的推上去(泡泡冒上来)
vector<int> Bubble_Sort(vector<int> nums) {
int n = nums.size();
for (int i = 0; i < n-1; i++)
for (int j = n-1; j > i; j--) {
if (nums[j] < nums[j - 1]) {
int temp = nums[j];
nums[j] = nums[j - 1];
nums[j - 1] = temp;
}
}
return nums;
}
//冒泡排序二,将大的沉下去
vector<int> Bubble_Sort(vector<int> nums) {
int n = nums.size();
for (int i = 0; i < n - 1; i++) //大的沉下去
for (int j = 0; j < n - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
return nums;
}
每一次循环都是比较相邻数字大小,但是每次循环都比上次少比较1个数字。
复杂度分析:
- 最好/最坏/平均的情况:内层循环无论怎样比较的次数都是n次,因此都为O(n2)。
1.3 Selection Sort
选择排序则是每次循环都挑出未排序队列里最小的数放入已排序的那一队末尾。
//选择排序
vector<int> Selection_Sort(vector<int> nums) {
int n = nums.size();
for (int i = 0; i < n; i++) {
int min = i;
for (int j = i+1; j < n; j++) {
if (nums[j] < nums[min]) {
min = j;//更新最小值所指的下标
}
}
int temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
return nums;
}
复杂度分析:
- 最好/最坏/平均的情况:对于任何输入来说,内层循环比较次数相同,因此都为O(n2)。然而键的交换次数仅为n-1次,即O(n),这是选择排序优于其他排序算法的地方。
1.4 Shell Sort
希尔排序(缩小增量排序),是插入排序的变体。
步骤:
- 每执行一次循环,把序列分为若干个互不相连的子序列,使各个子序列中的元素在整个数组中的间距相同,然后对每一个子序列都采用插入排序的方法进行排序。
- 一次循环完成后缩减增量,再进入循环。
- 知道子序列划分的间距为1时,终止循环,最后进行1次全序列的插入排序则得到结果。
//希尔排序
vector<int> Shell_Sort(vector<int> nums) {
int n = nums.size();
for (int step = n/2; step > 0; step/=2) { //定步长step。做完一次循环后缩减增量
for (int j = step; j < n; j++) { //进行若干个子序列的插入循环
int temp = nums[j];
int k = j;
for (; k >= step && temp < nums[k - step]; k -= step) {
nums[k] = nums[k - step];
}
nums[k] = temp;
}
}
return nums;
}
复杂度分析:
- 最好的情况:同插入排序一样,原本的数组就是从小到大排好序的,因此每个节点刚进入内层循环就退出,相当于只有1层循环,时间复杂度为O(n)
- 最差的情况:时间复杂度为O(n2)
- 平均情况:O(n1.5)
1.5 Merge Sort
归并排序
思想:递归+分治法,所谓分治法则是将问题分成若干个小问题(子问题最好规模相同)再对子问题求解,合并这些子问题的解,得到原问题的答案。
下图则是二路归并排序,多路归并排序在外排序上运用较多。
//归并排序(二路归并)
void Merge_Sort(vector<int>&nums){ //让形参&nums成为引用变量,以便可以在函数中对nums的值进行修改
int n = nums.size();
if (n <= 1)
return;
if (n > 1) {
vector<int>sub_nums1;
vector<int>sub_nums2;
for (int i = 0;i<n;i++) {
if(i<n/2)
sub_nums1.push_back(nums[i]);
else
sub_nums2.push_back(nums[i]);
}
Merge_Sort(sub_nums1);//递归划分子序列
Merge_Sort(sub_nums2);
nums.clear();//将nums清空,以便放入排序后的新序列
Merge(nums, sub_nums1, sub_nums2);//合并两个排序好的子序列
}
}
void Merge(vector<int>&nums,vector<int>&nums1, vector<int>&nums2) { //合并两个子序列
int n1 = nums1.size();
int n2 = nums2.size();
int i = 0;
int j = 0;
while (i < n1 && j < n2) {
if (nums1[i] < nums2[j]) {
nums.push_back(nums1[i]);
i++;
}
else {
nums.push_back(nums2[j]);
j++;
}
}
if (i == n1) //若nums1都加上了,那么将nums2剩下的直接加到结果的末尾
nums.insert(nums.end(),nums2.begin() + j, nums2.end());
else
nums.insert(nums.end(),nums1.begin() + i, nums1.end());
}
复杂度分析:
- 最好/最坏/平均的情况:对于任何输入来说,都为O(nlogn)。
1.6 Quick Sort
快速排序,应用十分广泛,因为当运用得恰到好处时,它是所有内排序算法中在平均情况下最快的一种!然而因为最差时间代价O(n2)在某些应用中无法采用。在现在快速排序算法在轴值pivot的选择上有不同的方法:选择子数组的第一位,随机选择
思想:递归+分治法。
法一(Hoare划分法)步骤:
- 以子数组第1个元素为轴值pivot
- 分别从子数组的两端进行扫描,将扫描到的元素与轴值比较。从左到右的扫描由第2个元素开始,扫描直到遇到第一个大于轴值的元素停止;从右到左的扫描由末尾开始,扫描直到遇到第一个小于轴值的元素为止。当两个扫描都停止时,比较左扫描指针i和右扫描指针j的大小:若i<j,交换i和j指向的元素,i加1,j减1然后继续扫描;若i>=j,此时我们成功建立了一个数组的划分,分裂点的位置是j指向的位置,因此交换轴值和j指向的元素,将轴值置于分裂点上。
- 继续对分裂点的左右两个子数组进行上述操作,直到无法继续划分则成功将序列排序好了。
//快速排序
void Quick_Sort(vector<int>&nums, int left, int right) {
if (left < right) {
int pivot = Partition(nums, left, right);//确定划分点
Quick_Sort(nums, left, pivot - 1);//递归划分
Quick_Sort(nums, pivot + 1, right);
}
}
int Partition(vector<int>&nums, int left, int right) { //返回分裂点位置的下标
int pivot = nums[left];//轴值定为子序列的第一位
int i = left;
int j = right;
while (i < j) { //将满足条件的交换,最后交换j指向的值和轴值
while (nums[j] >= pivot && j > i)
j--;
nums[i] = nums[j];
while (nums[i] <= pivot && i < j)
i++;
nums[j] = nums[i];
}
nums[j] = pivot;
return j;
}
快速排序 VS 归并排序
快速排序不需要额外的数组空间,空间效率高,快速排序是按照元素的值对它们进行划分,合并排序是按照元素在数组中的位置对它们划分。
区别 | 快速排序 | 归并排序 | 堆排序 |
---|---|---|---|
算法的主要工作 | 划分阶段,不需要再去合并子问题的解 | 合并子问题的解,划分阶段很快 | |
空间效率 | 不需要额外的数组空间,需要一个额外的堆栈来存储还没有被排序的子数组的参数 | 需要额外的数组空间 | 不需要任何额外的存储空间(堆排序是在bit的排序) |
时间效率 | 最佳和平均为O(nlogn),最差为O(n2) | 最佳、平均、最差都为O(nlogn) | 最佳、平均、最差都为O(nlogn) |
稳定性 | 不稳定 | 稳定 | 不稳定 |
随机文件的计时实验 | 较快 | 较慢 | 较慢 |
复杂度分析:
- 最好和平均的情况:时间复杂度为O(nlogn)
- 最差的情况:时间复杂度为O(n2)
最差情况(退化为冒泡排序)出现在:
1)数组已经是正序排过序的。 (每次最右边的那个元素被选为枢轴)
2)数组已经是倒序排过序的。 (每次最左边的那个元素被选为枢轴)
3)所有的元素都相同(1、2的特殊情况)
1.7 Heap Sort
堆排序,首先将待排序列构建成堆(也就是完全二叉树),然后调整成最大堆,把最大键去除后再调整成最大堆,直至堆中元素完全删除,得到排序好的从小到大的新序列。
【完全二叉树是指除了最后一个最右边的元素可能为空之外,其他节点都是满的】
【最大堆:父节点>=子节点,同一层节点之间不存在大小关系】
步骤:
- 构造最大堆。
- 删除最大键,即对剩下的堆应用n-1次根删除操作。
//堆排序
void Heap_Sort(vector<int>&nums) {
BuildMaxHeap(nums);//构建堆
for (int i = nums.size() - 1; i >= 0; i--) { //将最大值(根)与末尾元素交换,重新构建去除了最大值的最大堆
int temp = nums[i];
nums[i] = nums[0];
nums[0] = temp;
AdjustMaxHeap(nums,0,i);
}
}
void BuildMaxHeap(vector<int>&nums) { //构建最大堆
for (int i = nums.size() / 2; i >= 0; i--) //对于未排序堆,从下标为N/2的元素往前依次调整位置
AdjustMaxHeap(nums, i, nums.size());
}
void AdjustMaxHeap(vector<int>&nums,int node,int len) { //把当前未排序完成的堆调整为最大堆
int index = node;
int child = 2 * index + 1;//左子节点
while (child < len) {
if (child + 1 < len && nums[child] < nums[child + 1]) { //选择左右子节点较大的一个来与该节点比较大小
child++;
}
if (nums[index] < nums[child]) { //该节点小于其子节点就交换
int temp = nums[index];
nums[index] = nums[child];
nums[child] = temp;
index = child; //再与交换后新位置的子节点比较大小直至目标(子节点不大于父节点)
child = 2 * index + 1;
}
else
break;
}
}
复杂度分析:
- 最好/最坏/平均的情况:对于任何输入来说,都为O(nlogn)。
1.8 Count Sort
计数排序,是一种线性排序。
思想:时空权衡,以空间换时间。
计数排序 | 比较计数排序法 | 分布计数排序法 |
---|---|---|
空间效率 | 以空间换时间。需要额外的数组空间,大小取决于待排序列元素个数 | 以空间换时间。需要额外的数组空间,大小取决于待排序列数据范围,对于数据范围很大的序列会有巨大的内存消耗 |
时间效率 | 最佳、平均、最差都为O(n2) | 最佳、平均、最差都为O(n),是利用了序列独特的自然属性 |
使用条件 | 无 | 已知数组中所有数都位于(left,right)区间内 |
//计数排序(比较计数法)
void Count_Sort(vector<int>&nums) {
vector<int>count(nums.size()); //需要一个额外的数组来计数
int n = nums.size();
for (int i = 0; i < n-1; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[i]) { //若其他元素小于目标值,目标值的计数++
count[i]++;
}
else {
count[j]++;
}
}
}
vector<int>result(nums);
for (int i = 0; i < n; i++) { //将排序后的序列输入nums数组
nums.at(count[i]) = result[i];
}
}
//计数排序(分布计数法,条件是已知数组中所有数都位于(left,right)区间内)
void Distribution_Count_Sort(vector<int>&nums,int left,int right) {
vector<int>count(right-left+1); //需要一个额外的数组来计数
int n = nums.size();
vector<int>nums_copy(nums);//复制一个nums_copy数组,以便将排序好的序列放入nums
for (int i = 0; i < n; i++) {
count[nums[i] - left]++;
}
for (int i = 1; i < right-left+1; i++) {
count[i]+=count[i-1];
}
for (int i = n-1; i >= 0; i--) { //排序好的序列放入nums
int j = nums_copy[i] - left;
nums[count[j] - 1] = nums_copy[i];
count[j]--;
}
}
(比较计数法)复杂度分析:
- 最好/最坏/平均的情况:对于任何输入来说,都为O(n2),执行的键值比较次数和选择排序一样多。优点是使得键值可能移动的次数最小化。
(分布计数法)复杂度分析:
- 最好/最坏/平均的情况:对于任何输入来说,都为O(n)
1.9 Bucket Sort
桶排序,是分配排序的进一步拓展。理论上来讲,桶的数量越多,时间复杂度就越低,当然空间复杂度就越高。而且和计数排序很相似,如果桶的数量是 max - min + 1,这个时候,桶排序和分布计数排序几乎就是一样的。
思想:时空权衡,以空间换时间。
步骤:
- 设置桶的数量。
- 将序列一个个放入桶中。
- 对非空桶进行排序。
- 将非空桶中的内容(已排序的数列)替换原来的序列。
//桶排序
void Bucket_Sort(vector<int>&nums) {
int n = nums.size();
if (!n)
return;
int max, min;
max = min = nums[0];
for (int i = 1; i < n; i++) { //确定待排序列的数值范围
if (nums[i] < min)
min = nums[i];
if (nums[i] > max)
max = nums[i];
}
int gap = 2;//设置间隔
int bucket_num = (max - min) / gap + 1;
vector<list<int>>bucket(bucket_num); //创建桶来放入数据
vector<int>nums_copy(nums);//复制一个nums_copy数组,以便将排序好的序列放入nums
for (int i = 0; i < n; i++) {
insert(bucket[(nums[i] - min) / gap], nums[i]); //将数据放入桶中
}
int index = 0;
for (int i = 0; i < bucket_num; i++) { //将排好的序列置换原序列
if (bucket[i].size()) {
for (auto& value : bucket[i])
nums[index++] = value;
}
}
}
void insert(list<int>& bucket, int val){
auto iter = bucket.begin();
while (iter != bucket.end() && val >= *iter)
++iter;
bucket.insert(iter, val);
}
复杂度分析:
- 最好和平均的情况:时间复杂度为O(n)
- 最差的情况:时间复杂度为O(n2)
1.10 Radix Sort
基数排序,
//基数排序
void Radix_Sort(vector<int>&nums) {
int n = nums.size();
if (!n)
return;
int max_bit = Maxbit(nums);//获取最大位数
int r = 1;
vector<int>count(10); //需要一个额外的数组来计数
vector<int>nums_copy(nums);//复制一个nums_copy数组,以便将排序好的序列放入nums
for (int i = 0; i < max_bit; i++) {
for (i = 0; i < n; i++) { //计算每个桶的记录数
int remainder = (nums_copy[i] / r) % 10;
count[remainder]++;
}
for (i = 1; i < 10; i++) { //计算本轮排序位置
count[i]+=count[i-1];
}
for (int k = n - 1; k >= 0; k--) { //进行右往左第r位的排序
int remainder = (nums_copy[k] / r) % 10;
nums[count[remainder] - 1] = nums_copy[k];
count[remainder]--;
}
r *= 10; //位数左移一位
}
}
int Maxbit(vector<int>&nums) { //计算序列中最大位数
int max_bit = 1;
for (int i = 0; i<nums.size();i++) {
int bit = 1;
int num = nums[i];
while (num / 10) {
num /= 10;
bit++;
}
if (bit > max_bit) //更新最大位数
max_bit = bit;
}
return max_bit;
}
复杂度分析:
- 最好/最坏/平均的情况:都为O(n)。对于n个数据的序列,基数为r,这个算法需要k轮分配工作,每一轮分配的时间为O(n+r),因此总时间代价为O(nr+kr)。r是基数,对于整数可以选2或10,对于字符串可以选26。k是以r为基数时,关键码可能具有的最大位数。在一般情况下,视k和r为常数,则时间代价为O(n),这是所有排序算法中最佳的时间代价!
2. 外排序
上面讲完了十种内排序算法,内排序是指待排序序列放在主存里进行排序的过程。 而有时候我们会需要对一个大文件进行排序,而计算机内存是有限的,当数据无法完全存入内存时,则无法使用正常的内排序算法一次性完成排序。这时候就必须利用磁盘空间的辅助进行外排序了。
外排序思想:利用有限的内存每次读入部分数据,进行内排序得到一个顺串后暂时放到磁盘,最后将多个顺串进行归并直到最终完成排序。
因为从磁盘中读写一个块所花费时间是通过主存访问同样大小的块所花费时间的100万倍!因此可以合理认为,在主存中对一个块内记录采用内部排序算法排序所花费的时间远少于读写这个块所花费的时间。因此:
外排序算法的主要目标是:减少读写磁盘的信息量,即磁盘I/O次数。
所有好的外排序算法都基于下面两步:
- 把文件分成大的初始顺串。
- 把所有顺串归并到一起,形成一个已排序文件。
一个好的外排序算法会尽量做好以下方面:
- 建立尽可能大的初始顺串
- 在所有阶段尽可能使输入、处理和输出并行
- 使用尽可能多的工作主存,以加速处理。
- 如果有可能,可以使用多块磁盘,以使I/O处理有更大的并行性,并且允许顺序文件处理。
如果你的操作系统支持虚拟存储,最简单的外排序算法是:把整个文件读入虚拟存储中,然后运行一个内部排序算法。
【知识回顾】回顾一下从磁盘中访问信息的基本方式。
- 待排序文件是有一定顺序的、固定大小的块(block),扇区是I/O的基本单位,所有的磁盘读写都是对一个或多个完整的扇区进行的,扇区的大小一般是2的若干次幂,一般在512Bytes-16KBytes之间。外排序算法使用的块大小应当等于扇区大小,或者是扇区大小的若干倍。
- 要准确理解在什么情况下顺序文件访问实际上比随机文件访问更快,因为它会影响外排序算法的设计方法。
下面将介绍基本的外排序算法:简单外部归并排序算法。以及在此基础上进行改进的:置换选择算法、多路归并排序算法。
2.1 简单外部归并排序
步骤:
- 把原始文件分成两个大小相等的顺串文件(run file)。
- 从每个顺串文件中取出一个块,读入输入缓冲区中。
- 从每个输入缓冲区中取出第一条记录,把它们按照排好的次序写入一个顺串输出缓冲区。
- 从每个输入缓冲区中取出第二条记录,把它们按照排好的次序写入另一个顺串输出缓冲区。
- 再两个顺串缓冲区之间交替输出,重复这些步骤直到结束。当遇到一个输入块的末尾时,从相应的输入文件中读出第二个块。当一个顺串输出缓冲区已满时,把它写回相应的输出文件。
- 使用原始输出文件作为输入文件,重复步骤2~5。在第2趟扫描中,每个输入顺串文件中的前两条记录已经排好了次序。这样就可以把这两个顺串归并成一个长度为4的顺串输出了。
- 对顺串文件的每一趟扫描都会产生更大的顺串,知道最后只剩下一个顺串。
算法特点:
- 该算法可以方便地利用双缓冲技术,每一趟扫描都顺序地读出输入顺串文件,然后顺序地写入输出顺串文件。然而要使顺序处理和双缓冲技术有效率,需要每个文件单独使用一个I/O磁头,意味着每个输入文件和输出文件必须在一个单独的磁盘上,要使得效率最高,就要使用4个磁盘。
- 对于长度很小的顺串,不需要使用归并排序就可以显著减少扫描趟数,读入一块数据,在主存中进行排序,然后作为一个已排序的顺串输出。
- 如果处理的初始顺串再大一些,归并排序需要的趟数就会更少一些。——置换选择算法
- 另一种减少趟数的方法是在每一趟扫描中多归并几个顺串。——多路归并排序
对简单外部归并排序算法的优化——减少归并排序时需要的趟数,有两种方法:
- 初始顺串再大一些——置换选择算法
- 在每一趟扫描中多归并几个顺串。——多路归并排序
2.2 置换选择算法
怎样为一个磁盘文件创建尽可能大的初始顺串?
- 简单的方法:把一个尽可能大的RAM分配给一个大数组,从磁盘中读出数据,放到这个数组中,然后使用快速排序算法为该数组排序。如果分配给数组的可用主存大小是M条记录,那么就可以把输入文件分成长度为M的初始顺串。
- 更好的方法:使用一个置换选择的算法,在平均情况下可以创建长度为2M条记录的顺串。置换选择方法是把RAM看成一块长度为M的连续数组,再加上输入缓冲区和输出缓冲区(如果操作系统支持双缓冲技术,可能还需要额外的I/O缓冲区,因为置换选择方法在输入输出中都进行顺序处理)。把输入文件和输出文件都想象成记录流。置换选择方法在需要时从输入流中顺序地取出一条记录,然后一次一条记录的向输出流输出顺串。通过使用缓冲区,一次磁盘I/O就可以处理一个块。最初读入一个块的记录,放到输入缓冲区中,置换选择方法每次从输入缓冲区中移除一条记录,直到缓冲区为空;此时再读入下一个块的记录,向缓冲区输出也是类似的:一旦填满了输出缓冲区,就把它作为一个整写回磁盘。
置换选择算法实际上是堆排序的一个微小变体。
步骤:(假定主要处理在一个大小为M条记录的数组中完成)
- 从磁盘中读出数据,放到数组中,设置LAST=M-1;
- 建立一个最小值堆;
- 重复以下步骤,直到数组为空:
- 把具有最小关键码值的记录(根节点)送到输出缓冲区;
- 设R是输入缓冲区中的下一条记录。如果R的关键码值大于刚刚输出的关键码值,把R放在根节点,否则使用数组中LAST位置的记录代替根节点,然后把R放到LAST位置。设置LAST=LAST-1;
- 筛选出根节点,重新排列堆;
2.3 多路归并排序
使用简单的二路归并,对于整个文件来说R个顺串需要logR趟扫描。二路归并并不能充分利用可用主存(由于归并是作用在两个顺串上的顺序过程,每个顺串一次只需要有一个块的记录在主存中。将一个顺串的多个块都放在内存中并不能减少归并过程中的I/O次数,只能让磁盘的顺序访问比单独读入时间要少)。因此,置换选择算法的堆使用的大多数空间(一般为多个块)并没有在归并过程中得到利用。
如果一次归并多个顺串,就可以更好地利用这些空间,同时还可以大大减少归并顺串所需要的扫描趟数。
步骤:
多路归并与二路归并类似,如果有B个顺串需要归并,从每个顺串中取出一个块放在主存中使用,那么B路归并算法仅仅查看B个值(每个输入顺串中最前面的值),并选择最小的一个输出,把这个值从它的顺串中移除,然后重复这个过程。当任何一个顺串的当前块用完时,就从磁盘中读出这个顺串的下一个块。
- 多路归并假定每个顺串存储在一个单独的文件中(实际上非必须),只需要知道每个顺串在某个文件中的位置,每当需要从魔偶一个顺串中得到新数据时,就使用seekg把文件指针指向相应的块。但是使用这样的方法就不能对输入文件就行顺序处理了。
- 如果主存中存在为每个顺串存储一个块的空间,那么一趟扫描就可以归并所有顺串,这样一来,置换选择方法在一趟扫描中就可以建立初始顺串,多路归并一趟扫描就可以归并所有顺串,总代价是两趟扫描。
一趟扫描可以归并多大的文件呢?
假定可以为置换选择方法的堆分配B个块(这样顺串的平均长度就是2B个块),接下来进行一次B路归并,在一次多路归并中,平均可以处理具有2B2个块大小的文件。在k个B路归并中平均可以处理具有2B2+1个块大小的文件。
- 举个例子:假如有大小为0.5MB的工作主存可以使用,一个块的大小是4KB,那么在工作主存中就有128个块,平均顺串长度是1MB(工作主存大小的2倍),一趟扫描可以归并128个顺串。因此,在0.5MB的工作主存中平均2趟扫描(1趟建立顺串+1趟用于归并)就可以处理大小为128MB的文件。
- 再举个例子:假如块的大小为1KB,工作主存是1MB=1024个块,那么一趟可以归并1024个平均长度为2MB的(因此排序的总量为2GB)顺串。如果工作主存大小固定,块大一些可以减少在一趟扫描中要处理的文件大小;块小一些或工作主存大一些可以增加一趟扫描处理的文件大小。对于0.5MB的工作主存+4KB的块,2趟扫描可以处理16GB大小的文件。