排序:计数、选择、冒泡、插入、基数、稳定

计数排序
计数排序是利用元素在序列中的名次,将元素移动到与其名次对应的位置。
我们先来看看代码:

template <class T>
void rank(T a[],int n,int r[])
{//给数组a排出名次,写到数组r中
for(int i=0;i<n;i++)
r[i]=0;
//比较所有元素对
for(int i=0;i<n-1;i++)
for(int j=i+1;j<n;j++)
if(a[j]>=a[i]) r[j]++;
else r[i]++;
}
template <class T>
void rearrange(T a[],int n,int r[])
{//使用一个附加数组使元素排序
T *u[] = new T[n];//创建附加数组

for(int i=0;i<n;i++)//将a中的元素按顺序排入数组u中
u[i] = a[r[i]];
for(int i=0;i<n;i++)//将u中的元素复制进数组a中
a[i] = u[i];
delete []u;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
假设调用new操作符分配空间的操作是成功的,那么执行函数rearrange,完成排序需要(n-1)n/2次比较与2n次移动,复杂度是Q(n2)。
如果我们不想使用附加数组,那我们来试试原地重排,代码如下:

template <class T>
void rearrange2(T a[],int n,int r[])
{//原地重排
for(int i=0;i<n;i++)
while (r[i]!=i)
{
swap(a[i],a[r[i]]);
swap(r[i],r[r[i]]);
}
}
1
2
3
4
5
6
7
8
9
10
在优化后的算法中,我们易知,交换次数最少为0,最多为2(n-1)(应为每次交换会使得至少一个元素在正确的位置上,所以会交换(n-1)次),由此可见,优化后的算法复杂度为Q(n-1),而且程序所需内存减少了。

选择排序
选择排序是指首先找到最大的元素,把它移到最高位,然后在找到其余元素中最大的元素,把它移到次高位。如此进行下去,直到剩下一个元素。
看看代码吧:

template <class T>
int indexOfMax(T a[],int n)
{//查找数组的最大元素
int indexOfMax = 0;
for(int i = 1;i<n;i++)
if(a[indexOfMax]<a[i])
indexOfMax = i;
return indexOfMax;
}
template <class T>
void selectionSort(T a[],int n)
{//选择排序
for(int i=n;i>1;i--) {
int j = indexOfMax(a, i);
swap(a[j], a[i - 1]);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在这个算法中,我们需要比较(n-1)n/2次,元素的移动次数3(n-1)次,这样我们的比较次数与计数排序相同,但是移动次数更多,那么我们为了降低时间复杂度,我们看下一种选择排序算法:

template <class T>
void selectionSort2(T a[],int n)
{//及时终止的选择排序
bool sort = false;//判断数列是否有序
for(int i=n;!sort&&(i>1);i--){
int indexOfMax = 0;
sort = true;
//查找最大的元素
for(int j=1;j<i;j++) {
if (a[indexOfMax] <= a[j]) indexOfMax = j;
else sort = false;
}
swap(a[indexOfMax],a[i-1]);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对于及时终止的选择排序,最好的情况是一开始数组元素有序,这是外部的for循环只运行一次,数组元素的比较次数为(n-1)次,最坏的情况是外部的for循环直到i=1使才停止,比较次数为(n-1)n/2。

冒泡排序
冒泡排序是一种简单的排序算法,它使用一种“冒泡策略”,把最大的元素一道序列的最右端。
在一次冒泡中,,相邻的元素比较,如果左边的元素大于右边的元素,则交换,反之,无需交换。
一次冒泡完成后,我们得到一个新的序列,在这个序列中,最大的元素已经到了序列最右端,我们只需要将除去最大数的序列再进行冒泡过程,反复至最后一个元素,则排序完成。
我们来看看代码:

template <class T>
void bubbleSort(T a[],int n)
{//对数组元素使用冒泡排序
for(int i=n;i>1;i--){
for(int j=0;j<i-1;j++)
if(a[j]>a[j+1])
swap(a[j],a[j+1]);
}
}
1
2
3
4
5
6
7
8
9
这段代码结构十分的简单,嵌套的循环为一个序列的一次冒泡过程。在冒泡排序中,长度为n的序列,我们共需要比较n(n-1)/2次,所需交换次数最多为n(n-1)/2次,最少为0次。时间复杂度为Q(n2)。

插入排序
插入排序是采用一中插入策略,通过例子我们应该会比较好理解:
我们将3这个数插入有序数列:2,4,6,8,9,12,15;我们会如何做呢?我们通过数组基本的操作遍历,从数组最右端开始连续把一些元素向右移动一些位置,知道为新元素找到位置,将元素放入就可以了。在我们这个数组中我们要将2以后的数字向后移动一位,然后将3插入第二个位置。
当我们明白了一次插入的方法后,就要考虑插入排序了。我们先来看看代码:

template <class T>
void insertSort(T a[],int n)
{
for(int i=1;i<n+1;i++){
T t = a[i];
int j;
for (j = i-1; j >= 0 && t < a[j]; j--)
a[j+1] = a[j];
a[j+1] = t;
}
}
1
2
3
4
5
6
7
8
9
10
11
在这段代码中,我们明显的可以看到,我们将一个数字取出,虽然开始时为乱序,但我们的插入策略是插入到比自己打的数前面,具体实现为:

for (j = i-1; j >= 0 && t < a[j]; j--)
1
但我们将所有的数字遍历完成后,就会变为有序的数列。
现在我们来进行程序性能分析,在这种排序方法中,最好的比较次数是n-1,最坏的比较次数是n(n-1)/2,数组的移动最坏为n(n-1)/2,效率相对其他集中来讲比较低,但是逻辑简单,特别是一次有序插入,用处比较多。

基数排序
基数排序实际上是对数据结构中箱子排序的扩展,时间复杂度仅为Q(n),就可以对0~nc之间的n个整数进行排序,它不直接对数进行排序,而是把数按照某个基数分解为数,例如我们说1024=1103+0102+2*10+4;就是把1024按照10为基数进行排序。为了更好地进行理解,我们来看一道例题:
无需数列: 99,1,28,64,1024,2346,57,3;
1.我们要利用基数排序将其变为由小到大的有序数列,那我们先从低位由基数取数字,分别为:9,1,8,4,4,6,7,3,将其对应的数排序后得:1,3,64,1024,2346,57,28,99;
2.之后,我们在取十位的数字:0,0,6,2,4,5,2,9;将其对应得数进行排序可得:1,3,1024,28,2346,57,64,99;
3.然后,我们再取百位的数字:0,0,0,0,3,0,0,0,将其对用的数进行排序可得:
1,3,1024,28,57,64,99,2346;
4,最后,大家想必也猜到了,取千位的数字:0,0,1,0,0,0,0,2;将其对应的数进行排序:1,3,28,57,64,99,1024,2346
最终,我们得到了有序的数列。
在基数排序中,我们的基本思路就是将0~9作为十个桶,将位上的数字分别放入这十个桶中再取出,通过每一位的排序,最终可以得到有序的序列。
接下来,我们来看看代码:

int bitMaxNum(int a[],int n)
{
int maxNum = 0;
for(int i=0;i<n;i++){
int temp = a[i];
int max = 0;
while (temp!=0){
temp=temp/10;
max++;
}
if(max>maxNum)
maxNum = max;
}
return maxNum;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个函数是用来确定数列中数的最大的位数,那么我们再来看看基数排序的函数部分:

void bucketSort(int a[],int bitMaxNum,int n)
{
int bit = 1;//确定计算的数位
for(int i=0;i<bitMaxNum;i++){
int temp = 0;
int bucket[10][n];//定义十个桶
int count[10];//每个桶中元素个数
for(int j=0;j<10;j++)//初始化
count[j] = 0;
for(int j=0;j<10;j++)//初始化
for(int k=0;k<n;k++)
bucket[j][k] = 0;
for(int j=0;j<n;j++){
int num = a[j]/bit;//确定数位
int bucketNum = num%10;//取该位上的数字
bucket[bucketNum][count[bucketNum]] = a[j];//放入桶
count[bucketNum]++;
}
for(int j=0;j<10;j++){//从桶中取出数
if(count[j]>0)
for(int k=0;k<count[j];k++){
a[temp] = bucket[j][k];
temp++;
}
}
bit = bit*10;//取更高位
}
}
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
由此,我们可以完成了基数排序。

稳定排序

先来讲讲稳定排序的概念:当我们说一个排序算法能保证同值元素的相对位置不变,我们就说这个排序算法是稳定的。通俗的说,例如a1=a2 ,排序完成后,a1,a2的相对顺序没有变,那么这个排序算法是稳定的。
那我们来看看我们刚说的几种排序算法:
基数排序,冒泡排序无疑是稳定的,而选择排序,计数排序,插入排序是不稳定的。
当然了,事无绝对,我们对算法进行改进,不稳定的算法也可以变为稳定的,这里就不多赘述了。
————————————————
版权声明:本文为CSDN博主「小那猿同学」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43714332/article/details/102592056

posted @ 2019-12-09 15:50  Hello坚果  阅读(365)  评论(0编辑  收藏  举报