710 likes | 821 Views
第九章 内部排序. 对数据元素集合建立某种有序排列的过程称为排序。 本章介绍的排序算法都是基于待排序的数据元素序列构成的线性表采用顺序存储结构,即 采用数组存储,并且按关键字非递减排序(升序)。. 9.1 概 述 一、排序的基本概念: 排序 (Sorting) 是指将一个数据元素(或记录)的任意序列,按规定顺序重新排列成一个按关键字有序的序列。 排序的目的是便于查询和处理,提高解决问题的效率。它是计算机程序设计中的一种重要操作,如字典是典型的排序结构,将字或词按字母顺序排列,便于查询。. 排序的定义 :
E N D
第九章 内部排序 对数据元素集合建立某种有序排列的过程称为排序。 本章介绍的排序算法都是基于待排序的数据元素序列构成的线性表采用顺序存储结构,即采用数组存储,并且按关键字非递减排序(升序)。
9.1 概 述 一、排序的基本概念: 排序(Sorting)是指将一个数据元素(或记录)的任意序列,按规定顺序重新排列成一个按关键字有序的序列。 排序的目的是便于查询和处理,提高解决问题的效率。它是计算机程序设计中的一种重要操作,如字典是典型的排序结构,将字或词按字母顺序排列,便于查询。
排序的定义 : 假设含n个记录的序列为: {R1,R2,…,Rn} (9-1) 其相应的关键字序列为: {K1,K2,…,Kn} (9-2) 需确定1,2,…,n的一种排列p1,p2,…,pn,使其 相应的关键字满足如下的非递减(或非递增)关系: Kp1≤Kp2≤…≤Kpn 即使式(9-1)的序列成为一个按关键字有序的序列 {Rp1,Rp2,…,Rpn} 这样一种操作称为排序。
上述排序定义中的关键字Ki可以是记录Ri ( i=1,2,…,n)的主关键字,也可以是记录Ri的次关键字,甚至是若干数据项的组合。若Ki是主关键字,则任何一个记录的无序序列经排序后得到的结果是唯一的;若Ki是次关键字,则排序的结果不唯一,因为待排序的记录序列中可能存在两个或两个以上关键字相等的记录。 假设Ki=Kj(1≤i≤ n,1≤j≤n,i!=j),且在排序前的序列中Ri领先于Rj(即i<j)。若在排序后的序列中 Ri仍领先于Rj,则称所用的排序方法是稳定的;反之,若可能使排序后的序列中Rj领先于Ri,则称所用的排序方法是不稳定的。 排序的稳定性是衡量排序方法的一个重要指标。
二、排序的分类 按待排序记录所在位置分为: • 内部排序:指的是待排序记录存放在计算机随机存储器中进行的排序过程; • 外部排序:排序过程中需对外存进行访问的排序。 按排序策略划分内部排序方法: • 插入排序:直接插入排序、折半插入排序、希尔排序 • 交换排序:冒泡排序、快速排序 • 选择排序:简单选择排序、堆排序 • 归并排序:2路归并排序 • 基数排序
按稳定性划分: • 在排序过程中,如果关键字相同的两个元素的相对次序不变,则称为稳定的排序,否则是不稳定的排序方法.。 • 在排序过程中,如果关键字相同的两个元素的相对次序改变,则称则是不稳定的排序。
三、内部排序的分类 • 插入排序:从未排序序列中依次取出元素与已排序序列中的元素进行比较,将其放入已排序序列中的正确位置上的排序方法。 • 交换排序:当每两个元素比较出现逆序时,就相互交换位置的排序方法。 • 选择排序:从未排序序列中挑选元素,并将其放入已排序序列的一端的方法。 • 归并排序:依次将每两个相邻的有序表合并成一个有序表的排序方法。 • 基数排序:借助多关键字排序的思想对单逻辑关键字进行排序的方法。
在每类排序方法中又有许多不同的算法,这些算法各有优缺点,没有一个绝对“最优”的算法。对于一个具体的排序问题,要根据记录原始排列状态的特点以及问题的处理要求,选取相应合适的算法,或者将几种方法结合使用。在每类排序方法中又有许多不同的算法,这些算法各有优缺点,没有一个绝对“最优”的算法。对于一个具体的排序问题,要根据记录原始排列状态的特点以及问题的处理要求,选取相应合适的算法,或者将几种方法结合使用。
四、排序算法的分析指标 • 1. 在排序过程中需进行的操作: • (1)比较两个关键字的大小; • (2)将记录从一个位置移动到另一个位置。 • 为了讨论方便,设待排序的一组记录存储在地址连续的一组存储单元上,设关键字均为整数,待排序记录的数据类型定义如下: • #define MAXSIZE 20 //顺序表的最大长度 • typedef int KeyType; //定义关键字类型为整数类型 • typedef struct { • KeyType key; //关键字项 • InfoType otherinfo; //其它数据项 • }RcdType; • typedef struct { • RcdType r[MAXSIZE+1]; // r[0] 闲置或用作哨兵单元 • int length; //顺序表长度 • }SqList; //顺序表类型
2. 评价排序算法好坏的标准: ① 时间性能分析 以算法中用得最多的基本操作的执行次数(或者其数量级)来衡量,这些操作主要是比较元素、移动或交换元素。在一些情况下,可能还要用这些次数的平均数来表示。 ②空间性能分析排序算法的空间性能主要是指在排序过程中所占用的辅助空间的情况,若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间是O(1),则称之为就地排序。
9.2 插入排序 插入排序方法是从未排序序列中依次取出元素与已排序序列中的元素进行比较,将其放入已排序序列中的正确位置上的排序方法。 一、 直接插入排序 (Straight Insertion Sort) 基本思想是:假设待排序的记录存放在数组R[1..n]中。初始时,R[1]自成1个有序区,无序区为R[2..n]。从i=2起直至i=n为止,依次将R[i]插入当前的有序区R[1..i-1]中,生成含n个记录的有序区。 当插入第i (i 1) 个记录时,前面的 R[1], …, R[i-1]已经排好序。这时,用R[i]的关键字与R[i-1], R[i-2], … R[1] 的关键字顺序进行比较,找到插入位置即将R[i]插入,插入位置之后的所有记录依次向后移动。
第i趟直接插入排序的操作为:在含有i-1个记录的有序子序列R[1..i-1]中插入一个记录后,变成含有i个记录的有序子序列R[1..i];并且和顺序查找类似,为了在查找插入位置的过程中避免数组下标出界,在R[0]处设置监视哨。在自i-1起往前搜索的过程中,可以同时后移记录。第i趟直接插入排序的操作为:在含有i-1个记录的有序子序列R[1..i-1]中插入一个记录后,变成含有i个记录的有序子序列R[1..i];并且和顺序查找类似,为了在查找插入位置的过程中避免数组下标出界,在R[0]处设置监视哨。在自i-1起往前搜索的过程中,可以同时后移记录。 整个排序过程为n-1趟插入,即先将序列中第1个记录看成是一个有序子序列,然后从第2个记录开始,逐个进行插入,直至整个序列有序。
直接插人排序算法描述: 方法:Ki与Ki-1,Ki-2,…K1依次比较,直到找到应插入的位置。 void InsertSort (SqList&L){ //对顺序表L作直接插入排序。 for(i=2;i<=L.length;++i) if LT(L.r[i].key,L.r[i-1].key){//“<”,需将L.r[i]插入有序子表 L.r[0]=L.r[i] //复制为哨兵 L. r[i] = L. r[i-1];//后移 for ( j=i-2 ; LT(L.r[0].key,L.r[j].key); --j) L.r[j+1]=L.r[j]; //记录后移 L.r[j+1]=L.r[0] // 插入到正确位置 } }//Insertsort
算法评价: • 时间复杂度 • 1.若待排序记录按关键字从小到大排列(正序) 记录不需要移动 2.待排序记录按关键字从大到小排列(逆序) • 3. 若待排序记录是随机的,取平均值: • 关键字比较次数:约n2/4, 记录移动次数:约n2/4 • 所以,时间复杂度:T(n)=O(n²) • 空间复杂度: S(n)=O(1) 直接插入排序是稳定的排序方法。
待排元素序列: (53) 27 36 15 69 42 第一趟排序: (27) (27 53) 36 15 69 42 第二趟排序: (36)(27 36 53) 15 69 42 第三趟排序: (15) (15 27 36 53) 69 42 第四趟排序: (15) (15 27 36 53 69) 42 第五趟排序: (42) (15 27 36 42 53 69) 直接插入排序 例:一组关键字序列(53,27,36,15,69,42),请用直接插入排序的方法进行排序。
二、折半插入排序 (Binary Insertion sort) 在直接插入排序中,为了找到插入位置,采用了顺序查找的方法。由于插入排序的基本操作是在一个有序表中进行的查找和插入,为了提高查找速度,可以采用折半查找,这种排序称折半插入排序。折半插入排序在寻找插入位置时,不是逐个比较而是利用折半查找的原理寻找插入位置。待排序元素越多,改进效果越明显。 折半插入排序基本思想是:设在顺序表中有一个序列R[1], …, R[n]。其中R[1], …, R[i-1] 是已经排好序的序列。在插入 R[i] 时,利用折半查找法寻找 R[i] 的插入位置。
算法描述: void BInsertSort(SqList &L) //对顺序表L作折半插入 { for (i=2; i<= L.length; ++i){ L.r[0]=L.r[i]; //r[0]作为监视哨 low =1; high = i-1; while (low<=high) { //在r[low..high]中折半查找有序插入的 位置 m=(low+high)/2; if ( LT(L.r[0]. key, L[m]. key)) high = m-1; //插入点在低 半区 else low = m+1; //插入点在高半区 } //while for ( j = i-1; j>=high+1;--j) L. r[j+1]=L. r[j]; //记录后移 L. r[high+1] = L. r[0]; //插入 } // for }// BInsertSort
算法分析: 折半插入排序所需附加存储空间和直接插入排序相同,从时间上比较,折半插入排序仅减少了关键字间的比较次数,而记录的移动次数不变。因此,折半插入排序的时间复杂度仍为O(n2)。 折半插入排序是稳定的排序方法。
三、希尔排序 希尔排序(Shell’s Sort)又称“缩小增量排序”(Diminishing Increment Sort),它也是一种插入排序类的方法,但在时间效率上较前述两种排序方法有较大的改进。 从对直接插入排序的分析得知,其算法时间复杂度为O(n2),但是,若待排记录序列为“正序”时,其时间复杂度可提高至O(n)。由此可设想,若待排记录序列按关键字“基本有序”,即序列中具有下列特性 L.r[i].key < Max{L.r[j].key} 1≤j<i 的记录较少时,直接插入排序的效率就可大大提高,从另一方面来看,由于直接插入排序算法简单,则在n值很小时效率也比较高。希尔排序正是从这两点分析出发对直接插入排序进行改进得到的一种插入排序方法。
基本思想: 先将整个待排对象序列按照一定间隔分割成为若干子序列,分别进行直接插入排序,然后缩小间隔,对整个对象序列重复以上的划分子序列和分别排序工作,直到最后间隔为1,此时整个对象序列已 “基本有序”,进行最后一次直接插入排序。 排序过程: 先设置一个正整数d1,称为增量,d1<n,把所有相隔d1的记录放在同一组,例如,若d1=5,则记录R1,R6,R11…放在同一组,记录R2,R7,R12…放在同一组中,如此类推,将整个待排记录序列分割成若干子序列。然后对各组内的记录分别进行直接插入排序,这样的一次分组排序过程称为一趟希尔排序。再设置另一个新的增量d2,使d2<d1,采用上述方法进行第二趟排序……如此进行下去,当某次排序时设置的增量di=1时,表明序列中全部记录放在了同一组内,该次排序结束时,整个序列的排序工作完成。
void ShellInsert (SqList &L,int dk) { //对顺序表L作一趟希尔插入排序。本算法是和一趟直接插入排序相比,作了以下修改: //1、前后记录位置的增量是dk,而不是1; //2、r[0]只是暂存单元,不是哨兵。当j<=0时,插入位置已找到。 for (i=dk+1;i<=L.length;++i) if LT(L.r[i].key,L.r[i-dk].key){ //需将L.r[i]插入有序增量子表 L.r[0]=L.r[i]; //暂存在L.r[0] for (j=i-dk;j>0&<(L.r[0].key,L.r[j].key);j-=dk) L.r[j+dk]=L.r[j] ; //记录后移,查找插入位置 L.r[j+dk]=L.r[0]; //插入 }//if }//ShellInsert
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]的插入排序 }//ShellSort 增量序列可以有各种取法,但需注意应使增量序列中的值没有除1之外的公因子,并且最后一个增量值必须等于1。
希尔排序特点: 1. 子序列的构成不是简单的“逐段分割”,而是将相隔某个 增量的记录组成一个子序列; 2. 希尔排序可提高排序速度,因为 • 分组后n值减小,而T(n)=O(n²),所以T(n)从总体上看是减小了; • 关键字较小的记录跳跃式前移,在进行最后一趟增量为1的插入排序时,序列已基本有序; • 3. 是不稳定的排序方法; • 4. 时间复杂度约为O(n3/2) • 5. 增量序列的取法:不含除1以外的公因子,并且最后一个增量值必须为1。
9.3 快速排序 本节讨论借助“交换”进行排序的方法,其中有起泡排序和快速排序两种。 交换排序的基本思想是:两两比较待排序对象的关键字,如果发生逆序(即排列顺序与排序后的次序正好相反,即L.r[1].key> L.r[2].key),则交换之,直到所有对象都排好序为止。
一、起泡排序(Bubble Sort冒泡排序): 设待排序对象序列中的对象个数为n。首先将待排序序列中第一个记录的关键字和第二个记录的关键字进行比较,若为逆序(即L.r[1].key >L.r[2].key),则将两个记录交换;然后比较第二个记录和第三个记录的关键字。以此类推,直至第n-1个记录和第n个记录的关键字进行过比较为止。上述过程称为第一趟起泡排序,其结果使得关键字最大的记录被安置到最后一个记录的位置上。然后进行第二趟起泡排序,对前n-1个记录进行同样操作。直到在某趟排序过程中没有进行过交换记录的操作为止。 一般地,第i趟起泡排序从L.r[1].key到L.r[n-i+1].key依次比较相邻两个记录的关键字,如果发生逆序,则交换之,其结果是这n-i+1个记录中,关键字最大的记录被交换到第n-i+1的位置上。整个排序过程序最多进行n-1趟。
21 16 08 21 21 21 16 08 16 25 25 25 08 21 21 49 25 16 25 25 25 25 16 08 25 25 25 16 08 25 08 49 49 49 49 49 初 始 关 键 字 第 一 趟 排 序 第 二 趟 排 序 第 三 趟 排 序 第 四 趟 排 序 第 五 趟 排 序 起泡排序的过程
算法描述: • void Bubble-Sort (Sqlist &L ,int n) { • //将L中整数序列重新排列成自小到大有序的整数序列 • for (i=n-1, change = TRUE; i>=1 && change; - -i) { • change = FALSE; • for (j = 0; j< i; ++j ) • if (L.r[j].key> L.r[j+1].key) { • L.r[j].key←→ L.r[j+1].key; change=TURE; } • } } • 算法评价: • 若初始序列为“正序”序列:只需进行一趟排序,在排序过程中进行n-1次比较,不移动记录,时间复杂度为O(n)。 • 若初始序列为“逆序”序列:需进行n-1趟排序,在排序过程中进行n(n-1)/2次比较,并作等数量级的记录移动,时间复杂度为O(n2)。 • 起泡排序适合于数据较少的情况,是稳定的排序方法。
二、快速排序(Quick Sort): 快速排序(Quick Sort)是对起泡排序的一种改进。它的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行快速排序,以达到整个序列有序。 1. 一趟快速排序: 假设待排序的序列为{L.r[s],L.r[s+1],…,L.r[t]},首先选取第一个记录L.r[s]作为枢轴(或支点)(pivot),然后按下述原则进行一趟快速排序:附设两个指针low和high ,初值分别指向第一个记录和最后一个记录,设枢轴记录的关键字为 pivotkey ,首先从 high所指位置起向前搜索,找到第一个小于pivotkey的记录与枢轴记录互相交换,然后从low所指位置起向后搜索,找到第一个大于pivotkey的记录与枢轴记录互相交换,重复这两步直至low=high为止。
经过一趟快速排序之后,无序序列R[s..t]将分割成两部分:R[s..i-1]和R[i+1..t], 凡关键字小于枢轴的记录均移动至该记录之前,反之,凡关键字大于枢轴的记录均移动至该记录之后。 即: R[j].key≤ R[i].key ≤ R[k].key 枢轴 i+1≤k≤t s≤j≤i-1
一趟快速排序的算法描述: int Partition(SqList &L, int low, int high) { //交换顺序表L中子表L.r[low..high]的记录,使枢轴记录到位,并返回其所在位置,此时在它之前(后)的记录均不大(小)于它。 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[low] ←→L.r[high]; //将比枢轴记录大的记录交换到高端 } return low; //返回枢轴所在位置 }//Partition 每交换一对记录需进行三次记录移动,实际上,当low=high的位置才是枢轴的最后位置
改写上述算法,先将枢轴记录暂存在r[0]的位置上,排序过程中只作r[low]或r[high]的单向移动,直至一趟排序结束后再将枢轴记录移至正确位置上。
算法描述如下: int Partition(SqList &L,int low,int high){ //交换顺序表L中子表r[low..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; //返回枢轴位置 }//Partition
2. 快速排序: 首先对无序的记录序列进行“一次划分”,之后分别对分割所得两个子序列“递归”进行快速排序。 无 序 的 记 录 序 列 一次划分 无序记录子序列(1) 枢轴 无序子序列(2) 分别进行快速排序
递归形式的快速排序算法: void QSort(SqList &L,int low,int high){ //对顺序表L中的子序列L.r[low..high]作快速排序 if (low<high){ //长度大于1 pivotloc = Partition(L,low,high); //进行一次划分 QSort(L,low,pivotloc-1); //对低子表递归排序 QSort(L,pivotloc+1,high); //对高于表递归排序 }//QSort void QuickSort(SqList &L){ //对顺序表L作快速排序,第一次调用函数 Qsort 时,待排序记录序列的上、下界分别为 1 和 L.length。 QSort(L,1,L.length); }//QuickSort
3. 算法评价: • 时间复杂度 T(n)=O(nlog2n) • 空间复杂度:需用栈空间以实现递归 • 最坏情况:S(n)=O(n) • 一般情况:S(n)=O(log2n) • 对某个无序元素序列进行快速排序时,每次划分选择的枢轴(基准元素)为该序列的中间值时,得到的两个子区间是均匀的。 • 快速排序的平均性能优于前面讨论的各种排序方法,是目前被认为最好的一种内部排序方法,是不稳定的排序方法。
9.4 选择排序 选择排序(Selection Sort)的基本思想是:每一趟在n-i+1(i=1,2,…,n-1)个记录中选取关键字最小的记录,作为有序序列中的第i个记录。 常用的有简单选择排序(Simple Selection Sort)和堆排序。 一、简单选择排序 排序过程: • 首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换; • 再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记录交换; • 重复上述操作,共进行n-1趟排序后,排序结束。
假设排序过程中,待排记录序列的状态为: 有序序列R[1..i-1] 无序序列R[i..n] 从中选出 关键字最小的记录 第 i 趟 简单选择排序 有序序列R[1..i] 无序序列R[i+1..n]
算法: void SelectSort (SqList &L) { //对顺序表L作简单选择排序 for (i=1; i < L. length; ++i ) { //选择第i个小的记录,并交换到位 j=SelectMinKey(L, i); //在L. r[i .. L.length] 中选择key最小的记录 if ( i! = j; L. r[i] ←→ L. r[j]; //与第i个记录交换 } } 算法性能分析: 无论记录的初始排列如何,所需移动的记录的次数较少,最小值为0,最大值为3(n-1)。所需进行的关键字之间的比较次数均为n(n-1)/2, 时间复杂度T(n)=O(n²),空间复杂度S(n)=O(1)。 简单选择排序是一种不稳定的排序方法。
二、堆排序 在堆排序中,把待排序的序列逻辑上看作是一棵完全二叉树,并用到堆的概念。堆排序(HeapSort)只需要一个记录大小的辅助空间,每个待排序的记录仅占有一个存储空间。 1. 堆的定义: n个元素的序列{k1,k2,…,kn}当且仅当满足下关系时,称之为堆。 ki≤k2i ki≥k2i 或 ( i = 1, 2 ,…, n/2) ki≤k2i+1, ki≥k2i+1 前者称为小顶堆(小根堆),后者称为大顶堆(大根堆)。
96 12 83 27 36 24 38 11 09 85 47 30 53 91 若将和此序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。例如,下列两个序列为堆,对应的完全二叉树如图所示。 {96,83,27,38,11,09} {12,36,24,85,47,30,53,91} 大根堆小根堆
2. 堆排序 将无序序列建成一个堆,得到关键字最小(或最大)的记录;输出堆顶的最小(大)值后,使剩余的n-1个元素重新又建成一个堆,则可得到n个元素的次小值;重复执行,得到一个有序序列,这个过程叫堆排序。 由此,实现堆排序需要解决两个问题: (1)如何由一个无序序列建成一个堆? (2)如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
先讨论第二个问题,如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?先讨论第二个问题,如何在输出堆顶元素之后,调整剩余元素成为一个新的堆? 使用“筛选”的方法:输出堆顶元素之后,以堆中最后一个元素替代它;然后将根结点值与左、右子树的根结点值进行比较,并与其中较小者进行交换;重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”。
第一个问题的解决方法:如何由一个无序序列建成一个堆?第一个问题的解决方法:如何由一个无序序列建成一个堆? 从一个无序序列建堆的过程就是一个反复“筛选”的过程。若将此序列看成是一个完全二叉树,则最后一个非终端结点是第n/2个元素,由此“筛选”只需从第n/2个元素开始。 例:初始序列为(25,56,49,78,11,65,41,36),建 立初始堆(小根堆 )