排序算法(下)

(六)、快速排序(Quick Sort)

1、算法描述

快速排序 的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

步骤1:从数列中挑出一个元素,称为 “基准”(pivot );
步骤2:重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区操作
步骤3:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

2、动图演示

image

3、代码实现

#include <stdlib.h>
#include <stdio.h>

void quicksort(int *arr,unsigned int len)
{
  if (len<2) return;  // 数组的元素小于2个就不用排序了。

  int itmp=arr[0];  // 选取最左边的数作为中心轴。
  int ileft=0;      // 左下标。
  int iright=len-1; // 右下标。
  int imoving=2;    // 当前应该移动的下标,1-左下标;2-右下标。

  while (ileft<iright)
  {
    if (imoving==2) // 移动右下标的情况。
    {
      // 如果右下标位置元素的值大于等于中心轴,继续移动右下标。
      if (arr[iright]>=itmp) { iright--; continue; }
      
      // 如果右下标位置元素的值小于中心轴,把它填到左下标的坑中。
      arr[ileft]=arr[iright];
      ileft++;    // 左下标向右移动。
      imoving=1;  // 下次循环将移动左下标。
      continue;
    }

    if (imoving==1) // 移动左下标的情况。
    {
      // 如果左下标位置元素的值小等于中心轴,继续移动左下标。
      if (arr[ileft]<=itmp) { ileft++; continue; }

      // 如果左下标位置元素的值大于中心轴,把它填到右下标的坑中。
      arr[iright]=arr[ileft];
      iright--;   // 右下标向左移动。
      imoving=2;  // 下次循环将移动右下标。
      continue;
    }
  }

  // 如果循环结束,左右下标重合,把中心轴的值填进去。
  arr[ileft]=itmp;

  quicksort(arr,ileft);             // 对中心轴左边的序列进行排序。
  quicksort(arr+ileft+1,len-ileft-1); // 对中心轴右边的序列进行排序。
}

int main(int argc,char *argv[])
{
  int arr[]={44,3,38,5,47,15,36,26,27,2,46,4,19,50,48};
  int len=sizeof(arr)/sizeof(int);

  quicksort(arr,len);   // 调用插入排序函数对数组排序。

  // 显示排序结果。
  int yy; for (yy=0;yy<len;yy++) printf("%2d ",arr[yy]); printf("\n");

  // system("pause");  // widnows下的C启用本行代码。

  return 0;
}

4、性能特点

快速排序之所以比较快,是因为与冒泡排序相比,每次的交换时跳跃式的,**每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。**这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。

因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O ( n 2 ) O(n^2)O(n2 ),它的平均时间复杂度为O ( n log ⁡ 2 n ) O(n\log_2n)O(nlog 2n)。

(七)、堆排序(Heap Sort)

1、算法描述

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

2、动画演示

image

3、代码实现

#include <stdio.h>
#include <stdlib.h>

// 交换两个元素的值。
void swap(int *a,int *b) { int temp=*b; *b=*a; *a=temp; }

// 采用循环实现heapify(元素下沉)。
// arr-待排序数组的地址,start-待heapify节点的下标,end-待排序数组最后一个元素的下标。
void heapify(int *arr,int start,int end) 
{
  // 确定父节点和左子节点的数组下标。
  int dad=start;
  int son=dad*2+1;

  // 如果子节点的下标没有超出范围,循环继续。
  while (son<=end) 
  { 
    // 先比较两個子节点大小,选择最大的。
    if ((son+1<=end) && (arr[son]<arr[son+1])) son++;

    // 如果父节点大于子节点代表调整完毕,直接跳出函数。
    if (arr[dad]>arr[son]) return;

    // 否则交换父子內容再继续子节点和孙节点比较。
    swap(&arr[dad],&arr[son]);
    dad=son;
    son=dad*2+1;
  }
}

// 采用递归实现heapify。
void heapify1(int *arr,int start,int end) 
{
  // 确定父节点和左子节点的数组下标。
  int dad=start;
  int son=dad*2+1;

  // 如果子节点的下标没有超出范围,循环继续。
  if (son>end ) return;

  // 先比较两個子节点大小,选择最大的。
  if ((son+1<=end) && (arr[son]<arr[son+1])) son++;

  // 如果父节点大于子节点代表调整完毕,直接跳出函数。
  if (arr[dad]>arr[son]) return;

  // 否则交换父子內容再继续子节点和孙节点比较。
  swap(&arr[dad],&arr[son]);

  heapify(arr,son,end);
}

void heapsort(int *arr, int len) 
{
  int ii;

  // 初始化堆,从最后一個父节点开始调整。
  for (ii=(len-1)/2;ii>=0;ii--) heapify(arr,ii,len-1);

  // 把第一个元素和堆最后一个元素交换,然后重新调整,直到排序完毕。
  for (ii=len-1;ii>0;ii--) 
  {
    swap(&arr[0],&arr[ii]);
    heapify(arr,0,ii-1);
  }
}

int main() 
{
  int arr[]={44,3,38,5,47,15,36,26,27,2,46,4,19,50,48};

  int len=sizeof(arr)/sizeof(int);

  heapsort(arr,len);

  // 显示排序结果。
  int yy; for (yy=0;yy<len;yy++) printf("%2d ",arr[yy]); printf("\n");

  // system("pause");  // widnows下的C启用本行代码。

  return 0;
}

4、性能特点

堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1…n]中选择最大记录,需比较n-1次,然后从R[1…n-2]中选择最大记录需比较n-2次。

事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。

对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。

(八)、计数排序(Counting Sort)

1、算法描述

计数排序是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序

2、动图演示

image

3、代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 获取待排序数组的最大元素的值。
int arrmax(int *arr,unsigned int len)
{
  int ii=0;
  int imax=0;

  for (ii=0;ii<len;ii++) if (imax<arr[ii]) imax=arr[ii];

  return imax;
}

// 计数排序主函数,arr-待排序数组的地址,len-数组的长度。
void countsort(int *arr,unsigned int len) 
{
  if (len<2) return;

  int imax=arrmax(arr,len);  // 获取待排序数组的最大元素的值。
  int arrtmp[imax+1];        // 临时数组的大小为imax+1。

  memset(arrtmp,0,sizeof(arrtmp));  // 初始化临时数组。

  int ii,jj,kk;
  
  // 临时数组计数。
  for (ii=0;ii<len;ii++) arrtmp[arr[ii]]++;

  // 把临时数组计数的内容填充到arr中。
  ii=0;
  for (jj=0;jj<imax+1;jj++)
  {
    for (kk=0;kk<arrtmp[jj];kk++) arr[ii++]=jj;
  }
}

int main() 
{
  int arr[]={2,3,8,7,1,2,2,2,7,3,9,8,2,1,4,2,4,6,9,2};

  int len=sizeof(arr)/sizeof(int);

  int xx; for (xx=0;xx<len;xx++) printf("%2d ",arr[xx]); printf("\n");

  countsort(arr,len);

  // 显示排序结果。
  int yy; for (yy=0;yy<len;yy++) printf("%2d ",arr[yy]); printf("\n");

  // system("pause");  // widnows下的C启用本行代码。

  return 0;
}

4、性能特点

当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。
计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存

(九)、桶排序(Bucket Sort)

1、算法描述

桶排序 是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

桶排序 (Bucket sort)的工作的原理:

假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

2、动画演示

image

3、代码实现

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// 采用两层循环实现冒泡排序的方法。
// 参数arr是待排序数组的首地址,len是数组元素的个数。
void bubblesort(int *arr,unsigned int len)
{
  if (len<2) return; // 数组小于2个元素不需要排序。

  int ii;    // 排序的趟数的计数器。
  int jj;    // 每趟排序的元素位置计数器。
  int itmp;  // 比较两个元素大小时交换位置用到的临时变量。

  // 44,3,38,5,47,15,36,26,27,2,46,4,19,50,48  
  for (ii=len-1;ii>0;ii--)  // 一共进行len-1趟比较。
  {
    for (jj=0;jj<ii;jj++)  // 每趟只需要比较0......ii之间的元素,ii之后的元素是已经排序好的。
    {
      if (arr[jj]>arr[jj+1])  // 如果前面的元素大于后面的元素,则交换它位的位置。
      {
        itmp=arr[jj+1];
        arr[jj+1]=arr[jj];
        arr[jj]=itmp;
      }
    }
  }
}

// 桶排序主函数,参数arr是待排序数组的首地址,len是数组元素的个数。
void bucketsort(int *arr,unsigned int len)
{
  int bucket[5][5];   // 分配五个桶。
  int bucketsize[5];  // 每个桶中元素个数的计数器。

  // 初始化桶和桶计数器。
  memset(bucket,0,sizeof(bucket));
  memset(bucketsize,0,sizeof(bucketsize));

  // 把数组arr的数据放入桶中。
  int ii=0;
  for (ii=0;ii<len;ii++)
  {
    bucket[arr[ii]/10][bucketsize[arr[ii]/10]++]=arr[ii];
  }

  // 对每个桶进行冒泡排序。
  for (ii=0;ii<5;ii++)
  {
    bubblesort(bucket[ii],bucketsize[ii]);
  }

  // 把每个桶中的数据填充到数组arr中。
  int jj=0,kk=0;
  for (ii=0;ii<5;ii++)
  {
    for (jj=0;jj<bucketsize[ii];jj++)
      arr[kk++]=bucket[ii][jj];
  }
}

int main(int argc,char *argv[])
{
  int arr[]={21,3,30,44,15,36,6,10,9,19,25,48,5,23,47};
  int len=sizeof(arr)/sizeof(int);

  int xx; for (xx=0;xx<len;xx++) printf("%2d ",arr[xx]); printf("\n");

  bucketsort(arr,len);

  // 显示排序结果。
  int ii; for (ii=0;ii<len;ii++) printf("%2d ",arr[ii]); printf("\n");

  // system("pause");  // widnows下的C启用本行代码。

  return 0;
}

4、性能特点

如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize,增加下一循环桶的数量,否则会陷入死循环,导致内存溢出

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

(十)、基数排序(Radix Sort)

1、算法描述

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

2、动画演示

img

3、代码实现

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// 获取数组arr中最大值,arr-待排序的数组,len-数组arr的长度。
int arrmax(int *arr,unsigned int len)
{
  int ii,imax;

  imax=arr[0];

  for (ii=1;ii<len;ii++)
    if (arr[ii]>imax) imax=arr[ii];

  return imax;
}

// 对数组arr按指数位进行排序。
// arr-待排序的数组,len-数组arr的长度。
// exp-排序指数,exp=1:按个位排序;exp=10:按十位排序;......
void _radixsort(int *arr,unsigned int len,unsigned int exp)
{
  int ii;
  int result[len];       // 存放从桶中收集后数据的临时数组。
  int buckets[10]={0};   // 初始化10个桶。

  // 遍历arr,将数据出现的次数存储在buckets中。
  for (ii=0;ii<len;ii++)
    buckets[(arr[ii]/exp)%10]++;

  // 调整buckets各元素的值,调整后的值就是arr中元素在result中的位置。
  for (ii=1;ii<10;ii++)
    buckets[ii]=buckets[ii]+buckets[ii-1];

  // 将arr中的元素填充到result中。
  for (ii=len-1;ii>=0;ii--)
  {
    int iexp=(arr[ii]/exp)%10;
    result[buckets[iexp]-1]=arr[ii];
    buckets[iexp]--;
  }
  
  // 将排序好的数组result复制到数组arr中。
  memcpy(arr,result,len*sizeof(int));
}

// 基数排序主函数,arr-待排序的数组,len-数组arr的长度。
void radixsort(int *arr,unsigned int len)
{
  int imax=arrmax(arr,len);    // 获取数组arr中的最大值。

  int iexp;    // 排序指数,iexp=1:按个位排序;iexp=10:按十位排序;......

  // 从个位开始,对数组arr按指数位进行排序。
  for (iexp=1;imax/iexp>0;iexp=iexp*10)
  {
    _radixsort(arr,len,iexp);
    int yy; printf("exp=%-5d  ",iexp); for (yy=0;yy<len;yy++) printf("%2d ",arr[yy]); printf("\n");
  }
}

int main(int argc,char *argv[])
{
  int arr[]={144,203,738,905,347,215,836,26,527,602,946,504,219,750,848};
  int len=sizeof(arr)/sizeof(int);

  radixsort(arr,len);  // 基数排序。

  // 显示排序结果。
  int yy; for (yy=0;yy<len;yy++) printf("%2d ",arr[yy]); printf("\n");

  // system("pause");  // widnows下的C启用本行代码。

  return 0;
}

4、性能特点

设待排序的数组R[1…n],数组中最大的数是d位数,基数为r(如基数为10,即10进制,最大有10种可能,即最多需要10个桶来映射数组元素)

处理一位数,需要将数组元素映射到r个桶中,映射完成后还需要收集,相当于遍历数组一遍,最多元素数为n,则时间复杂度为O(n+r)。所以,总的时间复杂度为O(d*(n+r))。

基数排序过程中,用到一个计数器数组,长度为r,还用到一个rn的二位数组来做为桶,所以空间复杂度为O(rn)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值