排序算法
最近打算重新复习和总结排序算法,参考文章有:
排序的基本概念与分类
假设含有n个记录的序列为 r1,r2,⋯,rn ,其相应的关键字分别为 k1,k2,⋯,kn ,需要确定 1,2,⋯,n 的一种排列 p1,p2,⋯,pn ,使其相应的关键字满足 kp1≤kp2≤⋯≤kpn 非递减(或非递增)关系,即使得序列成为一个按关键字有序的序列 rp1,rp2,⋯,rpn ,这样的操作就称为排序。
在排序问题中,通常将数据元素称为记录。
排序的依据是关键字之间的大小关系,那么,对同一个记录集合,针对不同的关键字进行排序,可以得到不同序列。
这里关键字 ki 可以是记录 r 的主关键字,也可以是次关键字,甚至是若干数据项的组合。
排序的稳定性
由于排序不仅是针对主关键字,还有针对次关键字,因为待排序的记录序列中可能存在两个或两个以上的关键字相等的记录,排序结果可能会存在不唯一的情况,下面给出稳定与不稳定排序的定义。
假设
ki=kj (1≤i≤n,1≤j≤n,i≠j) ,且在排序前的序列中 ri 领先于 rj (即 i<j )。如果排序后 ri 仍领先于 rj ,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中 rj 领先于 ri ,则称所用的排序方法是不稳定的。不稳定的排序算法有:希尔、快速、堆排和选择排序。
内排序和外排序
根据在排序过程中待排序的记录是否全部被放置在内存中,排序可以分为:内排序和外排序。
内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
对于内排序来说,排序算法的性能主要是受到3个方面的影响:
时间性能
在内排序中,主要进行两种操作:比较和移动。高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。
辅助空间
辅助存储空间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间。
算法的复杂性
这里指的是算法本身的复杂度,而不是算法的时间复杂度。
根据排序过程中借助的主要操作,我们把内排序分为:插入排序、交换排序、选择排序和归并排序。
排序用到的结构与函数
这里先提供一个用于排序用的顺序表结构,这个结构将用于接下来介绍的所有排序算法。
#define MAXSIZE 10 typedef struct { // 用于存储待排序数组 int r[MAXSIZE]; // 用于记录顺序表的长度 int length; }SqList;
此外,由于排序最常用到的操作是数组两元素的交换,这里写成一个函数,如下所示:
// 交换L中数组r的下标为i和j的值 void swap(SqList *L, int i, int j){ int temp = L->r[i]; L->r[i] = L->r[j]; L->r[j] = temp; }
冒泡排序
简介
冒泡排序(Bubble sort)是一种交换排序。它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,知道没有反序的记录为止。
算法描述和分析
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要 O(n2) 次交换,而插入排序只要最多 O(n) 交换。冒泡排序的实现通常会对已经排序好的数列拙劣地执行( O(n2) ),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到 O(n) 。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。
最差时间复杂度 O(n2) 最优时间复杂度 O(n) 平均时间复杂度 O(n2) 最差空间复杂度 总共 O(n) ,需要辅助空间O(1) 算法代码实现
首先介绍一个简单版本的冒泡排序算法的实现代码。
// 冒泡排序初级版 void BubbleSort0(SqList *L){ int i, j; for (i = 0; i < L->length - 1; i++) { for (j = i + 1; j <= L->length - 1; j++){ if (L->r[i] > L->r[j]){ // 实现递增排序 swap(L, i, j); } } } }
这段代码不算是标准的冒泡排序算法,因为不满足“两两比较相邻记录”的冒泡排序思想,它更应该是最简单的交换排序。它的思路是让每一个关键字都和后面的每一个关键字比较,如果大或小则进行交换,这样关键字在一次循环后,第一个位置的关键字会变成最大值或者最小值。
这个最简单的实现算法效率是非常低的。
下面介绍正宗的冒泡排序算法实现。
// 正宗的冒泡排序算法实现代码 void BubbleSort(SqList *L){ int i, j; for (i = 0; i < L->length; i++) { for (j = L->length - 2; j >= i; j--){ // j是从后往前循环 if (L->r[j] > L->r[j + 1]){ // 实现递增排序 swap(L, j, j + 1); } } } }
这里改变的地方是在内循环中,
j
是从数组最后往前进行比较,并且是逐个往前进行相邻记录的比较,这样最大值或者最小值会在第一次循环过后,从后面浮现到第一个位置,如同气泡一样浮到上面。这段实现代码其实还是可以进行优化的,例如待排序数组是
{2,1,3,4,5,6,7,8,9}
,需要进行递增排序,可以发现其实只需要交换前两个元素的位置即可完成,但是上述算法还是会在交换完这两者位置后继续进行循环,这样效率就不高了,所以可以在算法中增加一个标志,当有一次循环中没有进行数据交换,就证明数组已经是完成排序的,此时就可以退出算法,实现代码如下:
// 改进版冒泡算法 void BubbleSortOptimz(SqList *L){ int i, j; bool flag = true; for (int i = 0; i < L->length && flag; i++){ // 若 flag为false则退出循环 flag = false; for (j = L->length - 2; j >= i; j--){ // j是从后往前循环 if (L->r[j] > L->r[j + 1]){ // 实现递增排序 swap(L, j, j + 1); // 如果有数据交换,则flag是true flag = true; } } } }
完整的冒泡排序算法代码可以查看BubbleSort。
鸡尾酒排序/双向冒泡排序
简介
鸡尾酒排序等于是冒泡排序的轻微变形。不同的地方在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目。
算法描述和分析
- 依次比较相邻的两个数,将小数放在前面,大数放在后面;
- 第一趟可得到:将最大数放到最后一位。
- 第二趟可得到:将第二大的数放到倒数第二位。
如此下去,重复以上过程,直至最终完成排序。
鸡尾酒排序最糟或是平均所花费的次数都是 O(