数据结构:排序(个人复习)

排序的概念

排序:将一组杂乱无章的数据按一定规律顺次排序起来。即,将无序序列排成一个有序序列(由小到大或由大到小)的运算

排序方法分类:

请添加图片描述

按存储介质分为:

  • 内部排序:数据量不大,数据在内存,无需内外存交换数据
  • 外部排序:数据量较大,数据在外存(文件排序);外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入内存,显然外部排序要复杂得多

按比较器个数分为:

  • 串行排序:单处理机(同一时刻比较一对元素)
  • 并行处理:多处理机(同一时刻比较多对元素)

按主要操作分为:

  • 比较排序:用比较的方法,插入排序,交换排序,选择排序,归并排序

  • 基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置

按辅助空间分为:

  • 原地排序:辅助空间用量为O(1)的排序方法(所占的辅助空间与参加排序的数据量大小无关)

  • 非原地排序:辅助空间用量超过O(1)的排序方法

按稳定性分为:

  • 稳定排序:能够使任何数值相等的元素,排序以后相对次序不变
  • 非稳定性排序:不是稳定排序的方法

按自然性分为:

  • 自然排序:输入数据越有序,排序的速度越快的排序方法1
  • 非自然排序:不是自然排序的方法

按排序依据原则

  • 插入排序:直接插入排序,折半插入排序,希尔排序
  • 交换排序:冒泡排序,快速排序
  • 选择排序:简单选择排序,堆排序
  • 归并排序:2-路归并排序
  • 基数排序

按排序所需工作量

  • 简单的排序方法: T(n)=O(n2)
  • 基数排序:T(n)=O(d.n)
  • 先进的排序方法:T(n)=O(nlogn)

存储结构

记录序列以顺序表存储

#define MAXSIZE 20   //设记录不超过20个
typedef int KeyType; //设关键字为整型量(int型)
Typedef struct{    //定义每个记录(数据元素)的结构
   int key;   //关键字
   InfoType otherinfo;   //其他数据项
}RedType;
Typrdef struct{   //定义顺序表的结构
   RedType r[MAXSIZE+1];   //存储顺序表的向量,r[0]一般作哨兵或缓冲区
   int length;   //顺序表的长度
}SqList;

一. 插入排序

基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。

即边插入边排序,保证子序列中随时都是排好序的

请添加图片描述

基本操作:有序插入

  • 在有序序列中插入一个元素,保持序列有序,有序长度不断增加
  • 起初,a[0]是长度为1的子序列。然后,逐一将a[1]至a[n-1]插入到有序子序列中

有序插入方法:

  • 在插入a[i]前,数组a的前半段(a[0]-a[i-1])是有序段,后半段(a[i]~a[n-1])是停留于输入次序的”无序段“
  • 插入a[i]使a[0]~a[i-1]有序,也就是要为a[i]找到有序位置j(0<=j<=i),将a[i]插入在a[j]的位置上

插入位置图示:

(a)插在中间

请添加图片描述

(b)插在最前面

请添加图片描述

(c)插在最后面

请添加图片描述

插入排序的种类:

请添加图片描述

1. 直接插入排序

采用顺序查找法查找插入位置,使用哨兵

具体思路:

  1. 复制为哨兵:L.r[0]=L.r[i];

请添加图片描述

  1. 记录后移,查找插入位置:for ( j = i-1; L.r[0].key < L.r[j].key; --j ) { L.r[j+1] = L.r[j]; }

请添加图片描述

  1. 插入到正确位置:L.r[j+1] = L.r[0];

请添加图片描述

算法实现:

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];   //复制为哨兵
          for(j=i-1;L.r[0].key<L.r[j].key;--j){
             L.r[j+1]=L.r[j];   //记录后移
          }
          L.r[j+1]=L.r[0];   //插入到正确位置         
       }
   }
}

性能分析:

实现排序的基本操作有两个:

(1)"比较"序列中两个关键字的大小

(2)"移动"记录

  • 最好的情况(关键字在记录序列中顺序有序):11 25 32 48 56 69 77 85 99

"比较"次数: ∑ i = 2 n 1 = n − 1 \sum_{i=2}^n 1=n-1 i=2n1=n1

"移动"次数:0

  • 最坏的情况(关键字在记录序列中逆序有序):85 74 66 52 45 41 22 12

"比较"次数: ∑ i = 2 n i = ( n + 2 ) ( n − 1 ) 2 \sum_{i=2}^n i = \frac{(n+2)(n-1)}{2} i=2ni=2(n+2)(n1)

"移动"次数: ∑ i = 2 n ( i + 1 ) = ( n + 4 ) ( n − 1 ) 2 \sum_{i=2}^n (i+1) = \frac{(n+4)(n-1)}{2} i=2n(i+1)=2(n+4)(n1)

  • 平均的情况

比较次数: ∑ i = 1 n − 1 i + 1 2 = ( n + 2 ) ( n − 1 ) 4 \sum_{i=1}^{n-1} \frac{i+1}{2} = \frac{(n+2)(n-1)}{4} i=1n12i+1=4(n+2)(n1)

移动次数: ∑ i = 1 n − 1 ( i + 1 2 + 1 ) = ( n + 6 ) ( n − 1 ) 4 \sum_{i=1}^{n-1} (\frac{i+1}{2}+1) = \frac{(n+6)(n-1)}{4} i=1n1(2i+1+1)=4(n+6)(n1)

时间复杂度:

原始数据越接近有序,排序速度越快

最好情况下(输入数据是顺序有序的)Tb(n) = O(n)

最坏情况下(输入数据是逆序有序的) Tw(n) = O(n2)

平均情况下,耗时差不多是最坏情况的一半 Te(n) = O(n2)

要提高查找速度:① 减少元素的比较次数 ②减少元素的移动次数

空间复杂度:辅助存储O(1)——需要一个哨兵

稳定性:稳定

2. 折半插入排序

查找插入位置时采用折半查找法

请添加图片描述

算法实现:

void BInsertSort(SqList &L){
   for(i=2;i<=L.length;++i){   //依次插入第2~第n个元素
      L.r[0]=L.r[i];   //当前插入元素存到"哨兵"位置
      low=1; high=i-1; //采用二分查找法查找插入位置
      while(low<high){
         mid=(low+high)/2;
         if(L.r[0].key<L.r[mid],key) high=mid-1;
         else low=mid+1;
      }//循环结束,high+1则为插入位置
      for(j=i-1;j>=high+1;--j) L.r[j+1]=L.r[j]; //移动元素
      L.r[high+1]=L.r[0];  //插入到正确位置
   }
}

算法分析:

折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快

它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。在插入第i个对象时,需要经过 ⌊ l o g 2 i ⌋ \lfloor log_{2}i \rfloor log2i+1次关键码比较,才能确定其插入位置

  • 当n较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差

  • 在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少

折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列

  • 减少了比较次数,但没有减少移动次数
  • 平均性能优于直接插入排序

时间复杂度:O(n2)

空间复杂度:辅助存储O(1)——需要一个哨兵

稳定性:稳定

3. 希尔排序

基本思想

先将整个待排记录序列分割成若干个子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序

希尔排序算法特点:

(1)缩小增量

(2)多遍插入排序

  1. 定义增量序列 D k D_{k} Dk D M D_{M} DM > D M − 1 D_{M-1} DM1 > … > D 1 D_{1} D1=1
  • 例子: D 3 D_{3} D3=5, D 2 D_{2} D2=3, D 1 D_{1} D1=1
  1. 对每个 D k D_{k} Dk进行“ D k D_{k} Dk-间隔” 插入排序(k=M,M-1,…,1)

请添加图片描述

  • 一次移动,移动位置较大,跳跃式地接近排序后地最终位置
  • 最后一次只需要少量移动
  • 增量序列必须是递减的,最后一个必须是1
  • 增量序列应该是互质

算法实现

void ShellSort(Sqlist &L,int dlta[],int t){
   //按增量序列dlta[0..t-1]对顺序表L作希尔排序
   for(k=0;k<t;++k)
      ShellInsert(L,dlta[k]);  //一趟增量为dlta[k]的插入排序
}
void ShellInsert(SqList &L,int dk){
   //对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子
   for(i=dk+1;i<=L.length;++i){
      if(r[i].key<r[i-dk].key){
         r[0]=r[i];
         for(j=i-dk;j>0&&(r[0].key<r[j].key);j=j-dk)
            r[j+dk]=r[j];
         r[j+dk]=r[0];
      }
   }
}

算法分析

希尔排序算法效率于增量序列的取值有关

时间复杂度

最好情况:O(n)

最坏情况:O(n2)

平均情况:~O(n1.3)

空间复杂度:O(1)

稳定性:不稳定

二. 交换排序

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

常见交换排序方法:① 冒泡排序O(n2) ②快速排序O(n l o g 2 log_{2} log2n)

1. 冒泡排序

基本思想:每趟不断将记录两两比较,并按“前小后大”规则交换

请添加图片描述

算法实现

void bubble_sort(SqList &L){   
   int i,j,flag; //flag作为是否有交换的标记
   RedType x;   //交换时临时存储
   for(i=1; i<=n-1 && flag==1; m++){
      flag=0;
      for(j=1;j<n-i;j++){
         if(L.r[j].key>L.r[j+1].key){  //发生逆序
            flag=1;  //发生交换,flag设置为1,若没有发生交换,flag保持为0
            x=L.r[j]; L.r[j]=L.r[j+1]; L.r[j+1]=x;  //交换
         }
      }
   }
}

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

一旦某一躺比较时不出现记录交换,说明已排好序了,就可以结束算法

时间复杂度

最好情况(正序):O(n)

比较次数:n-1;

移动次数:0

最坏情况(逆序):O(n2)

比较次数: ∑ i = 1 n − 1 ( n − i ) = n 2 − n 2 \sum_{i=1}^{n-1} {(n-i)}= \frac{ n^2 - n }{2} i=1n1ni=2n2n ;

移动次数: 3 ∑ i = 1 n − 1 ( n − i ) = 3 ( n 2 − n ) 2 3\sum_{i=1}^{n-1} {(n-i)}= \frac{3( n^2 - n) }{2} 3i=1n1ni=23(n2n)

平均情况:O(n2)

空间复杂度

冒泡排序算法中增加一个辅助空间temp,辅助空间为S(n)=O(1)

稳定性:稳定

2. 快速排序

任取一个元素为中心(枢轴),所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表,对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个(递归思想)

请添加图片描述

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

具体实现:选定一个中间数作为参考,所有元素与之比较,小的调到其左边,大的调到其右边

中间数:可以是第一个数,最后一个数,最中间一个数,任选一个数等

算法实现

① 每一趟的子表的形成是采用从两头向中间交替式逼近法

②由于每趟中对各子表的操作都相似,可采用递归算法

请添加图片描述

void QSort(SqList &L,int low,int high) {
   if(low<high){
      pivotloc=Partition(L,low,high);
      //将L.r[low..high]一分为二,pivotloc为枢轴元素排好序的位置
      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;
}
void main(){
   QSort(L,1,L.length);   
}

算法分析

时间复杂度

  • 平均计算时间为 O(n l o g 2 log_{2} log2n)
  • 最好情况下:O(n l o g 2 log_{2} log2n)
  • 最坏情况下:O(n2)

就平均计算时间而言,快速排序是所讨论的所有内排序方法中最好的一个

空间复杂度:快速排序不是原地排序(由于使用了递归调用了栈,而栈的长度取决于递归调用的深度)

  • 在平均情况下:需要O( l o g 2 log_{2} log2n)的栈空间
  • 最坏情况下:栈空间可达O(n)

稳定性:不稳定

快速排序不适于对原来有序或基本有序的记录序列进行排序

划分元素的选取是影响时间性能的关键

  • 输入数据次序越乱,所选划分元素值的随机性越好,排序速度越快,快速排序不是自然排序方法
  • 改变划分元素的选取方法,至多只能改变算法平均情况下的时间性能,无法改变最坏情况下的时间性能,即最坏情况下,快速排序的时间复杂度总是O(n2)

三. 选择排序

1. 简单选择排序

基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置

请添加图片描述

算法实现

void SelectSort(SqList &K){
   for(i=1;i<L.length;++i){
      k=i;
      for(j=i+1;j<=L.length;j++){
         if(L.r[j].key<L.r[k].key)  k=j;  //记录最小值位置
         if(k!=i) L.r[i]<-->L.r[k];  //交换
      }
   }
}

算法分析

时间复杂度:O(n2)

① 记录移动次数

  • 最好情况:0
  • 最坏情况:3(n-1)

②比较次数:无论待排序列处于什么状态,选择排序所需进行的“比较”次数都相同

空间复杂度:O(1)

稳定性:不稳定

2. 堆排序

堆的定义:若n个元素的序列{a1,a2…an}满足

请添加图片描述

则分别称该序列{a1,a2…an}为小根堆大根堆

从堆的定义可以看出,堆实质是满足如下性质的完全二叉树:二叉树中任一非叶子结点均小于(大于)它的孩子结点

请添加图片描述

若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列又建成一个堆,则得到n个元素的次小值(次大值)…如此反复,便能得到一个有序序列,这个过程称之为堆排序

堆的调整

如何在输出堆顶元素后,调整剩余元素为一个新的堆?

小根堆:
1.输出堆顶元素之后,以堆中最后一个元素替代之:
2.然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换
重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选

请添加图片描述

算法实现

void HeapAdjust(elem R[],int s,int m){
   rc=R[s];
   for(j=2*s;j<=m;j*=2){
      if(j<m&&R[j]<R[j+1]) ++j;   // 沿key较大的孩子结点向下筛选
      if(rc>=R[j]) break;  //j为key较大的记录的下标
      R[s]=R[j];  s=j;  //rc应插入在位置s上
   }
   R[s]=rc;  //插入
}

对一个无序序列反复“筛选”就可以得到一个堆,即:从一个无序序列建堆的过程就是一个反复“筛选”的过程

显然:
单结点的二叉树是堆;
在完全二又树中所有以叶子结点(序号i>n/2)为根的子树是堆。这样,我们只需依次将以序号为n/2,n/2-1,……1的结点为根的子树均调整为堆即可。
即:对应由n个元素组成的无序序列,“筛选”只需从第n/2个元素开始。

由于堆实质上是一个线形表,那么我们可以顺序存储一个堆

将初始无序的R[1]到R[n]建成一个小根堆,可用以下语句实现:

for(i=n/2;i>=1;i--) HeapAdjust(R,i,n);

堆排序算法如下:

void HeapSort(elem R[]){   //对R[1]到R[n]进行堆排序
   int i;
   for(i=n/2;i>=1;i--){
      HeapAdjust(R,i,n);  //建初始堆
   }
   for(i=n;i>1;i--){  //进行-1趟排序
      Swap(R[1],R[i]);  //根于最后一个元素交换
      HeapAdjust(R,1,i-1);  //对R[1]到R[i-1]重新建堆
   }
   
}

算法分析

初始堆化所需时间不超过O(n)

排序阶段(不含初始堆化)

  • 一次重新堆化所需时间不超过O( l o g 2 log_{2} log2n)

  • n-1次循环所需时间不超过O(n l o g 2 log_{2} log2n)

  • 堆排序的时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上。堆排序在最坏情况下,其时间复杂度也为0(n l o g 2 log_{2} log2n),这是堆排序的最大优点。无论待排序列中的记录是正序还是逆序排列,都不会使堆排序处于"最好"或“最坏“的状态。

堆排序仅需一个记录大小供交换用的辅助存储空间

稳定性:不稳定

堆排序不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的

四. 归并排序

基本思想:将两个或两个以上的有序子序列“归并”为一个有序序列

在内部排序中,通常采用的是2-路归并排序

请添加图片描述

如上称为归并树,整个归并排序仅需 ⌈ l o g 2 n ⌉ \lceil log_{2} n \rceil log2n

算法实现

请添加图片描述

算法分析

时间复杂度:O(n l o g 2 log_{2} log2n)

空间复杂度:O(n)——需要一个与原始序列同样大小的辅助序列(R1),此算法的缺点

稳定性:稳定

五. 基数排序

基本思想:分配+收集

也叫桶排序箱排序:设置若干个箱子,将关键字k的记录放入第k个箱子,然后在按序号将非空的连接

基数排序:数字是有范围的,均由0-9这十个数字组成,则只需设置十个箱子,相继按个、十、百…进行排序

请添加图片描述

算法分析
时间复杂度:O(k*(n+m))

  • k:关键字个数
  • m:关键字取值范围为m个值

空间复杂度:O(n+m)

稳定性:稳定

各排序方法综合比较

一.时间性能

1.按平均的时间性能来分,有三类排序方法:

  • 时间复杂度为O(nlogn)的有:快速排序,堆排序和归并排序,其中快速排序为最好
  • 时间复杂度为O(n2)的有:直接插入排序,冒泡排序和简单选择排序,其中直接插入最好
  • 时间复杂度为O(n)的有:基数排序

2.当待排记录序列按关键字顺序有序时,直接插入排序和冒泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间性能退化为O(n2),因此是应该尽量避免的情况。

3.简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。

二、空间性能

指的是排序过程中所需的辅助空间大小

1.所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为O(1)

2.快速排序为O(logn),为栈所需的辅助空间

3.归并排序所需辅助空间最多,其空间复杂度为O(n)

4.链式基数排序需附设队列首尾指针,则空间复杂度为O(rd)

三、排序方法的稳定性

1.稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。

2.当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。

3.对于不稳定的排序方法,只要能举出一个实例说明即可。

4.快速排序和堆排序是不稳定的排序方法。

四、关子“排序方法的时间复杂度的下限“

本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法,可以证明,这类排序法可能达到的最快的时间复杂度为O(nlogn)。

(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)。

可以用一棵判定树来描述这类基于“比较关键字”进行排序的排序方法。

在这里插入图片描述

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值