排序是数据处理中经常使用的一种操作,其主要目的是便于查找。

1.没有特殊说明,本文排序后的结果都是从小到大(正序)

2.以下所用的算法,除桶式排序和基数排序外其r[0]作为哨兵或者闲置,数组从1开始存储。

3.主函数和输入样例如下:

#include<iostream>
using namespace std;
int main() {cin >> n;for (int i = 1; i <= n; i++)cin >> r[i];//Insertsort();//Shellsort();//Bubblesort();//Quicksort(1, n);//Quickselectsort();//Selectsort();//Heapsort();//Mergesort1();//Mergesort2(1, n);for (int i = 1; i <= n; i++)cout << r[i]<<" ";return 0;
}
/*
10 7 1 4 6 8 9 5 2 3 10—— 预期输出 ——1 2 3 4 5 6 7 8 9 1015 3 44 38 5 47 15 36 26 27 2 46 4 19 50 48—— 预期输出 ——2 3 4 5 15 19 26 27 36 38 44 46 47 48 50
*/

下面来介绍几种常用的排序。(教科书式开头

直接插入排序

具体的排序过程如下:
1)初始时有序区为待排序记录序列中的第一个记录,无序区包括所有剩余待排序的记录;
2)将无序区中的记录依次一个个的插入到有序区的合适位置中。

for(int i=2;i<=n;i++){//插入第n个记录}

完整代码如下:

int r[100];//排序数列
int n;//数列长度
void Insertsort() {int i, j;for (i = 2; i <= n; i++) {//无序数列依次插入r[0] = r[i];//利用哨兵存储待排记录,并防止数组下标越界for (j = i - 1; r[0] < r[j]; j--)r[j + 1] = r[j];//若没有找到待插点,就让有序数列依次覆盖后移r[j + 1] = r[0];//插入待插记录}
}

性能分析
最好情况:3(n-1)------O(n)
最坏情况:(n-1)(n+3)------O(n^2)
平均情况:(n-1)(n+3)/2------O(n^2)
直接插入排序算法简单、容易实现,但是效率并不算突出,当序列中的记录基本有序或待排序记录较少时,它是最佳的排序算法。
另外,直接插入排序还是一种稳定的排序方法。
稳定:相同元素的相对位置经过排序后不变

希尔排序

希尔排序是对直接插入排序的一种改进。
改进的着眼点是:
1)若待排序记录按关键码基本有序,直接插入排序的效率很高;
2)由于直接插入排序算法简单,则在待排序记录个数较少时效率也很高
其基本思想是:先将整个待排序记录序列分割成若干个子序列,在子序列中分别进行直接插入排序,待整个序列基本有序时,再对全体记录进行一次直接插入排序。
注意:子序列的构成不能是简单地“逐段分割”,而是将相距某个“增量”的记录组成一个子序列,这样才能有效地保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
完整代码如下:

int r[100];//排序数列
int n;//数列长度
void Shellsort() {int i, j, d;for (d = n / 2; d >= 1; d /= 2) {//设定间隔d,d代表子序列个数,n/d代表每个序列的长度for (i = d + 1; i <= n; i++) {//无序数列按间隔d依次插入r[0] = r[i];//利用哨兵存储待排记录,并防止数组下标越界for (j = i - d; j > 0 && r[0] < r[j]; j -= d)r[j + d] = r[j];//若没有找到待插点,就让按间隔d有序的数列依次覆盖后移r[j + d] = r[0];//插入待插记录}}
}

性能分析
最好情况:O(nlog2 n)
最坏情况:O(n^2)
平均情况:O(n^1.3)
希尔排序是一种不稳定的排序方法。

冒泡排序(起泡排序)

其基本思想是:两两比较相邻记录的关键码,如果反序则交换,直到没有反序的记录为止。
下面是一个简单的冒泡排序算法:

//简易的冒泡排序
for (int i = 1; i < n; i++){for (int j = 1; j <= n-i; j++) {if(r[j]>r[j+1])//交换}
}

然后我们再想想能不能进行优化。
其中,一个最大的优化点在于该算法的比较是不是存在“浪费现象”?
每次冒泡一趟真需要比较n-i次吗?
如果设置一个exchange变量记录交换处,设置一个bound变量表示冒泡一趟需进行的比较次数。
那么每次排序前让bound=exchange是不是可以降低比较的“浪费程度”?

下面是改进后的算法:

int r[100];//排序数列
int n;//数列长度
void Bubblesort() {//当一趟不再有交换时,即已排序完成//最后一次交换点就是下一趟排序的终止点(因为后面的皆已有序)int exchange = n, bound;//初始化exchange=nwhile (exchange != 0) {bound = exchange;//将接下来的排序终点设为上一次排序最后的交换处exchange = 0;//在排序之前将exchange初始化为0for(int i=1;i<bound;i++)if (r[i] > r[i + 1]){int tmp = r[i];r[i] = r[i + 1];r[i + 1] = tmp;exchange = i;//记录交换位置}}
}

性能分析
在平均情况下,起泡排序的时间复杂度与最坏情况同数量级O(n^2)
起泡排序是一种稳定的排序方法

快速排序

快速排序是对冒泡排序的一种改进,改进的着眼点是:在冒泡排序中,记录的比较和移动是在相邻位置进行的,记录每次交换只能后移一个位置,因而总的比较次数和移动次数较多。而在快速排序中,记录的比较和移动是从两端向中间进行的。
快速排序涉及到选择基准轴值的问题,可以选择中间记录的关键码,或者在每次划分之前比较待排序序列的第一个记录,最后一个记录和中间记录的关键码,选取居中的关键码作为轴值并调换到第一个记录的位置
下面提供选取第一个记录和最后一个记录作为基准轴值的情形。

int Partition(int first, int end) {//此处选择数列第一个记录作为基准while (first < end) {while (first < end&&r[first] <= r[end])end--;//终点位置的记录满足大于基准记录,则终点位置前移if (first < end) {int tmp = r[first];r[first] = r[end];r[end] = tmp;first++;//基准位置交换后,r[first]=r[end]恒成立,故first++}while (first < end&&r[first] <= r[end])first++;//起点位置的记录满足小于基准记录,则起点位置后移if (first < end) {int tmp = r[first];r[first] = r[end];r[end] = tmp;end--;//基准位置交换后,r[first]=r[end]恒成立,故end--}//当取数列最后一个记录作为基准时,上面两个while循环交换顺序}return first;//循环退出时,必定有first=end=partition
}

可以看到,上面的代码中足足有六行是关于一个普通的交换语句。那么在每次确定partition的过程中,真的需要每次都进行first与end之间的交换吗?
如果每次仅是将待交换的二者之间进行赋值覆盖,并在最后覆盖赋值基准位置。那么是不是既可以缩小代码量,又可以提高一点时间性能?
下面是改进后的代码示例:

int Partition(int first, int end) {//此处选择数列第一个记录作为基准int tmp = r[first];while (first < end) {while (first < end&&r[first] <= r[end])end--;//终点位置的记录满足大于基准记录,则终点位置前移if (first < end) {r[first] = r[end];first++;//基准位置交换后,r[first]=r[end]恒成立,故first++}while (first < end&&r[first] <= r[end])first++;//起点位置的记录满足小于基准记录,则起点位置后移if (first < end) {r[end] = r[first];end--;//基准位置交换后,r[first]=r[end]恒成立,故end--}//当取数列最后一个记录作为基准时,上面两个while循环交换顺序}r[end] = tmp;return end;//循环退出时,必定有first=end=partition
}

经过上面一轮函数,可以让起点为first,终点为end的无序序列朝着整体有序的方向发展,并且与冒泡排序不同的是,冒泡排序进行过一趟后,将会确定一个数的位置(这个数一般是无序序列中的最大值或者最小值),并且将确定的数移动到相应位置(移动的位置一般是无序序列的起点或终点),而快速排序经过一趟后,同样能确定一个数的位置,这个数就是基准轴值,但是该数经过移动后其位置是不确定的,可以是任意位置。并且该位置左边的数必定小于该位置的数,该位置右边的数必定大于等于该位置的数(当然,如果你要从小到大排序的话,就按上面说的反着来即可
很显然,需要一个数记录基准经过排序后的位置,并且让该数左右的序列变得更加有序(前面只是向着总体有序的方向发展),并且千万不要再把基准位置的记录加入到任何左右序列中,否则会形成死循环。
下面是快排的递归算法:

int r[100];//排序数列
int n;//数列长度
void Quicksort(int first,int end) {if (first < end) {int pivot = Partition(first, end);//得到基准记录必定所处位置,且使得数列基准左端的记录都小于基准记录,数列基准右端的记录都大于基准记录Quicksort(first, pivot - 1);//end不能为pivot,否则会陷入死循环Quicksort(pivot+1, end);//first不能为pivot,否则会陷入死循环//上面两个递归可交换顺序,其相对顺序仅代表左右排序先后}
}

性能分析
快速排序的平均时间性能其数量为O(nlog2 n)
快速排序是一种不稳定的算法
快速排序适用于待排序记录个数很大且原始记录随机排列的情况。快速排序的平均性能是迄今为止所有排序算法中最好的一种,因此得到广泛的应用,例如UNIX系统的库函数qsort函数。

简单选择排序

以前在写代码时,涉及到排序,我总是会首先想到直接选择排序,因为直接选择排序算法十分简单,特别适合于对时间性能要求不高的情形。
以下是直接选择排序的代码示例:

int r[100];//排序数列
int n;//数列长度
void Quickselectsort() {for(int i=1;i<n;i++)for(int j=i+1;j<=n;j++)if (r[j] < r[i]) {int tmp = r[i];r[i] = r[j];r[j] = tmp;}
}

这个排序算法实在不想写一丁点注释
下面进入正文:
简单排序算法的主要思想是:每趟排序在当前待排序序列中选出关键码最小的记录,添加到有序序列中。
其相比于上面的快速选择排序,优化点在于不再多次进行交换,而是采取选出关键码最小的记录,最后才进行交换,每一趟排序下来确定一个数,并将该数移动到合适的位置。
下面是完整的代码:

int r[100];//排序数列
int n;//数列长度
void Selectsort() {for (int i = 1,index; i < n; i++) {//i表示待排点index = i;//index初始化为ifor (int j = i + 1; j <= n; j++)if (r[j] < r[index])index = j;//选出最小记录所对应的数组下标if (index != i) {//交换int tmp = r[i];r[i] = r[index];r[index] = tmp;}}
}

以上代码不难看出还是存在一点瑕疵,该算法需要一个空间存储index,从而选择出最小记录。
上面的代码创建了一个index变量,来实现上述操作,但是数组是从1开始存储的,且r[0]并没有作为哨兵,所以可以让r[0]存储index,记录最小记录的数组下标。
代码如下:

void Selectsort() {for (int i = 1; i < n; i++) {//i表示待排点r[0] = i;//index初始化为ifor (int j = i + 1; j <= n; j++)if (r[j] < r[r[0]])r[0] = j;//选出最小记录所对应的数组下标if (r[0] != i) {//交换int tmp = r[i];r[i] = r[r[0]];r[r[0]] = tmp;}}
}

性能分析
简单选择排序最好、最坏和平均的时间性能为O(n^2)
简单选择排序是一种不稳定的算法

堆排序

堆排序是简单选择排序的一种改进,改进的着眼点是:如何减少关键码的比较次数。简单选择排序在一趟排序中仅选出最小关键码,没有把一趟比较结果保存下来,因而记录的比较次数较多。堆排序在选出最小关键码的同时,也找出较小关键码,减少了在后面的选择中的比较次数,从而提高了整个排序的效率。

:其本质是一棵完全二叉树,并且它还满足每个节点的值都小于等于其左右孩子节点的值(称为小根堆)(即r[i]>r[2i]&&r[i]>r[2i+1]);或者每个节点的值都大于等于其左右孩子节点的值(称为大根堆)(即r[i]>r[2i]&&r[i]>r[2i+1])。
堆的每个节点按层序编号对应于数组下标存储于数组当中。

堆排序代码如下:

int r[100];//排序数列
int n;//数列长度
//适用于从下到上筛选调整,此处构造大根堆
void Sift(int s,int m) {//筛选调整堆,s表示待筛最高根节点int j = 2 * s;//令比较节点初始化为待筛节点的左孩子while (j <= m) {if (j < m&&r[j] < r[j + 1])j++;//令j为左右孩子中较大的那个孩子节点if (r[s] > r[j])break;//如果不用调整,即退出循环else {int tmp = r[s];//孩子节点与根节点进行交换r[s] = r[j];r[j] = tmp;s = j;j *= 2;}}
}
void Heapsort() {int i;for (i = n / 2; i >= 1; i--)Sift(i, n);//从下至上初始建堆for (i = 1; i < n; i++) {//i表示有序序列长度,n表示调整堆次数int tmp = r[1];//将堆顶记录(最大记录)加入到有序序列的始端r[1] = r[n - i + 1];//并将无序序列的末端记录移动至无序序列的始端r[n - i + 1] = tmp;Sift(1, n - i);//以第一个节点作为调整堆的最高根节点}
}

两个函数共同实现堆排序,
第一个筛选函数对数组进行筛选调整,
值得一提的是,该函数以某个根节点为起始,然后实现单路径的调整,即若以该节点向下的所有子孙节点是完全无序随机的,那么用这个函数并不能实现真正的调整功能,所以该函数有一个特性就是必须至下向上使用,至于是大根堆还是小根堆只要改变if条件判断即可。
第二个函数可以比喻成一双操控之手,用它来合理调用筛选函数,从而实现真正的筛选调整功能,首先,对原始数组进行一遍至下向上的调整,然后取下大根堆的堆顶元素放入有序数列的始端(堆顶元素即根堆的第一个记录
因为有了初始建堆的基础,所以即使将堆顶元素取下,拿最后一个叶子节点进行替换。根堆除第一个元素外仍然是一个正确的根堆,所以只需要以第一个元素为最高根节点调整一次根堆即可得到一个新的正确的根堆。然后依次取出根顶元素。
选择排序的缺点就在于两次相邻的选择之间没有任何的联系,即一次选择完成后并不能有助于下一次选择的进行。
而堆排序恰恰完善了这一点,每一次调整堆都能够得到一个选择出来的堆顶记录,取出堆顶记录后,下一次的选择是和前一次的选择是有联系的,或者说前一次选择为下一次选择做好了基本准备,而所谓的第一次基本准备发生于初始建堆。

性能分析
堆排序最好、最坏和平均的时间性能为O(nlog2 n)
堆排序对原始记录的排列状态并不敏感,相比于快速排序,这是堆排序最大的优点。
堆排序是一种不稳定的排序方法

归并排序

归并:将两个或两个以上的有序序列归并成一个有序序列的过程
具体的排序过程是:将具有n个待排序的记录序列看成是n个长度为1的有序序列,然后进行两两归并…直至得到一个长度为n的有序序列。
设两个相邻的有序序列为r[s]~r[m]和r[m+1]到r[t],将这两个有序序列归并成一个有序序列r1[s]到r1[t]。
下面给出具体的一次归并算法

void Merge(int r[],int r1[],int s,int m,int t)//一次归并算法
{int i = s, j = m + 1, k = s;while (i <= m && j <= t) {if (r[i] <= r[j])r1[k++] = r[i++];//取r[i]和r[j]中较小者,放入有序序列末端。else r1[k++] = r[j++];//直到归并的两段序列中有一段已经全部放入新序列}while (i <= m)r1[k++] = r[i++];//剩下的一段序列必定有序,且均大于有序序列while (j <= t)r1[k++] = r[j++];//故将剩下的一段序列全部依次放入新序列的末端
}

一趟归并算法的代码如下:

void MergePass(int r[],int r1[],int h)//一趟归并排序算法
{int i = 1;//初始化i为1while (i <= n - 2 * h + 1) {//当以i为起点,数列后面不存在两个完整的子序列时,退出循环Merge(r, r1, i, i + h - 1, i + 2 * h - 1);//将两个子序列进行归并i += 2 * h;//i后移}if (i < n - h + 1) Merge(r, r1, i, i + h - 1, n);//如果以i为起点,数列后面存在一个完整的子序列和一个不完整的子序列时,将二者进行归并else while(i<=n)//否则以i为起点,数列后面必定为一个序列(可能不完整也可能完整)r1[i] = r[i++];//将该序列加入到新序列的末端
}

归并排序非递归算法

void Mergesort1()//归并排序非递归算法
{int h = 1;//初始化h为1while (h < n) {//数组一来一回,最后需要的结果是r中的记录有序,所以每次循环进行两趟mergepassMergePass(r, r1, h);//以h为子序列长度,将数组r两两归并加入到新数组r1上h *= 2;//序列长度每次扩大两倍MergePass(r1, r, h);//以2*h为子序列长度,将数组r1两两归并加入到数组r上h *= 2;//序列长度再次扩大两倍}//退出循环时,r数组必定有序,r1不一定
}

性能分析
非递归的归并排序最好、最坏和平均的时间性能为O(nlog2 n)
算法在归并过程中需要与待排序记录序列同样数量的存储空间,以便存放归并结果,因此其空间复杂度为O(n)
归并排序是一种稳定的排序方法

显然,归并排序相比于前面的几种排序代码十分繁杂,一共写了三个函数,这还不包括主函数的输入部分。
当我们因为代码量焦头烂额时,我们可能会很快想到使用递归从而减少代码量。
容易看出归并排序的非递归实现实际上是一种自底向上的方法,而使用递归的话,应当是一种自顶向下的分治法。

所以归并排序的递归可以按下面的步骤得以实现:
首先使用递归不断的对数组实行分治,直到不可再分(这个条件同样也是递归的终止条件)
然后在递归函数的最后增加一个合并函数,使得当分治完成后,能够开始进行正确的合并。

这里有一个很重要的注意点:非递归的归并算法中局部排序后的数列不断的在r1和r上相互进行交换存储,并且最后最完整的排序数列会位于数组r上。
即在非递归的算法中,数组r和r1是相互的“排序介质容器”,也就是说数组r要经过一次排序,必须要借助数组r1,而数组r1要经过一次排序,也必须要借助数组r,而这并没有所谓的主次关系,只是最后main函数中输出的是数组r中的记录。所以在循环中才要进行两次merge。
而递归算法则不同,如果不对r和r1制造明确的主次关系,那么最后在main函数中要想输出正确的排序记录是具有不确定性的。所以要使得r1为确定的“排序介质容器”,并且要让r1成为“与时俱进”的“排序介质容器”
即在每一次合并之前对将要合并的记录区间进行复制,让其复制到数组r1上,然后再以数组r1为基础进行归并排序,且将正确结果置于数组r中,这样就实现了r1为“与时俱进”的“排序介质容器”。
然后在递归的回溯中让r不断的归并,进而朝有序发展。

具体的代码如下:

void Merge(int r[],int r1[],int s,int m,int t)//一次归并算法
{int i = s, j = m + 1, k = s;while (i <= m && j <= t) {if (r[i] <= r[j])r1[k++] = r[i++];//取r[i]和r[j]中较小者,放入有序序列末端。else r1[k++] = r[j++];//直到归并的两段序列中有一段已经全部放入新序列}while (i <= m)r1[k++] = r[i++];//剩下的一段序列必定有序,且均大于有序序列while (j <= t)r1[k++] = r[j++];//故将剩下的一段序列全部依次放入新序列的末端
}void Mergesort2(int s,int t)//归并排序的递归算法
{if (s == t)return;//递归终止条件else {int m = (s + t) / 2;//二分Mergesort2(s, m);//左归并Mergesort2(m + 1, t);//右归并for (int i = s; i <= t; i++)r1[i] = r[i];//复制要进行合并的数列于r1上Merge(r1, r, s, m, t);//合并}
}

性能分析
归并排序的非递归实现算法效率较高,但可读性较差。
递归实现形式更为简洁,但效率相对来说较差

桶式排序

前面介绍的排序方法都是建立在关键码比较的基础上,分配排序是基于分配和收集的排序方法。
其基本思想是:先将待排序记录序列分配到不同的桶里,然后再把各桶中的记录依次收集到一起
桶式排序是一种简单的分配排序,其基本思想是:假设待排序记录的值都在0~m-1之间,设置m个桶,首先将值为i的记录分配到第i个桶中,然后再将各个桶中的记录依次收集起来。

首先来讨论桶的数据结构
因为一个桶可能存在0~n个记录,即存在空桶和满桶的情况,所以我们会立刻想到“队列”,用队列作为桶,效果当然非常好,但是这其中却存在一个很严重的问题,就是队列不具有普适性,即某些语言不存在队列的包或库,然而好的算法却必须具有普适性。然后我们可能会想到自定义建立链队列或者静态队列,但是建立多个链队列并且对其进行不断的维护在一个排序算法中的代价太高了,如果建立静态队列的话,那么每个静态队列的大小又必须至少为n,这样又对空间性能造成了巨大的负担。
那么我们考虑是否可以建立一个“形式队列”或者“虚拟队列”,即该队列本身仅保存队首和队尾记录所在位置,却又能通过该队列对队列中的“虚拟元素”进行队列链接。

struct QueueNode {int front;int rear;
};

所以这里面又涉及到每个记录的数据结构
想要仅通过虚拟队列实现队列的效果是很难实现的,但是如果记录的数据结构与虚拟队列的数据结构具有相容性,甚至能相互配合,那么这一切就都不再成为问题。
所以可以用静态链表存储记录

struct Node {int key;int next;
};

其中利用虚拟队列让记录实现虚拟链接,进而形成一个形式队列是该问题中最关键的算法
代码如下:

int i=0,k=r[0].key;//初始化while(i<n) {if (q[k].front == -1) q[k].front = i;//处理队列为空的情况else r[q[k].rear].next = i;//处理队列非空的情况q[k].rear = i;//更改尾指针k = r[++i].key;//静态链表后移}

代码注释:如果队列为空,就直接把记录的位置放进静态队列的front中,如果队列非空,就让队列的队尾元素指向该记录位置,从而实现“虚拟链接”。然后更改队尾指针为当前记录的位置。

经过一遍遍历以及“虚拟链接”,各桶中要么为空,要么front以及rear都有值,而借助队列实现的“虚拟链接”,只能局限在单个桶中,即每个桶中的“虚拟元素”是“虚拟链接”的,但是不同桶间,不同有记录的桶之间是没有任何关联的,所以还必须有一项收集操作

所谓的收集处理:就是建立桶之间的联系,这里的记录的数据结构–静态链表,也担任了不可替换的角色,更准确一点来说,应该是建立有记录的桶之间的联系,这时候虚拟队列的数据结构又恰恰相辅相成。
具体的方法就是:让前一个有记录的桶的尾指针所指向的元素的下一个元素为当前桶的第一个记录的位置(可能听起来有点绕口吧 )

这个步骤的代码如下:

int k,pre=-1;//初始化for (k = 0; k < m; k++) {//遍历桶while (k < m&&q[k].front==-1)k++;//找到下一个非空队列if (pre != -1)r[pre].next = q[k].front;//连接存储不同大小记录的静态链表if (pre == -1)ip=q[k].front;//保存数列中最小记录第一次出现的位置pre = q[k].rear;//记录下尾指针所在的位置以便连接下一个静态链表}if(pre!=-1)r[pre].next = -1;//处理m-1处静态链表有值的情况

在本例中桶的数量为m,m为数组中最大记录+1。起始段从0开始(因为样例给的最小值为0)

所以该算法如果不能确定数列中的最大记录(更准确一点来说是记录的最大关键码),那么使用基数排序消耗(浪费)的空间性能十分巨大!

使用基数排序更恰当的方式是
1.在得知了待排序序列的最大值和最小值时(并且二者相差不大或者数列中的数在两个界限之间分布比较均匀)
2.能够在序列中记录本身的值与桶的“标签”之间建立一个合适的函数,从而大大提高空间利用率,减少空间复杂度。

整个桶式排序的完整代码如下:

#include<iostream>
using namespace std;
struct Node {int key;int next;
};
struct QueueNode {int front;int rear;
};
int m,n,ip;//ip:initposition
Node r[100];
QueueNode q[100];
//改进分配函数
void Distribute() {int i=0,k=r[0].key;//初始化while(i<n) {if (q[k].front == -1) q[k].front = i;//处理队列为空的情况else r[q[k].rear].next = i;//处理队列非空的情况q[k].rear = i;//更改尾指针k = r[++i].key;//静态链表后移}
}
//自定义收集函数
void Collect() {int k,pre=-1;//初始化for (k = 0; k < m; k++) {//遍历桶while (k < m&&q[k].front==-1)k++;//找到下一个非空队列if (pre != -1)r[pre].next = q[k].front;//连接存储不同大小记录的静态链表if (pre == -1)ip=q[k].front;//保存数列中最小记录第一次出现的位置,ip可能出现bugpre = q[k].rear;//记录下尾指针所在的位置以便连接下一个静态链表}if(pre!=-1)r[pre].next = -1;//处理m-1处静态链表有值的情况
}
void Bucket() {for (int i = 0; i <= m; i++)q[i].front = q[i].rear = -1;//初始化队列Distribute();Collect();
}
int main() {cin >> m>> n;for (int i = 0; i < n; i++) {cin >> r[i].key;r[i].next = i + 1;}r[n - 1].next = -1;//初始化Bucket();for (; r[ip].next != -1; ip = r[ip].next) //输出cout << r[ip].key<<" ";cout << r[ip].key << endl;return 0;}
/*
13 10
7 1 4 1 8 1 5 2 3 10
9 8
3 5 3 1 5 6 3 8
—— 预期输出 ——1 2 3 4 5 6 7 8 9 1051 15
3 44 38 5 47 15 36 26 27 2 46 4 19 50 48—— 预期输出 ——2 3 4 5 15 19 26 27 36 38 44 46 47 48 50
*/

上面的代码只是一个雏形的模板,真正要完善桶式排序,还需要得到或者输入桶的最大值、最小值(遍历查找数列得到最大值和最小值也不是那么不可接受

性能分析
桶式排序的时间复杂度为O(n+m)
空间复杂度是O(m),用来存储m个静态队列表示的桶
由于桶采用队列作为存储结构,因此,桶式排序是一种稳定的排序方法

基数排序

基数排序也是一种分配排序,是借助对多关键码进行桶式排序的思想对单关键码进行排序
又分为最主位优先MSD和最次位优先LSD

基数排序实质上也是对桶式排序的一种优化
因为刚刚在上面提到过,如果不能确定给定数列中记录的最大值、最小值,或者内部元素分布稀疏,再或者最大值和最小值相差巨大,都很容易造成空间资源的浪费。
而基数排序恰恰优化了这一点,从而能够实现超大位数的排序(这个超大位数是可以超过8个字节的)
它把每个数存储在一个数组中,即每一位都是数组的一个元素,然后再使用最次位优先进行关键码排序

所以使用基数排序时,面临的新问题是:
位数如何确定?
这个也没有什么特别好的解决办法,不过可以使用暴力解法,比如设定最大位数为5,10,20,就基本可以解决这个问题,浪费的空间资源也不是很大,时间复杂度也没有加大太多

完整的代码如下:

#include<iostream>
using namespace std;
const int d = 5;//输入中最大记录的位数
struct Node {int key[d];int next;
};
struct QueueNode {int front;int rear;
};
int m=10, n, ip;//ip:initposition
Node r[100];
QueueNode q[100];
//改进分配函数
void Distribute(int j) {int i=ip, k = r[ip].key[j];//初始化while (r[i].next!=-1) {if (q[k].front == -1) q[k].front = i;//处理队列为空的情况else r[q[k].rear].next = i;//处理队列非空的情况q[k].rear = i;//更改尾指针i = r[i].next;k = r[i].key[j];//静态链表后移}if (q[k].front == -1) q[k].front = i;//处理队列为空的情况else r[q[k].rear].next = i;//处理队列非空的情况q[k].rear = i;
}
//自定义收集函数
void Collect() {int k, pre = -1;//初始化for (k = 0; k < m; k++) {//遍历桶while (k < m&&q[k].front == -1)k++;//找到下一个非空队列if (pre != -1)r[pre].next = q[k].front;//连接存储不同大小记录的静态链表if (pre == -1)ip = q[k].front;//保存数列中最小记录第一次出现的位置,ip可能出现bugpre = q[k].rear;//记录下尾指针所在的位置以便连接下一个静态链表}if (pre != -1)r[pre].next = -1;//处理m-1处静态链表有值的情况
}
void Bucket() {for (int j = d - 1; j >= 0; j--) {for (int i = 0; i <= m; i++)q[i].front = q[i].rear = -1;//每一次分配前都要初始化队列Distribute(j);Collect();}
}
int main() {int value;cin >> n;for (int i = 0; i < n; i++) {cin >> value;for (int j = 1; value > 0;value/=10,j++) r[i].key[d-j]=value%10;//将每一个输入拆位放入数组r[i].next = i + 1;//初始化}r[n - 1].next = -1;Bucket();for (; r[ip].next != -1; ip = r[ip].next) //输出{value = r[ip].key[0];for (int i = 1; i < d; i++)value = value * 10 + r[ip].key[i];//将数组的位合为一个数cout << value << " ";}value = r[ip].key[0];for (int i = 1; i < d; i++)value = value * 10 + r[ip].key[i];cout << value << endl;return 0;
}
/*
//输入样例
10
7 1 4 1 8 1 5 2 3 108
3 5 3 1 5 6 3 815
3 44 38 5 47 15 36 26 27 2 46 4 19 50 48—— 预期输出 ——2 3 4 5 15 19 26 27 36 38 44 46 47 48 50
*/

可以看到,我对代码中的分配函数进行了更改,更改的方向是循环条件发生了变化,因为有多个关键码,需要进行多次分配和收集,所以不能再像桶式排序那样将数组大小作为判断依据(其实在桶式排序中队列的初始化都是不必要的 )
里面还有一个比较重要的全局变量ip(initpositon,当然,你也可以设定为更加安全的局部变量,不过这样就需要函数传参了),这个变量用来保存第一个有值的桶的第一个记录,即链表头,在最后的输出,以及每一趟的分配中都担任了不可或缺的角色。

性能分析
基数排序的时间复杂度为O(d(n+m))
空间复杂度为O(m)
由于桶采用队列作为存储结构,因此基数排序是稳定的。

排序系列(代码c++版)相关推荐

  1. 经典十大排序算法(含升序降序,基数排序含负数排序)【Java版完整代码】【建议收藏系列】

    经典十大排序算法[Java版完整代码] 写在前面的话 十大排序算法对比 冒泡排序 快速排序 直接选择排序 堆排序 归并排序 插入排序 希尔排序 计数排序 桶排序 基数排序 完整测试类 写在前面的话   ...

  2. 睡眠排序法-objective C版的代码

    将开发过程比较重要的代码做个珍藏,下面代码内容是关于睡眠排序法-objective C版的代码,应该能对各位朋友有帮助. @interface NSArray (SleepSort) - (void) ...

  3. [分类整理IV]微软等100题系列V0.1版:字符串+数组面试题集锦

    微软等100题系列V1.0版整理IV:字符串+数组面试题集锦 July   2010年12月30日 第4章 字符串+数组面试题 在微软等100题系列V0.1版中,此类字符串+数组的问题,占了足足22道 ...

  4. 中希尔排序例题代码_【数据结构与算法】这或许是东半球分析十大排序算法最好的一篇文章...

    码农有道 历史文章目录(请戳我) 关于码农有道(请戳我) 前言 本文全长 14237 字,配有 70 张图片和动画,和你一起一步步看懂排序算法的运行过程. 预计阅读时间 47 分钟,强烈建议先收藏然后 ...

  5. 中希尔排序例题代码_超全面分析十大排序算法

    点击上方"零一视界",选择"星标"公众号 资源干货,第一时间送达 作者 | 不该相遇在秋天 责编 | 程序员小吴 前言 本文全长 14237 字,配有 70 张 ...

  6. OpenKE实现转移距离模型trans系列代码

    OpenKE实现转移距离模型trans系列代码 前言 前段时间学习了知识图谱表示的转移距离模型trans系列大礼包,编辑这篇博客的起因是一个学妹找我要trans系列的代码,所以就在周日的下午来回忆一下 ...

  7. 嗜血代码软件测试,噬血代码steam版

    <噬血代码steam版>(Code Vein)是一款探索动作RPG游戏,由<噬神者>团队Bandai Namco采用虚幻4引擎开发.噬血代码由<噬神者>系列总制作人 ...

  8. 经典的十种排序算法 C语言版

    经典的十种排序算法(C语言版) 1.冒牌排序 冒牌排序的特点 ​ 一趟一趟的比较待排序的数组,每趟比较中,从前往后,依次比较这个数和下一个数的大小,如果这个数比下一个数大,则交换这两个数,每趟比较后, ...

  9. 浙江高考VB之排序系列

    浙江信息技术Giao考之 "排序系列" 在浙江高考中, 排序算法 是一个大头, 下至冒泡选择, 上至桶排索引. 这里我们大致梳理一遍高考可能考到的排序算法. 排序算法有哪些? 在算 ...

  10. 一加7t人脸识别_一加7T系列国行版开启预约 谷歌Pixel 4系列高清图曝光

    据一加手机官方消息,一加7T系列国行版已经开启预约,全新系列将于10月15日正式发布. 一加7T采用6.55英寸,分辨率为2400×1080的AMOLED显示屏,具有90Hz刷新率.峰值亮度为1000 ...

最新文章

  1. 前端知识点(持续更新)
  2. NeurIPS 2019最佳论文出炉,今年增设“新方向奖”,微软华人学者获经典论文奖...
  3. 传统制造业面临大数据的7种改变方式
  4. 想要摆脱手工报表困境?1个工具+5个场景解决80%工作难题
  5. 如何发布第一个属于自己的npm包
  6. xlsx文件打开乱码_excel表格文件打开都是乱码怎么解决
  7. day32,尚硅谷视频学习中
  8. BAAF-Net源码阅读
  9. 【DSP】TMS320C64x系列--SPRU871参考手册--中断控制器部分
  10. 直观理解图片的EXIF orientation
  11. 第107章 SQL函数 $PIECE
  12. 手机破解并连接WiFi后,可以通过USB数据线与电脑共享WiFi
  13. ThinkPHP3.2.3手册阅读
  14. Oracle 10g RAC 维护工具完全详解
  15. 【MAUI】条形码,二维码扫描功能
  16. U-Net实现医学图像分割(pytorch)
  17. QT 版本号识别 不同系统区分
  18. 全球著名大学计算机视觉相关实验室
  19. ClassNotFoundException: org.apache.spark.AccumulatorParam 解决方案
  20. SpringBoot + Vue 个人网站接入QQ登录手把手教你 完整版 新手友好

热门文章

  1. [堆入门off-by-null]asis2016_b00ks
  2. 拨号连接显示服务器断开连接,弹出拨号连接的解决方法【详细介绍】
  3. git 解决push报错:[rejected] master -> master (fetch first) error: failed to push some refs to ‘ ‘
  4. 微型打印机方案(包含原理图、PCB和BOM表)
  5. 【华人学者风采】蔡达成 新加坡国立大学
  6. 极坐标xy的转换_极坐标与直角坐标的转化
  7. 如何撤回 Gmail 已发送的邮件
  8. PCB十六大可靠性测试,看看您的板是否经得起测试?
  9. 用计算机处理表格信息教案,表格信息的加工与表达教案
  10. Nebula Graph - 基于Docker 安装 及 Studio