C语言实现八大排序算法(二)

在这篇博文中,我们介绍了剩下4种排序算法,并将对所有的排序算法做一个总结。代码主要参见数据机构之十大排序,关于拓展里面的桶排序和计数排序,只了解了想法,并未自己实现,不过也给出了参考资料中别人的代码实现。

选择排序(Selection Sort)

基本思想

在要排序的一组数中,选出最小(或者最大)的一个数与第$1$个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第$2$个位置的数交换,依次类推,直到第$n-1$个元素(倒数第二个数)和第$n$个元素(最后一个数)比较为止。

算法流程

  1. 初始时,数组全为无序区$a[0, … , n-1]$, 令$i=0$;
  2. 在无序区$a[i, … , n-1]$中选取一个最小的元素与$a[i]$交换,交换之后$a[0, … , i]$即为有序区;
  3. 重复步骤2,直到$i=n-1$,排序完成。

复杂度分析时间复杂度为$O(n^2)$,空间复杂度为$O(1)$,不稳定

动态实例参见: https://img-blog.csdnimg.cn/20181108191520166.gif

算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//选择排序,以升序为例
void SelectSort(int a[],int n){
int i,j,min;
for(i=0;i<n-1;i++){
min=i;
for(j=i+1;j<n;j++){
if(a[j]<a[min)
min=j;
}
if(min!=i)
Swap(a[i],a[min]);
}
}
void Swap(int a,int b){
int temp;
temp=a;
a=b;
b=temp;
}

堆排序(Heap Sort)

堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点

  1. 小根堆:$L(i) \le L(2i)$且$L(i) \le L(2i+1)$
  2. 大根堆:$L(i) \ge L(2i)$且$L(i) \ge L(2i+1)$

其中($1 \le i \le \lfloor n/2 \rfloor$)

堆排序常用于解决top-k问题。

基本思想(以大根堆为例)

  1. 初始化堆: 将数列$a[1,…,n]$构造成大顶堆(即根结点最大,左右结点均小于根结点);
  2. 交换数据: 将$a[1]$(根结点)和$a[n]$(最后一个数据,不一定是最小值)互换,使得$a[n]$是数列中的最大值; 然后再将$a[1,…,n-1]$构造大顶堆,交换$a[1]和a[n-1]$; 再将$a[1,…,n-2]​$构造成大顶堆,……,直到剩下一个元素,就是序列的最小值; 此时整个序列就按从小到大排列好了。

这里的大顶堆是以数组形式存储的,按层(行)存储,第一个元素是顶堆,第2个元素是左子树第一个分支结点,第3个元素是右子树第一个分支结点,第4个元素是第3层左边第一个结点……,有如下性质(根结点的索引是0):

  1. 性质一:索引为$i$的左孩子的索引是 $(2*i+1)$;
  2. 性质二:索引为$i$的右孩子的索引是 $(2*i+2)​$;

复杂度分析时间复杂度为$O(nlog_2^n)$,空间复杂度为$O(1)​$,不稳定

一个大根堆的建立过程如下:

动态实例参见(大根堆): https://img-blog.csdnimg.cn/20181108192721466.gif

算法实现

要想实现堆排序,必须考虑2个方面:

  1. 如何建立大根堆?
  2. 大根堆建立好后,第一次输出堆顶元素后,堆被破坏,如何调整建立新的大根堆?
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
//堆排序方法一,主要包含2部分:创建堆;输出堆顶元素后重新调整堆
//交换函数
void Swap(int array, int i, int j)
{
int tmp;
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
}

//创建大根堆
void MaxHeapCreat(int array[], int heapSize)
{
int i;
for(i = heapSize/2; i >= 0; i--)
{
MaxHeapify(array, heapSize, i);
}

/*大根堆调整*/
void MaxHeapify(int array[], int heapSize, int currentNode)
{
int leftChild, rightChild, largest;
leftChild = 2*currentNode + 1;
rightChild = 2*currentNode + 2;
largest = currentNode;
if(leftChild < heapSize && array[leftChild] > array[currentNode])
largest = leftChild;
if(rightChild < heapSize && array[rightChild] > array[largest])
largest = rightChild;
if(largest != currentNode)
{
Swap(array, largest, currentNode);
MaxHeapify(array, heapSize, largest);
}
}
//堆排序
void HeapSort(int array[],int heapSize){
MaxHeapCreat(array,heapSize);
//直接输出
for(i = 0; i < heapSize; i++)
{
printf("%d\t", array[i]);
}
}

-------------------------------------------------------------------------------------------

//堆排序方法二
#include<stdio.h>
#include<stdlib.h>

//数据交换
void Swap(int *a, int* b){
int temp = *a;
*a = *b;
*b = temp;
}

//构造大顶堆
void heap_down(int* arr,int i, int N){
int child;
int temp;
for(temp=arr[i]; 2*i+1<N;i=child){ //temp是当前结点的值
child = 2*i+1; //当前结点i的左孩子的位置(在数组中的下标)
if(child!=N-1 && arr[child+1]>arr[child]){ //child+1是当前结点的右孩子的位置,判断右孩子是否大于左孩子
child++;
}
if(temp<arr[child]){ //判断根结点是否小于它的左右两个跟结点,如果小于,则交换大的为根结点
arr[i]=arr[child];
arr[child]=temp;
}
else{
break;
}
}
}


//堆排序
void heap_Sort(int *arr, int length){
int i;
//从length/2到0依次遍历,最终得到的数组是一个大顶堆
//最后叶子结点2×i+2=length,i=length/2-1是最后一个父节点
for(i=length/2;i>=0;i--){
heap_down(arr, i, length);
}
for(i=length-1;i>0;i--){
Swap(&arr[0],&arr[i]); //交换最大值a[0]到队列末尾
heap_down(arr,0,i); //执行去掉最大值的[0~n-1]个元素的大顶堆,
}
}

//打印元素
void printArr(int *arr,int length){
for(int i=0;i<length;i++){
printf("%d\n",arr[i]);
}
}

int main(){
int arr[]={3,5,2,10,8,9,6,4,7,19,5,43,56,3,24,98,76,123,456,76,432,987,12};
int length = sizeof(arr)/sizeof(int);
heap_Sort(arr,length);
printArr(arr,length);
return 0;
}

小根堆和大根堆类似,此处就不细说了。

归并排序(Merge Sort)

归并概念: 将两个的有序数列合并成一个有序数列,我们称之为”归并”(二路归并)。

基本思想

归并(Merge)排序算法采用分治策略,将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

复杂度分析时间复杂度为$O(nlog_2^n)$,空间复杂度为$O(n)​$,稳定

动态实例参见: https://img-blog.csdnimg.cn/2018110819232047.gif

算法实现

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
//归并排序思想
ElemType *b = (ElemType *) malloc ((n+1)*sizeof(ElemType)); //带排序表有n个记录
void Merge(ElemType a[],int low,int mid,int high){
//表a中两段a[low,...,mid]和a[mid+1,...,high]各自有序,将其合并成一个有序表
for(int k=low;k<=high;k++)
b[k]=a[k]; //将a中元素复制到辅助数组b中
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(b[i]<=b[j]) //比较b的左右两段中的元素
a[k]=b[i++]; //将较小值复制到a中
else
a[k]=b[j++];
}
while(i<=mid) //若左半部分表未检测完,复制
a[k++]=b[i++];
while(j<=high) //若右半部分表未检测完,复制
a[k++]=b[j++];
}
void MergeSort(ElemType a[],int low,int high){
if(low<high){
int mid=(low+high)/2; //从中间划分2个子序列
MergeSort(a,low,mid); //对左侧子序列进行递归排序
MergeSort(a,mid+1,high); //对右侧子序列进行递归排序
Merge(a,low,mid,high); //归并
}
}

-------------------------------------------------------------------------------------------

//递归排序的完整实现
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

//合并两个有序序列(即归并)
// A: 待合并的序列(含两个子序列,排在一起)
// Temp: 辅助空间
// L: 左边序列起点下标
// R: 右边序列起点下标,
// RightEnd: 右边序列终点下标
void Merge(int A[], int Temp[], int L, int R, int RightEnd){
int LeftEnd = R-1;
int p=L,i;
int num=RightEnd-L+1;

//先合并最短序列的长度的个数个元素
while(L<=LeftEnd&&R<=RightEnd){

if(A[L]<=A[R])
Temp[p++]=A[L++];
else
Temp[p++]=A[R++];
}

//判断如果是左侧序列还有剩余
while(L<=LeftEnd)
Temp[p++]=A[L++];
//判断如果是右侧序列还有剩余
while(R<=RightEnd)
Temp[p++]=A[R++];
// 将辅助空间中的值拷贝到原列表中,完成排序
for(i=0;i<num;i++,RightEnd--)
A[RightEnd]=Temp[RightEnd];
}


//递归拆分,递归归并
void m_sort(int* arr, int* temp, int L, int right_end){
int center;
if(L<right_end){
center = (L+right_end)/2;
m_sort(arr,temp,L,center);
m_sort(arr,temp,center+1,right_end);
Merge(arr,temp,L,center+1,right_end);
}
}

//归并排序
void merge_Sort(int* arr,int length){
int *temp=(int* )malloc(length*sizeof(int)); //申请辅助空间
if(temp==NULL){
return;
}
m_sort(arr,temp,0,length-1);
free(temp);
temp=NULL;
}


//打印元素
void printArr(int *arr,int length){
for(int i=0;i<length;i++){
printf("%d\n",arr[i]);
}
}


int main(){
int arr[10]={3,5,2,10,8,9,6,4,7,1};
int length = sizeof(arr)/sizeof(int);
length = 10;
merge_Sort(arr,length);
printArr(arr,length);
return 0;
}

基数排序(Radix Sort)

基数排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序。

基本思想

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

基数排序按照优先从高位或低位来排序有两种实现方案:

  1. MSD(Most significant digital) 从最左侧高位开始进行排序。先按k1排序分组, 同一组中记录, 关键码k1相等,再对各组按k2排序分成子组, 之后, 对后面的关键码继续这样的排序分组, 直到按最次位关键码kd对各子组排序后. 再将各组连接起来,便得到一个有序序列。MSD方式适用于位数多的序列
  2. LSD (Least significant digital)从最右侧低位开始进行排序。先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。LSD方式适用于位数少的序列

复杂度分析 : 时间复杂度为$O(d(n+r))$,空间复杂度为$O(r)$,$d$为位数,$r$为基数,稳定

动态实例参见https://img-blog.csdnimg.cn/20181108193151498.gif

算法实现

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

//求数字位数
int bit_num(int num){
if(num/10==0){
return 1;
}
return 1+bit_num(num/10);
}

//求序列最大值
int max_list(int *arr, int length){
int max_num = arr[0];
for(int i=1;i<length;i++){
if(arr[i]>max_num){
max_num = arr[i];
}
}
return max_num;
}

//找到一个num从低位到高位的第pos位的数据,数据最右侧最低位pos=1
int get_num_pos(int num, int pos){
if(pos<=0){return -1;};
int pow_num = 1;
for(int i=0;i<pos-1;i++){pow_num*=10;}
return (num/pow_num)%10;
}

//基数排序
void base_Sort(int* arr, int length){
int max_num, key_num;
int *base_arr[10]; //十进制的10个桶
max_num = max_list(arr,length);

key_num = bit_num(max_num);

for(int i=0; i<10;i++){
base_arr[i]=(int *)malloc(sizeof(int)*(length+1));
base_arr[i][0] = 0; //桶中第一个位置记录桶中元素的数量
}
for(int pos = 1; pos<= key_num;pos++){ //需要执行最大位数次
for(int i=0;i<length;i++){
int num = get_num_pos(arr[i],pos);
int index = ++base_arr[num][0];
base_arr[num][index]=arr[i];
}
for(int i=0,j=0;i<10;i++){
for(int k=1; k<=base_arr[i][0];k++){
arr[j++] = base_arr[i][k];
}
base_arr[i][0]=0;
}
}

}

//打印元素
void printArr(int *arr,int length){
for(int i=0;i<length;i++){
printf("%d\n",arr[i]);
}
}

int main(){
int arr[] ={3,5,7,2,1,0,4,65,7,89,5,3,2,5,45,3,2,54,4,543,3,33,2,34,45,5};
int length = sizeof(arr)/sizeof(int);
base_Sort(arr,length);
printArr(arr,length);
return 0;
}

拓展

桶排序(Bucket Sort)

基本思想

是将一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。也是典型的divide-and-conquer分而治之的策略。它是一个分布式的排序,当要被排序的数组内的数值是均匀分配的时候,桶排序时间复杂度是O(n),桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响,稳定。

算法流程

  1. 建立一定数量的数组当作空桶;
  2. 遍历原始数组,并将数据放入到对应的桶中;
  3. 对非空的桶进行排序;
  4. 按照顺序遍历这些非空的桶并放回到原始数组中即可构成排序后的数组。

桶排序关键要确定当前数据和桶的映射关系函数,即一个数据应该放到哪个桶里边

举个列子
例如要对大小为$[1..1000]$范围内的$n$个整数$A[1..n]$排序

  1. 首先,可以把桶设为大小为10的范围,具体而言,设集合$B[1]$存储$[1..10]$的整数,集合$B[2]$存储$(10..20]$的整数,…… ,集合$B[i]$存储$((i-1)10, i10]$的整数,$i=1,2,..100$,总共有100个桶。
  2. 然后,对A$[1, … , n]$从头到尾扫描一遍,把每个$A[i]$放入对应的桶$B[j]$中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任何排序法都可以。
  3. 最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这样就得到所有数字排好序的一个序列了。

算法实现

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

//桶排序
void bucket_Sort(int* arr, int length){
int i,j,max_num=arr[0];
int* bucket;
//先求出序列最大值
for(i=1;i<length;i++){
if(arr[i]>max_num){
max_num=arr[i];
}
}
max_num++; //最大值加1
if(arr==NULL || length<=1){
return;
}
if((bucket = (int*)malloc(sizeof(int)*max_num))==NULL){return;}
for(i=0;i<max_num;i++){
bucket[i]=0; //空桶数组初始化
}
for(i=0;i<length;i++){ // 寻访序列,把元素一个一个放入对应的桶里
bucket[arr[i]]+=1;
}
for(i=0,j=0;i<max_num;i++){
while((bucket[i])>0){ //对每个不是空的桶子进行排序
arr[j]=i; //从不是空的桶子里把项目再放回原来的序列中
j++;
bucket[i]--;
}
}
free(bucket);
bucket = NULL;
}

//打印元素
void printArr(int *arr,int length){
for(int i=0;i<length;i++){
printf("%d\n",arr[i]);
}
}


int main(){
int arr[] ={3,5,7,2,1,0,4,65,7,89,5,3,2,5,45,3,2,54,4,543,3,33,2,34,45,5};
int length = sizeof(arr)/sizeof(int);
bucket_Sort(arr,length);
//printf("%d\n\n",length);
printArr(arr,length);
return 0;
}

计数排序(Count Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中

基本思想

对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。

例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,有重复时需要特殊处理(保证稳定性),需要在最后反向填充目标数组,并将每个数字的统计减去1。
复杂度分析
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是$O(n+k)$,空间复杂度也是$O(n+k)$,其排序速度快于任何比较排序算法。

当序列的最大值是M时,需要辅助空间的长度是M,所以不太适合数据量很大,或者最大值很大,或者数据分布很离散的场合下。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

动态实例参见https://img-blog.csdnimg.cn/20181108192837268.gif

算法实现

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

//计数排序 length: 带排序元素个数 max_num: 待排序元素中最大值
void counting_Sort(int* arr, int length){
int *c, *b;
int i, min_num,max_num,range;
min_num=max_num=arr[0];
//先求出序列最大最小值
for(i=1;i<length;i++){
if(arr[i]>max_num){
max_num=arr[i];
}
if(arr[i]<min_num){
min_num = arr[i];
}
}

range = max_num - min_num +1;
c = (int*)malloc(sizeof(int)*range); //辅助排序数组,长度是元素的最大值-最小值+1
b = (int*)malloc(sizeof(int)*length);
if(c==NULL || b == NULL){return;}
for(i=0;i<range;i++){
c[i]=0; //辅助数组初始化
}
for(i=0;i<length;i++){
c[arr[i]-min_num]+=1; //统计数组arr中每个值为i的元素出现次数
}
for(i =1; i<range;i++){
c[i]=c[i-1]+c[i]; //确定值为i的元素在数组c中出现的位置
}
for(i=length-1;i>=0;i--){
//对原序列arr中的每一个元素,从后向前依次确定每个元素所在的最终位置,
//先放入辅助数组b中(再拷回原始arr中)
c[arr[i]]-=1;
b[c[arr[i]-min_num]]=arr[i];

}
for(i = 0; i<length;i++){
arr[i]=b[i]; //拷回原始arr中
}
free(c);
c = NULL;
free(b);
b = NULL;
}

//打印元素
void printArr(int *arr,int length){
for(int i=0;i<length;i++){
printf("%d\n",arr[i]);
}
}


int main(){
int arr[] ={3,5,7,2,1,0,4,65,7,89,5,3,2,5,45,3,2,54,4,543,3,33,2,34,45,5};
int length = sizeof(arr)/sizeof(int);
counting_Sort(arr,length);
//printf("%d\n\n",length);
printArr(arr,length);
return 0;
}

计数排序、桶排序、基数排序区别

  1. 基数排序和计数排序都可以看作是一种特殊的桶排序;
  2. 计数排序是按照元素出现的次数划分桶,桶排序是按值区间划分桶,基数排序是按数位来划分桶。

排序算法总结

  1. 任何借助“比较”的排序算法,至少需要$O(nlog_2^n)​$空间
  2. 记录本身信息量较大时,用链表作为存储结构
  3. 排序趟数与原始状态无关:直接插入、简单选择、基数
  4. 排序中比较次数的数量级与序列初始状态无关:简单选择、归并

时间复杂度

  1. 平方阶$(O(n^2))$排序:直接插入、直接选择和冒泡排序;
  2. 线性对数阶$(O(nlogn))$排序:快速排序、堆排序和归并排序;
  3. $O(n1+§))$排序,$§$是介于0和1之间的常数:希尔排序
  4. 线性阶$(O(n))$排序:基数排序、计数排序、桶排序等。

稳定性

  1. 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
  2. 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

选择排序算法准则

  1. 待排序的记录数目n的大小;
  2. 记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
  3. 关键字的结构及其分布情况;
  4. 对排序稳定性的要求。

针对n的大小选择不同排序算法

  1. 当n较大,则应采用时间复杂度为$O(n*logn)$的排序方法:快速排序、堆排序或归并排序。

    快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
    堆排序:如果内存空间允许且要求稳定性的;
    归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。

  2. 当n较大,内存空间允许,且要求稳定性:归并排序

  3. 当n较小,可采用直接插入或直接选择排序。

    直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
    直接选择排序:当元素分布有序,如果不要求稳定性,选择直接选择排序。

  4. 一般不使用或不直接使用传统的冒泡排序。

  5. 基数排序
    它是一种稳定的排序算法,但有一定的局限性:

    1、关键字可分解;
    2、记录的关键字位数较少,如果密集更好;
    3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。

参考资料

八大排序算法
C 数据结构之十大排序
排序算法总结

------ 本文结束------
bluesliuf wechat
坚持技术分享,欢迎大家扫码交流!