1.排序的基本概念
排序的稳定性:假设Ki==Kj(1<=i<=n,1<=j<=n),且在排序前的序列中ri(记录)领先于rj。如果排序后ri仍领先于rj,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中rj领先于ri,则称所用的排序方法是不稳定的。
内排序和外排序:内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。根据排序过程中借助的主要操作,我们把内排序分为:插入排序、交换排序、选择排序和归并排序。
按算法的复杂度分为两类,冒泡排序、简单选择排序和直接插入排序属于简单算法,而希尔排序、堆排序、归并排序、快速排序属于改进算法。
排序常用到的结构:顺序表结构和交换函数
#define MAXSIZE 10 //要排序数组的个数最大值 typedef struct { int r[MAXSIZE+1]; 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->[j] = temp; }
2.冒泡排序
冒泡排序是一种交换排序,它的基本思想是两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
最简单的排序实现——交换排序:
//交换排序(冒泡排序初级版) void BubbleSort0(SqList *L) { for (int i=1; i<L->length; i++) { for (int j=i+1; j<=L->length; j++) { if (L->r[i] > L->r[j]) { swap(L, i, j); } } } }
这个算法需要大量的比较和交换,效率非常低。下面来看看正宗的冒泡算法
//冒泡排序 void BubbleSort(SqList *L) { for (int i=1; i<L->length; i++) { for (int j=L->length-1; j>=i; j--) //注意j是从后向前循环的 { if (L->r[j] > L->r[j+1]) { swap(L, j, j+1); } } } }
为了提高冒泡排序的性能,可以避免在有序后的无意义循环
//改进冒泡排序 void BubbleSort2(SqList *L) { bool flag = true; for (int i=1; i<L->length && flag; i++)//添加了交换标示 { flag = false; for (int j=L->length-1; j>=i; j--) //注意j是从后向前循环的 { if (L->r[j] > L->r[j+1]) { swap(L, j, j+1); flag = true; } } } }
算法复杂度,最好时只需做n-1次比较,时间复杂度为O(n);最坏的情况,需要做1+2+3+…+(n-1)=n(n-1)/2次比较和交换。因此,总的时间复杂度为O(n2)。
3.简单选择排序
简单选择排序法就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1~n)个记录交换之。
//简单选择排序 void SelectSort(SqList *L) { int minnum; for (int i=1; i<L->length; i++) { minnum = i; for (int j=i+1; j<=L->length; j++) { if (L->r[minnum] > L->r[j]) { minnum = j; } } if (i != minnum) { swap(L, i, minnum); } } }
从简单选择排序的过程来看,它的最大特点就是交换移动数据的次数很少,这样就节约了相应的时间。分析它的时间复杂度,无论最好最差的情况,其比较次数是一样多的,即(n-1)+(n-2)+...+1=n(n-1)/2次。而对于交换次数,最好时交换次数为0,最差时需要交换n-1次,因此总的时间复杂度依然是O(n2)。
应该说,尽管与冒泡排序同为O(n2),但是简单选择排序的性能上还是要略优于冒泡排序。
4.直接插入排序
直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
//直接插入排序 //思想如下,可根据具体需要改写 void InsertSort(SqList *L) { int j; for (int i=2; i<=L->length; i++) { if (L->r[i] < L->r[i-1]) { L->r[0] = L->r[i]; //这里设置“哨兵”,对具体问题中可以改成临时变量temp的形式 for (j=i-1; L->r[j]>L->r[0]; j--) { L->r[j+1] = L->r[j]; } L->r[j+1] = L->r[0]; } } }
复杂度分析,从空间上来看,它只需要一个记录的辅助空间,因此主要看它的时间复杂度。
当最好的情况下,也就是要排序的表本身就是有序的,那么需要比较的次数其实就是L.r[i]和L.r[i-1]的比较,共n-1次,此时没有移动的记录,时间辅助度为O(n)。
当最坏的情况下,即待排序的表是逆序的情况,此时需要比较2+3+...+n=(n+2)(n-1)/2次,而记录的移动次数也达到了最大值3+4+…+(n+1)=(n+4)(n-1)/2次。
可知:直接插入排序法的时间复杂度为O(n2),但是从记录的随机性角度来看,平均性能上,直接插入排序要优于冒泡和简单选择排序(复杂度:交换>移动)。
5.改进排序算法
内排序中改进算法包括:希尔排序,堆排序,归并排序和快速排序。这里主要介绍下希尔排序、堆排序和归并排序的思想,下篇博文再详细介绍常用的快速排序。
希尔排序简介
优秀的排序算法的首要条件就是速度,于是人们想了许多许多的办法,目的就是为了提高排序的速度。但是在很长的时间里,众人发现尽管各种排序算法花样繁多(比如上面的算法),但内排序算法的时间复杂度都是O(n2),此时的计算机界充斥着“排序算法不可能突破O(n2) ”的声音。在这种背景下,希尔排序的出现具有了特殊的意义。
希尔排序是D.L.Shell于1959年提出的一种排序算法,回顾前面的直接插入排序,应该说,它的效率在某些时候是很高的,比如,记录本身就是基本有序的,我们只需要少量的插入操作,就可以完成整个记录的排序工作,此时直接插入很有效;还有就是记录比较少时,直接插入的优势也比较明显。可问题在于,现实中记录少或者基本有序都属于特殊的情况。于是希尔研究了这种排序方法,对直接插入排序改进后增加了效率。
如何让待排序的记录个数较少呢?很容易想到的就是将原来大量记录数的记录进行分组,分割成若干个子序列,此时子序列的记录个数是比较少了,然后在这些子序列中分别进行直接排序,当整个序列都基本有序时,再对全体记录进行一次直接插入排序。为了实现“基本有序”的目标,希尔采用了跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样便保证了各子序列内分别进行直接插入得到的结果是基本有序而不是局部有序的,然后不断缩小“增量”的大小,最终实现序列的排序。
复杂度分析:这里“增量”的选取与更新是十分关键的,迄今为止还没人找到一种最好的增量序列。不过大量的研究表明,当增量序列为:时,可以获得不错的效果,其时间复杂度为O(n3/2),要好于直接排序的O(n2)。需要注意的是:增量序列的最后一个增量必须等于1才行,另外由于记录是跳跃式的移动,希尔排序并不是一个稳定的排序算法。
堆排序简介
前面讲到简单选择排序,它在待排序的n个记录中选择一个最小的记录需要比较n-1次。可惜的是,这样的操作并没有将每一趟的比较结果保存下来,在后一趟的比较中,有许多比较在前一趟中已经做过了,但是由于前一趟未保存这些比较的结果,所以后一趟又重复执行了这些比较结果,因而记录的比较次数较多。
如果可以做到每次在选择到最小记录的同时,并根据比较结果对其他结果做出相应的调整,那样排序的效率就会非常高了。堆排序就是对简单选择排序进行的一种改进,堆排序算法是Floyd和Williams在1964年共同发明的,同时,他们发明了“堆”这样的数据结构。
堆是具有下列性质的完全二叉树:每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆;或者每个节点的值都小于或等于左右孩子节点的值,称为小顶堆。
堆排序就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构建成一个大顶堆。此时,整个堆的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造一个堆,这样就会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列了。
复杂性分析:总体来说,堆排序的时间复杂度为O(nlogn),由于堆排序对原始记录的排序状态并不敏感,因此它无论最好、最坏和平均时间复杂度都是O(nlogn);空间复杂度上,它只需要一个用来交换的暂存单元,也非常的不错。
不过由于记录的比较是跳跃式进行的,因此堆排序也是一种不稳定的排序方法。另外,由于初始构建堆所需的比较次数较多,因此,它不适合待排序序列个数较少的情况。
归并排序简介
归并排序是一种更加直接简单的排序方法,”归并“一词的中文含义就是合并和并入的意思,而在数据结构中的定义是将两个或两个以上的有序表组合成一个新的有序表。
归并排序就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]([x]表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直到得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
时间复杂度:归并排序总的时间复杂度为O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。由于归并排序在归并过程中需要与原记录序列同样数量的存储空间存放归并结果,因此空间复杂度为O(n)。
另外,它需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法。总体来讲,归并排序是一种比较占内存,但却效率高且稳定的算法。