排序算法

       最近正在学习数据结构与算法,边学习边记录一下。



1. 基本概念

(1)时间复杂度:算法中基本操作重复执行的次数是问题规模n的某个函数,其时间量度记作 T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))

(2)空间复杂度:在算法运行时所需的辅助空间大小的度量。

(3)排序稳定:具有相同关键字的元素,经过排序后相对次序仍保持一致

(4)排序不稳定:具有相同关键字的元素,经过排序后相对次序不能保持一致

(5)内部排序:待排序内容在内存中就可以完成。

(6)外部排序:待排序内容无法一次装入内存,待排序的记录存储在外存储器上,需要在内存与外存储器之间进行多次数据交换

2. 插入排序

       插入排序由N-1趟排序组成,每趟插入P都保证位置0到位置P-1的上的元素都已经是排序好的。举个例子,假设关键字为:7,4,-2,19,13,6,插入排序过程如下。
在这里插入图片描述

void InsertionSort(ElementType A[], int N)
{
  int j,P;
  ElementType Tmp;
  for(P=1; P<N; P++)
  {
    Tmp=A[P];
    for(j=P; j>0&&A[j-1]>Tmp; j--)
      A[j] = A[j-1];  //若该值大于插入值则后移该值
    A[j] = Tmp;       //跳出循环,把插入元素插入到适当位置
  }
}

       由于嵌套循环每一次都花费N次迭代,因此插入排序的平均时间复杂度为 O ( N 2 ) O(N^2) O(N2),如果数据已经预先排序,则运行时间为 O ( N ) O(N) O(N)对于几乎被排序的数据,使用插入排序运行非常快

3. 希尔排序

       希尔排序也属于插入排序法,它是一种分组的插入排序,通过比较具有相同间距的子序列进行插入排序,各趟比较的间距越来越小,直到间距为1为止。这些间隔组成的序列叫做增量序列。例如待排列关键字为:9,13,8,2,5,13,7,1,15,11,增量序列是5,3,1,则排序过程如下:
在这里插入图片描述

void ShellSort(ElementType A[], int N)
{
  int i, j, Increment;
  ElementType Tmp;
  //递增序列,逐渐2倍衰减
  for(Increment=N/2; Increment>0; Increment/=2)  
    for(i=Increment; i<N; i++)
    {
      Tmp = A[i];
      //开始插入排序
      for(j=i; j>=Increment; j-=Increment)
        if(Tmp < A[j-Increment])  //逆序则修改
          A[j] = A[j-Increment];  //大的值放后面
        else
          break;  //若位置合适则停止
      A[j] = Tmp; //将Tmp放到适合位置处
    }
}

       希尔排序的时间复杂度比较复杂,因为它跟增量序列有关。增量序列有很多种选法,上面代码使用的是 1 / 2 1/2 1/2递减的序列,还有其他更好的方法,例如Sedgewick增量序列最坏运行时间可以达到 O ( N 4 / 3 ) O(N^{4/3}) O(N4/3),但值得注意的是:应使增量序列中的值没有除1之外的公因子,并且最后一个增量值必须是1

4. 堆排序

       堆排序是基于二叉堆的方法,堆中每个结点的值均不大于(或不小于)其左、右孩子结点的值,因此堆顶元素为序列中的最小值(或最大值)。以最大堆为例子,先根据序列建立一个堆,然后将堆顶元素和与堆序列中的最后一个元素互换,缩减堆大小进行下滤,如此循环直到堆大小为1为止。其实这相当于执行N-1次DeleteMax操作。例如,输入序列为:31,41,59,26,53,58,97,执行一次DeleteMax如下:
在这里插入图片描述
       经过6次DeleteMax后得到的堆数组就是所需的排序数组。下面是堆排序的代码,注意不同于二叉堆,数据是从数组下标0处开始

#define LeftChild(i) (2*i+1)
//下滤函数
void PercDown(ElementType A[], int i, int N)
{
  int Child;
  ElementType Tmp;
  for(Tmp=A[i];LeftChild(i)<N; i=Child)
  {
    Child = LeftChild(i);   //结点左孩子的索引
    if(Child!=N-1 && A[Child+1]>A[Child])  //找更大的子结点
      Child++;                             //如果右孩子大于左孩子,则取右
    if(Tmp<A[Child])                       //下滤
      A[i]=A[Child];
    else
      break;
  }
  A[i] = Tmp;
}

void HeapSort(ElementType A[], int N)
{
  int i;
  for(i=N/2; i>=0; i--)   //建立堆
    PercDown(A, i, N);
  for(i=N-1; i>0; i--)
  {
    Swap(&A[0], &A[i]);   //交换堆顶元素和堆最后元素
    PercDown(A, 0, i);    //下滤调整,堆大小减1
  }
}

       堆排序不是一种稳定的算法,根据完全二叉树的性质,其在最坏情况下的时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)

5. 归并排序

       归并排序的核心思想是合并两个已排序的数组,分别从两个数组的起始端开始比较,将较小的元素存入到输出数组,然后继续比较使这两数组合并为一个数组,这样递归的合并显然是容易的。例如,输入关键字分别为:23,38,22,45,23,67,31,15,41,则递归过程如下:
在这里插入图片描述

//合并算法
/*Lpos=左半部的开始, Rpos=右半部的开始*/
void Merge(ElementType A[], ElementType TmpArray[], int Lpos, int Rpos, int RightEnd)
{
  int i, LeftEnd, NumElements, TmpPos;  
  LeftEnd = Rpos - 1;   //定位
  TmpPos = Lpos;        //从左半部开始
  NumElememts = RightEnd - Lops + 1;     //元素个数
  while(Lops <= LeftEnd && Rpos <= RightEnd)  
    //开始扫描左右数组,依次从小到大开始存储
    if(A[Lops]<=A[Rpos])                 
      TmpArray[TmpPos++] = A[Lpos++];
    else
      TmpArray[TmpPos++] = A[Rpos++];
    
    while(Lpos <= LeftEnd)   //左半部多于右半部则copy剩余部分
      TmpArray[TmpPos++] = A[Lpos++];
    while(Rpos <= RightEnd)  //右半部多于左半部则copy剩余部分
      TmpArray[TmpPos++] = A[Rpos++];
      
    //copy排序好的TmpArray给A
    for(i=0; i<NumElements; i++, RightEnd)  
      A[RightEnd] = TmpArray[RightEnd];
}

//递归算法
void MSort(ElementType A[], ElementType TmpArray[], int Left, int Right)
{
  int Center;
  if(Left<Right)
  {
    Center = ( Left + Right ) / 2;
    MSort(A, TmpArray, Left, Center);       //递归左半部
    MSort(A, TmpArray, Center+1, Right);    //递归右半部
    Merge(A, TmpArray, Left, Center+1, Right);  //合并
  }
}

void MergeSort(ElementType A[], int N)
{
  ElementType *TmpArray;
  TmpArray = malloc(N * sizeof(ElementType));  //申请内存
  if(TmpArray != NULL)
  {
    MSort(A, TmpArray, 0, N-1);
    free(TmpArray);
  }
  else
    FatalError("No space for tmp array!!!");
}

       归并排序排序的时间分析需要一些计算,对N个数归并排序的用时等于完成两个大小为N/2的递归排序所用时间再加上合并的时间
T ( 1 ) = 1 T(1)=1 T(1)=1
T ( N ) = 2 T ( N / 2 ) + N T(N)=2T(N/2)+N T(N)=2T(N/2)+N
       可以变换为:
T ( N ) N = T ( N / 2 ) N / 2 + 1 \frac{T(N)}{N}=\frac{T(N/2)}{N/2}+1 NT(N)=N/2T(N/2)+1
       该方程对2的幂的任意的N是成立的,还可以写成:
T ( N / 2 ) N / 2 = T ( N / 4 ) N / 4 + 1 \frac{T(N/2)}{N/2}=\frac{T(N/4)}{N/4}+1 N/2T(N/2)=N/4T(N/4)+1
T ( N / 4 ) N / 4 = T ( N / 8 ) N / 8 + 1 \frac{T(N/4)}{N/4}=\frac{T(N/8)}{N/8}+1 N/4T(N/4)=N/8T(N/8)+1
. . . ... ...
T ( 2 ) 2 = T ( 1 ) 1 + 1 \frac{T(2)}{2}=\frac{T(1)}{1}+1 2T(2)=1T(1)+1
       将这些式子等号左右加和:
T ( N ) N = T ( 1 ) 1 + k , N = 2 k \frac{T(N)}{N}=\frac{T(1)}{1}+k,N=2^k NT(N)=1T(1)+kN=2k
T ( N ) N = T ( 1 ) 1 + l o g N \frac{T(N)}{N}=\frac{T(1)}{1}+logN NT(N)=1T(1)+logN
       因此,我们可以得到:
T ( N ) = N + N l o g N = O ( N l o g N ) T(N)=N+NlogN=O(NlogN) T(N)=N+NlogN=O(NlogN)
       可以看到归并排序的时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN),虽然低于插入排序等,但它很难用于主存排序,由于合并两个序列需要线性附加内存,因此对于重要的内部排序应用而言,大多还是选择快速排序,不过它合并思想可以在外部排序中广泛应用

6. 快速排序

       快速排序是已知的实践中最快的算法,其思想也是分治的递归算法,选中数组中的一个枢纽元p,将数组分为大于p和小于p的两组,然后再递归的处理这两个数组,具体步骤如下图所示:
在这里插入图片描述
       对于枢纽元的选择,比较简单的方法是选择数组最左端、最右端、中间3个位置上的数的中值作为枢纽元,这种方法叫做三数中值分割法,能够避免预排序。例如输入为:8,1,4,9,6,3,5,2,7,0,则枢纽元选择为median(8,6,0)=6,下图为将数组变为左侧小于枢纽值,右侧大于枢纽值的过程:
在这里插入图片描述

//三数中值分割法
ElementType Median3(ElementType A[], int Left, int Right)
{
  int Center = ( Left + Right ) / 2; 
  if(A[Left]>A[Center])
    Swap(&A[Left], &A[Center]);
  if(A[Left]>A[Right])
    Swap(&A[Left], &A[Right]);
  if(A[Center]>A[Right])
    Swap(&A[Center], &A[Right]);
  //调整为为A[Left]<=A[Center]<=A[Right]
  
  Swap(&A[Center], &A[Right-1]); 
  //将枢纽元存储在A[Right-1]处,A[Right]已经大于枢纽元了,不需要进行操作
  return A[Right-1];      //返回枢纽元
}

//递归程序
#define Cutoff (3)
void Qsort(ElementType A[], int Left, int Right)
{
  int i, j;
  ElementType Pivot;             //枢纽元
  if(Left + Cutoff <= Right)
  {
    Pivot = Median3(A, Left, Right);  //返回枢纽元素
    //初始化i,j的位置,比正确位置多1,不需要考虑特殊情况
    i = Left; j = Right - 1;   
    for( ; ; )
    {
      while(A[++i]<Pivot) {}   //满足小于枢纽元则右移
      while(A[--j]>Pivot) {}   //满足大于枢纽元则左移
      if(i<j)                  
        Swap(&A[i], &A[j]);    //不满足且i<j则进行交换
      else
        break;   //i>=j则跳出循环,找到枢纽元
    }
    Swap(&A[i], &A[Right-1]);  //将枢纽元与A[i]交换
    Qsort(A, Left, i-1);       //递归
    Qsort(A, i+1, Right);
  }
  else
    InsertionSort(A+Left, Right-Left+1);  
    //若不满足Left + 3 <= Right执行插入排序
}

//快速排序的驱动程序
void QuickSort(ElementType A[], int N)
{
  Qsort(A, 0, N-1);
}

       可以证明的快速排序的平均时间复杂度同样是 O ( N l o g N ) O(NlogN) O(NlogN),在最坏情况下也为 O ( N 2 ) O(N^2) O(N2),但这种最坏情况的出现也是微不足道,因此快速排序仍然是大量数据排序的优先选择。

7. 桶式排序

       桶排序是使用一个大小为M的Count数组,Count数组中每一个元素就是一个桶。将桶数组初始化为空,依次读入排序数据,例如入读 A i A_i Ai C o u n t [ A i ] Count[A_i] Count[Ai]增1,在读入所有数据后,扫描Count数组即可得到排序好的数组。例子,输入数组为5,2,2,7,9,1,3,8,4,10,3,桶排序过程如下:
在这里插入图片描述

void BucketSort(int A[], int N, int Max)
{
  int i, j;
  int *buckets;
  if(a==NULL || N<1 || Max<1)
    return;
  if((buckets==(int *)malloc(Max*sizeof(int)))==NULL)
    return;
  memset(buckets, 0, Max*sizeof(int));
  //桶计数
  for(i=0; i<N; i++)
    buckets[A[i]]++; 
  //桶输出
  for(i=0,j=0; i<Max; i++)
    while((buckets[i]--)>0)
      A[j++] = i;
  free(buckets;)
}

       桶排序的时间复杂度是 O ( N ) O(N) O(N),相比于其他算法的比较机制,桶排序似乎能够提供最少的时间消耗,但是随着桶的增值,这种优势显然会下降。虽然桶排序看起来平凡且用处不大,但对于那些输入只是小的整数情况使用桶排序会比快排更适合

8. 基数排序

       基数排序是也不同于之前的使用比较移动的算法,它是基于多关键字排序的思想来对单关键字进行排序,它其实算是桶排序的升级版。其首先找到数组最大值,然后按照最大值统一数组元素的位数,最后从低到高按照个、十、百、千等位的值大小依次进行排序。例如,输入数组为21,475,32,67,178,386,72,9,49,217,156,基数排序过程如下:
在这里插入图片描述

//获取桶个数
int GetMax(int A[], int N)
{
  int i,max;
  max = A[0];
  for(i=1; i<N; i++)
    if(A[i]>max)
      max = A[i];   
  return max;
}
//
void CountSort(int A[], int N, int exp)
{  
  int output[N];
  int i;
  int buckets[10] = {0};  //阿拉伯数字桶
  for(i=0; i<N; i++)
    buckets[(A[i]/exp)%10]++;    //桶排序,记录出现的次数

  for(i=1; i<10; i++)
    buckets[i] += buckets[i-1];  //记录总计数
    
  //A[i]对应的位置及总计数减1  
  for(i=N-1; i>=0; i--)
  {
    output[buckets[(A[i]/exp)%10]-1] = A[i];  
    buckets[(A[i]/exp)%10]--;
  }
    
  for(i=0; i<N; i++)
    A[i] = output[i];
}

//基数排序驱动程序
void radix_sort(int A[], int N)
{
  int exp;
  int max = GetMax(A, N);
  for(exp=1; max/exp>0; exp*=10)
    CountSort(A, N, exp);
}

       基数排序的时间复杂度可以达到 O ( N ) O(N) O(N),设关键字位数为d,每位有r种取值,那么时间复杂度为 O ( d ( N + r ) ) O(d(N+r)) O(d(N+r)),空间复杂度为 O ( N + r ) O(N+r) O(N+r)。在大数据情况下相当于用空间换时间

9. 总结

排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性应用范围
插入排序 O ( N 2 ) O(N^2) O(N2) O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)稳定N较小,基本有序
希尔排序 O ( N 4 / 3 ) O(N^{4/3}) O(N4/3) O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)不稳定N较大
堆排序 O ( N l o g N ) O(NlogN) O(NlogN) O ( N l o g N ) O(NlogN) O(NlogN) O ( 1 ) O(1) O(1)不稳定N较大
归并排序 O ( N l o g N ) O(NlogN) O(NlogN) O ( N l o g N ) O(NlogN) O(NlogN) O ( N ) O(N) O(N)稳定N较大,外部排序
快速排序 O ( N l o g N ) O(NlogN) O(NlogN) O ( N 2 ) O(N^2) O(N2) O ( l o g N ) O(logN) O(logN)不稳定基本都适用
桶式排序 O ( N ) O(N) O(N) O ( N ) O(N) O(N) O ( M ) O(M) O(M)稳定较小的正整数
基数排序 O ( d ( N + r ) ) O(d(N+r)) O(d(N+r)) O ( d ( N + r ) ) O(d(N+r)) O(d(N+r)) O ( N + r ) O(N+r) O(N+r)稳定长度较短的数

       本文代码基于数据结构与算法分析(C语言描述)以及skywang12345的数据结构与算法系列博客(https://www.cnblogs.com/skywang12345/p/3603935.html )。如有错误及时更新。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值