数据结构中各种排序算法小结

数据结构课程的排序算法特点小结

概述


  1. 衡量排序算法好坏的三个指标 :
        时间效率——排序速度(比较次数与移动次数)
        空间效率——占内存辅助空间的大小
        稳定性——A 和 B 的关键字相等,排序后 A、B
        的先后次序保持不变,则称这种排序 算法是稳定的

  2. 排序算法存储结构:记录序列以顺序表存储,其伪代码如下。

# define MAXSIZE 20               //设记录不超过 20 个 
Typedef int KeyType ;             //设关键字为整型量(int 型) 
Typedef struct {                  //定义每个记录(数据元素)的结构   
  KeyType  key ;                  //关键字 
  InfoType  otherinfo;            //其它数据项 
}RedType ; 
Typedef struct {                  //定义顺序表的结构  
  RedType r [ MAXSIZE +1 ];       //存储顺序表的向量,  //r[0]一般作哨兵或缓冲区  
  int length ;                    //顺序表的长度 
}SqList ; 
  1. 排序算法分类 :按照规则不同,可分为插入排序、选择排序、交换排序、归并排序、基数排序五类。其中每一种排序方法都有又可以有各种具体的排序算法,按照算法时间复杂度的不同又可以把排序算法分为简单排序O(n^2)和先进排序O(nlog2n)

插入排序

    基本思想:
        每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。 即边插入边排序,保证子序列中随时都是排好序的。
    排序中设有哨兵位置在R[0];

    插入排序的基本步骤:
        STEP1:在 R[1…i-1]中查找 R[i]的插入位置, 使得R[1…j].key<R[i].key<R[j+1…i-1].key;         STEP2:将 R[j+1…i-1]中的所有记录均后移一个位置;
        STEP3:将 R[i] 插入到 R[j+1]的位置上。

具体实现时又根据不同的算法描述,分为三种插入算法!
直接插入排序(基于顺序查找)
折半插入排序(基于折半查找)
希尔排序(基于逐趟缩小增量)

直接插入排序

    排序过程:整个排序过程为 n-1 趟插入,即先将序列中第 1 个记录看成是一个有序子 序列,然后从第 2 个记录开始,逐个进行插入,直至整个序列有序。

void InsertSort(SqList &L){
  int i,j; 
  for(i=2;i<=L.length;++i){
    if (L.r[i] .key<L.r[i-1]. key){  //将 L.r[i]插入有序子表
      L.r[0]=L.r[i];        // 复制为哨兵  
      L.r[i]=L.r[i-l]; 
      for(j=i-2; L.r[0].key<L.r[j].key;--j){
        L.r[j+1]=L.r[j];     // 记录后移 
      }
      L.r[j+l]=L.r[0];        //插入到正确位置  
    }
  }
}

**算法分析:**比较次数和移动次数与初始排列有关 ,最好情况下每趟只需比较 1 次,不移动 ,总比较次数为 n-1 。最坏情况下:第 i 趟比较 i 次(包含与哨兵比较算一次),移动 i+1 次,则累加可得总比较次数为(n+2)(n-1)/2,总移动次数为(n+4)(n-1)/2。 若出现各种可能排列的概率相同,则可取最好情况和最坏情况的平均情况,平均情况比较次数和移动次数为 (n*n)/4 ,故平均情况下,时间复杂度为O(n^2),空间复杂度为O(1),稳定性:稳定。

折半插入排序

    排序过程:类似的,每次插入却不是逐个比较,而是利用折半查找法寻找R[i]的最终位置。

void BlnsertSort(SqList &L) {
  for (i = 2; i <= L.length ; ++i ) {
    L.r[0] = L.r[i]; low = 1 ; high = i-1; 
    while (low <= high)  {
      m = (low + high)/ 2 ; 
      if (L.r[0].key < L.r[m]. key)
        high = m -1 ; 
      else low = m + 1; 
    } 
    for (j=i-1; j>=high+1;--j)
      L.r[j+1] = L.r[j];
      L.r[high+1] = L.r[0]; 
  }  
} // BlnsertSort 
 

算法分析:折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快
它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。 在插入第 i 个对象时,需要经过⌊log2i+1⌋次关键码比较,才能确定它应插入的位置 当 n 较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情 况要差
在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列 。
故总的来说,折半插入排序减少了比较次数,但没有减少移动次数。
    平均性能优于直接插入排序
    时间复杂度为O(n^2)
    空间复杂度为O(1)
    是一种稳定的排序方法

希尔排序

算法产生的思想:直接插入排序在基本有序时,效率较高。在待排序的记录个数较少时,效率较高 。
基本思想:
    先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的
记录“基本有序”时,再对全体记录进行一次直接插入排序。
步骤:将相隔某个增量 dk 的记录组成一个子序列,再让增量 dk 逐趟缩短(例如依次取 5,3,1) ,知道dk=1时结束,得到的子序列即为有序序列。

void ShellSort(SqList &L,int dlta[ ]int t){ //按增量序列 dlta[0…t-1]对顺序表 L作Shell排序     for(k=0;k<t;++k){
  ShellInsert(L,dlta[k]); //增量为 dlta[k]的一趟插入排序  
}
} // ShellSort
void ShellInsert(SqList &L, int dk) {
   for(i=dk+1; i<=L.length; ++ i){  //开始将 r[i]插入有序增量子表  
     if(L.r[i].key <L.r[i-dk].key) { 
       L.r[0]=L.r[i];           //暂存在 r[0] 
       for(j=i-dk; j>0 &&(L.r[0].key<L.r[j].key); j=j-dk)  
         L.r[j+dk]=L.r[j];    //关键字较大的记录在子表中后移.  
       L.r[j+dk]=L.r[0];    //在本趟结束时将 r[i]插入到正确位置 
     } 
   } 
}

时间复杂度是 n 和 d 的函数:
O(n1.25)~O(1.6n^1.25)—经验公式
空间复杂度为 O(1)
是一种不稳定的排序方法。

交换排序

基本思想:两两比较,如果发生逆序则交换,直到所有记录都排好序为止。

  1. 冒泡排序
  2. 快速排序

冒泡排序

特点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素。

void bubble_sort(SqList &L){
 int m,i,j,flag=1; RedType x; m=n-1;  
 while((m>0)&&(flag==l)) {   
   flag=0;  
   for(j=1;j <=m;j ++){
   if(L.r[j].key>L.r[j+1].key) { 
   flag=1; 
   x=L.r[ j ] ;
   L.r[j ]=L.r[:j+l]; 
   L.r[j+l]=x; //交换 
   } //endif  
   m--;  
   } //endwhile 
} 

比较次数和移动次数与初始排列有关最好情况下:只需 1 趟排序,比较次数为 n-1,不移动 。
最坏情况下:需 n-1 趟排序,第 i 趟比较 n-i 次,移动 3(n-i)次 。
平均情况:
    时间复杂度为 o(n2)
    空间复杂度为 o(1)
    是一种稳定的排序方法

快速排序

**基本思想:**任取一个元素(如第一个) 为中心;所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表;对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个;
快速排序思想:
(1)递归
(2)每一次剖分是一个核心过程枢轴值的选择 先从后边开始,交替相向行进

void main (){
  QSort (L, 1, L.length ); 
} 
void QSort (SqList &L,int low,int high) {  
  if(low < high){ 
    pivotloc = Partition(L, low, high) ; 
    Qsort (L, low, pivotloc-1); 
    Qsort (L, pivotloc+1, high ) 
  } 
} 
int Partition (SqList &L, int low, int high ) { 
  L.r[0] = L.r[low]; 
  pivotkey = L.r[low].key; 
  while (low < high) {  
    while (low < high && L.r[high].key >= pivotkey)                    
      --high; 
    L.r[low] = L.r[high]; 
    while (low < high && L.r[low].key <= pivotkey)                  
      ++low; 
    L.r[high] = L.r[low]; 
  } L.r[low]=L.r[0]; 
  return low;  
}

算法分析 可以证明,平均计算时间是 O(nlog2n)。
实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。 快速排序是递归的,需要有一个栈存放每层递归调用时参数(新的 low 和 high)。 最大递归调用层次数与递归树的深度一致,因此,要求存储开销为 O(log2n) 。 最好:划分后,左侧右侧子序列的长度相同
最坏:从小到大排好序,递归树成为单支树,每次划分只得到一个比上一次少一个对象的子序列,必须经过 n-1 趟才能把所有对象定位,而且第 i 趟需要经过 n-i 次关键码比 较才能找到第 i 个对象的安放位置

快速排序算法分析:
    时间效率:O(nlog2n) —每趟确定的元素呈指数增加
    空间效率:O(log2n)—递归要用到栈空间
    稳 定 性: 不稳定 —可选任一元素为支点。

选择排序

基本思想: 每一趟在后面 n-i +1 个中选出关键码最小的对象, 作为有序序列的第 i 个记录(比较贴切易懂)。

简单选择排序

void SelectSort(SqList &K) {
  for (i=l; i<L.length; ++i)  {                 / / 在 L.r[i..L.length]中选择 key 最小记录  
    k=i;  
    for(j=i+l;j<=L.length ; j++){
      if (L.r[j].key <L.r[k].key) 
        k=j;  
    }
    if (k!=i){
      L.r[i]<-> L.r[k]; 
    }
  } 
}

算法分析 移动次数:    最好情况:0     最坏情况:3(n-1)
比较次数:     时间复杂度:O(n²)      空间复杂度:O(1) 稳定性:稳定。

堆排序

基本思想:将待排序序列建成一个大根堆或者小根堆(这里以大根堆为例),易知根节点(即堆顶元素)为序列最大值,将根节点与最后一个叶子节点交换位置,输出堆顶元素。将剩下的元素再次建成一个大根堆,重复上述步骤。输出的序列即为一个有序序列。(下图过程举例)
在这里插入图片描述

算法分析 **
    时间效率:O(nlog2n)
    空间效率:O(1)
    稳 定 性:不稳定 适用于 n 较大的情况

归并排序

归并:将两个或两个以上的有序表组合成一个新有序表 2-路归并排序 排序过程 初始序列看成 n 个有序子序列,每个子序列长度为 1 两两合并,得到⌊n/2⌋个长度为 2 或 1 的有序子序列 再两两合并,重复直至得到一个长度为 n 的有序序列为止

例:初始关键字: [49] [38] [65] [97] [76] [13] [27]
一趟归并后: [38 49] [65 97] [13 76] [27]
二趟归并后: [38 49 65 97] [13 27 76]
三趟归并后: [13 27 38 49 65 76 97]
算法分析:
时间效率:O(nlog2n)
空间效率:O(n) (需要空间容量最大的排序算法)
稳定性:稳定

基数排序

最高位优先(Most Significant Digit first)法,简称MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。
最低位优先(Least Significant Digit first)法,简称LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。
**链式基数排序法:**首先对低位关键字排序,各个记录按照此位关键字的值‘分配’到相应的序列里。按照序列对应的值的大小,从各个序列中将记录‘收集’,收集后的序列按照此位 关键字有序。在此基础上,对前一位关键字进行排序。

最低位优先法:
    278, 109, 063, 930, 184, 589, 269, 008, 083
按 个 位 排 序     930, 063, 083, 184, 278, 008, 109, 589, 269
按 十 位 排 序     008, 109, 930, 063, 169, 278, 083, 184, 589
按 百 位 排 序     008, 063, 083, 109, 169, 184, 278, 589, 930

算法分析
n 个记录,每个记录有 d 位关键字,关键字取值范围 rd(如十进制为 10),重复执行 d 趟“分配”与“收集” 每趟对 n 个记录进行“分配”,对 rd 个队列进行“收集” 需要增加 n+2rd 个附加链接指针。

链式基数排序算法分析 时间效率:O(d( n+rd)) 空间效率:O(n+rd) 稳 定 性:稳定

排序算法的比较

在这里插入图片描述
排序算法比较
按平均时间排序方法分为四类
O(n2)、O(nlog2n)、O(n1+ε )、O(n) 。
快速排序是基于比较的内部排序中平均性能最好的。
基数排序时间复杂度最低,但对关键字结构有要求(知道各级关键字的主次关系、知道各级关键字的取值范围)
为避免顺序存储时大量移动记录的时间开销,可考虑用链表作为存储结构的:直接插入排序、归并排序、基数排序。
不宜采用链表作为存储结构的:
折半插入排序、希尔排序、快速排序、堆排序
排序算法选择规则 n 较大时
① 分布随机,稳定性不做要求,则采用快速排序 ② 内存允许,要求排序稳定时,则采用归并排序 ③ 可能会出现正序或逆序,稳定性不做要求,则采用堆排序或归并排序 。
n 较小时
① 基本有序,则采用直接插入排序 ② 分布随机,则采用简单选择排序,若排序码不接近逆序,也可以采用直接插入排序。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值