在这篇博文中,我们介绍了剩下4种排序算法,并将对所有的排序算法做一个总结。 代码主要参见数据机构之十大排序,关于拓展里面的桶排序和计数排序,只了解了想法,并未自己实现,不过也给出了参考资料中别人的代码实现。
在要排序的一组数中,选出最小(或者最大)的一个数与第 1 1 1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第 2 2 2个位置的数交换,依次类推,直到第 n − 1 n-1 n−1个元素(倒数第二个数)和第 n n n个元素(最后一个数)比较为止。
复杂度分析:时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1),不稳定
动态实例参见:
//选择排序,以升序为例
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;
}
堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
其中( 1 ≤ i ≤ ⌊ n / 2 ⌋ 1 \le i \le \lfloor n/2 \rfloor 1≤i≤⌊n/2⌋)
堆排序常用于解决top-k问题。
这里的大顶堆是以数组形式存储的,按层(行)存储,第一个元素是顶堆,第2个元素是左子树第一个分支结点,第3个元素是右子树第一个分支结点,第4个元素是第3层左边第一个结点…,有如下性质(根结点的索引是0):
复杂度分析:时间复杂度为 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n),空间复杂度为 O ( 1 ) O(1) O(1),不稳定
一个大根堆的建立过程如下:
动态实例参见(大根堆):
要想实现堆排序,必须考虑2个方面:
//堆排序方法一,主要包含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
#include
//数据交换
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)排序算法采用分治策略,将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
复杂度分析:时间复杂度为 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n),空间复杂度为 O ( n ) O(n) O(n),稳定。
动态实例参见:
//归并排序思想
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
#include
#include
//合并两个有序序列(即归并)
// 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;
}
基数排序过程无须比较关键字,而是通过**“分配”和“收集”**过程来实现排序。
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序按照优先从高位或低位来排序有两种实现方案:
复杂度分析 : 时间复杂度为 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)),空间复杂度为 O ( r ) O(r) O(r), d d d为位数, r r r为基数,稳定。
动态实例参见:
#include
#include
#include
//求数字位数
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;
}
是将一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。也是典型的divide-and-conquer分而治之的策略。它是一个分布式的排序,当要被排序的数组内的数值是均匀分配的时候,桶排序时间复杂度是O(n),桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响,稳定。
桶排序关键要确定当前数据和桶的映射关系函数,即一个数据应该放到哪个桶里边。
举个列子
例如要对大小为 [ 1..1000 ] [1..1000] [1..1000]范围内的 n n n个整数 A [ 1.. n ] A[1..n] A[1..n]排序
#include
#include
#include
//桶排序
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;
}
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。
例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,有重复时需要特殊处理(保证稳定性),需要在最后反向填充目标数组,并将每个数字的统计减去1。
复杂度分析
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是 O ( n + k ) O(n+k) O(n+k),空间复杂度也是 O ( n + k ) O(n+k) O(n+k),其排序速度快于任何比较排序算法。
当序列的最大值是M时,需要辅助空间的长度是M,所以不太适合数据量很大,或者最大值很大,或者数据分布很离散的场合下。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
动态实例参见:
#include
#include
#include
//计数排序 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、记录的关键字位数较少,如果密集更好;
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。
八大排序算法
C 数据结构之十大排序
排序算法总结