今天这篇主要讲讲数据结构里面常用的几种排序算法。
排序的基本概念假设现在有n个记录的序列{r1,r2,……,rn},其相应关键字分别为{k1,k2,……,kn},需要确定1,2,……,n的一种排列p1,p2,……,pn,使其相应的关键字蛮子kp1<=kp2<=kp3<=……<=kpn的关系,即使得序列成为一个按关键字有序排列的序列,这样的操作称为排序。
关键字又分为主关键字和次关键字,主关键字只有一个,但是次关键字个数不限。因为排序不仅针对主关键字,也会参考次关键字进行排序。因待排序的记录序列中可能存在两个或两个以上的关键字相等的记录,排序结果会出现不唯一的情况。如果两个序列值在按关键字排序前是A在B前面,如果经过排序后,A仍在B前面,则称排序是稳定的;若排序后A到了B的后面,则排序是不稳定的。
内排序是在排序的整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存中,整个排序过程需要在内外存之间多次交换数据才能进行。
排序总共有四种类别,七种算法,具体类别如下:
插入类排序重点在插入这两个字,具体是在一个已经有序的序列中,插入一个新的关键字,通过将待插入关键字与已经有序的序列中每个值进行比较,找到合适的插入位置,使插入后整个序列依然是有序的。直接插入排序、折半排序、希尔排序均属于这类排序。
交换类排序重点在交换这两个字,通过不停比较两个数值之间的大小然后交换两个数值的位置,重复这个过程,直到最后整个序列有序。冒泡排序和快速排序属于这类别排序。
选择类排序的重点在选择这两个字,从待排序序列中选出一个最小(或最大)的关键字,把它和序列中的第一个(或最后一个)关键字进行交换,这样最小(或最大)的关键字就排到了有序的位置,继续循环这个过程,直到整个序列有序。简单选择排序和堆排序属于这种排序类别。
归并类排序
归并类排序就是将两个或两个以上的有序序列合并成一个新的有序序列
用于排序的顺序表结构,此结构将会用于接下来要讲的所有顺序结构。
#define MAXSIZE 10 //要排序数组个数最大值typedef struct{
int r[MAXSIZE + 1]; //用来存储要排序数组 int length; //用来记录顺序表长度}
排序最常用的操作就是数组两元素的交换,我们将这个交换过程写成函数,方便之后的调用。
void swap(SqList *L,int i,int j){
int temp = L->r[i]; L->r[i] = L->r[j] L->r[j] = temp;}
插入类排序
直接插入排序类似于我们在打扑克的时候的理牌过程,我们一般都是一边接牌一边理牌,理牌的时候都是直接通过将新接的牌(待排序的数据)插入到前面已经排好序的序列中。插入排序的具体过程如下:
从第一个元素开始,默认第一个元素是已经排好序的
取出下一个元素作为待插入元素,然后将这个待插入元素与它前面的已经排好序的每一个元素(即已经插入到序列中的所有值)进行比较,如果待插入元素的值比它前面已经排序好的数值小,则将已经在序列中的数后移一位,直到不需要后移了,新元素插入到空位
重复上一步,直到待插入序列中的所有数值全部插入完毕
具体实现代码如下:
void Insertsort(int R[],int n)//R[]用来存放待插入排序的所有值{
int i,j; int temp; //用来存放待插入值 for(i = 1;i < n;++i) {
temp = R[i];//待插入值为序列R[]中的第i个值 j = i - 1;//待插入数值前一位 while(j>=0&&temp//当待插入值temp小于它前面的数时,前面的数就需要后移 {
R[j+1] = R[j]; --j;//通过前移遍历已排序好的序列中的每一个值 } R[j+1] = temp;//将temp插入到R[j+1]的位置 }}
折半排序是在直接插入排序的基础上进行改进的,直接插入是遍历待排序中的每一个值,而折半插入排序是采用每次寻找当前位置的一半进行插入排序,其实就是我们高中学的二分查找求根的方法。
直接插入排序在有些时候效率是很高的,比如待排序的数据本身是基本有序的,我们只需要执行少量的插入操作即可;或者是待排序的记录很少时,直接插入排序效率也是挺高。但是现实中的数据很难满足这两个条件,所以就需要人为去把数据整理成符合这两个条件的数据。
如何让待排序的记录个数较少呢?比较直接的方法就是将原来的大量记录数进行分组,分割成若干个子序列,这样待排序记录数就变少了,然后可以在各个组内直接利用插入排序,当整个序列基本有序时,再对全体记录进行一次直接插入排序。我们把这种方式称作希尔排序。
现在比较重要的问题就是如何对原数据进行分组,如果直接等距离切割的话,比如原序列是{9,1,5,8,3,7,4,6,2},现在把他们等距离切割成三份,{9,1,5},{8,3,7},{4,6,2},对这三组分别采用直接插入排序以后变成{1,5,9},{3,7,8},{2,4,6},然后将三组进行合并变成{1,5,9,3,7,8,2,4,6},这个序列目前还是杂乱的,还需要对这个序列整体再来一遍直接插入排序,但是这个经过切割以后序列和原序列并没有太大的不同,这主要是因为我们切割的时候是等距切割的。为了避免这种切割以后用处不大的情况,所以我们采用以某种相隔距离开始分组,比如选择1、3、5位置的数作为一组,2、4、6位置的数作为一组这样跳跃切割的方式进行。实现代码如下:
void ShellSort(SqList *L){
int i,j; int incremrnt = L->length; do {
incremrnt = increment/3+1; //间隔序列 for(i=increment+1;i {
if(L->r[i][i-increment]) {
L->r[0]=L->r[i]; for(j=i-increment;j>0&&L->r[0]r[j];j-=increment) L->r[j+increment]=L->r[j]; //记录后移,查找插入位置 L->r[j+increment]=L->r[0]; //插入 } } }}
交换类排序
冒泡排序是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序记录为止。
实现步骤如下:
从第一位开始,两两比较相邻数值,如果两个数值是降序,则交换彼此的位置,直到最后一位,这样最后一位肯定是最大的数值
因最后一位已经是最大值了,所以除最后一位外,其他数值再次执行上一步
重复上述的两步直到所有数值排序完成
遍历序列中的每一个值,然后将该值与其后面序列中的每个值作比较,如果大于则交换彼此位置。
void BubbleSort0(SqList *L){
int i,j; for(i=1;i {
for(j=i+1;j {
if(L->r[i] > L->r[j]) {
swap(L,i,j) } } }}
这种算法严格意义上并不是冒泡排序,因为他不满足两两比较相邻记录的冒泡排序思想。
(2)正规冒泡排序
正规冒泡排序执行的过程是按照我们前面的讲过的冒泡排序的基本步骤来的,具体实现代码如下:
void BubbleSort(SqList *L){
int i,j; for(i=1;i {
for(j=L-length-1;j>=i;j--) {
if(L->r[j] > L->r[j+1]) //注意这里是比较j与j+1 {
swap(L,j,j+1); //交换L->r[j]与L->r[j+1]的位置 } } }}
上面代码表示从序列的末尾依次往前遍历比较,先从倒数第二位与倒数第一位开始比较,i值用来控制最后不参加比较的数值的位数(即已经是最大值的位数),刚开始i值是1,表示所有数值数值均参与排序,当比较完一次(即j的for循环执行完一次)以后,i的值加1,而参与比较的数值个数减1,循环此过程,直到所有的数值均已排序完成(即i的值大于等于待排序序列的长度)。
我们上面讲的普通的冒泡排序中,只有最后一位不参与排序,除最后一位以外的其他序列还是都得参与排序,不管是否有序,如果除第一位以外的其他序列已经是全部或部分有序的,那么是不是就可以不去遍历比较了呢?答案肯定是的,只需要添加一个标志用来判断哪一部分序列是排好序的。
void BubbleSort2(SqList *L){
int i,j; Status flag = True; //flag用来标记哪部分是已排序好的 for(i=1,i {
flag = False; for(j=L->length-1;j>=i;j--) {
if(L->r[j] > L->r[j+1]) {
swap(L[j],L[j+1]) flag = True } } }}
终于到了传说中的快排了,快排是快速排序的简称,是交换类排序的其中一种。快速排序在开始排序之前会先选中一个中间值M(注意这里的中间值并非实际意义上的中值),一般会用待排序列中的第一个数,每执行一次排序会将待排序序列分成两部分,其中一部分中的所有数都要比中间值M大,而另一部分中的所有值要比中间值M小,在这两部分内再分别进行快排,也是同样先找一个中间值M,然后进行数区间切分,循环这个过程,直到所有的序列切分完毕,最后会得到一个有序的序列,具体实现代码如下:
void QuickSort(int R[],int low,int high){
int temp; int i = low;j = high; if(low < high) {
temp = R[low];//序列的第一个值作为中间值 while(i {
while(j>i&&R[j]>=temp)//从右往左寻找小于temp的值 --j; if(i {
R[i]=R[j];//放在temp左边 ++i;//i后移一位 } while(i//从左往右寻找大于temp的值 ++i; if(i {
R[j]=R[i];//放在temp右边 --j;//左移一位 } } } R[i] = temp;//将temp挡在最终位置 QuickSort(R,low,i-1);//对temp左边的值再执行快排 QuickSort(R,i+1,high);//对temp右边的值再执行快排}
选择类排序
冒泡排序是在一边比较一边交换,只要出现后面的值小于前面的值,就把两者进行调换,然后继续比较;而简单选择排序是比较某一位置的数与该位置之后的所有数,如果该位置之后序列中的数有比该位置的数小,则调换两者的位置,否则进入下一个循环,这个方法有点类似于基础的冒泡排序。简单选择排序与冒泡排序相比就是省略了很多交换的过程。具体实现代码如下:
void SelectSort(SqList *L){
int i,j,min; for(i=1;ilength;i++) {
min = i; for(j = i+1;j<=L->length;j++) {
if(L->r[min] > L->r[j]) //如果存在i后面的值比i值小 //则把该值传给参数min min = j; } if(i != min) //如果最小值不是i,则交换i和min的位置 swap(L,i,min); }}
前面的简单选择排序中,在待排序的n个记录中选择一个最小的记录需要比较n-1次,继续在剩下的n-1条记录里面比较n-2次才能得出第二个最小值。每次都需要比较剩下的所有值,然后从中挑选出最小值。但实际上有一些值之间是已经对比过了,就没必要再对比一次,如果要是可以针对已经比较过的值做一个调整,那样就可以避免很多不必要的比较啦。堆排序就是专门为了解决这个问题的,堆排序是改进版的简单选择排序。
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序就是利用堆进行排序的方法。它的基本思想是,将待排序的序列构成一个大顶堆。这样,整个序列的最大值就是堆顶的根节点。将根节点移走,根节点是最大值,然后再将剩余的n-1个序列重新构造成一个堆,新堆的根节点是新堆的最大值,也是这n个元素中的次大值。如此重复,便可得到一个有序序列。
所以堆排序其实就是两个步骤,第一步是将待排序数据转换成一个大堆顶,第二步就是逐步将每个最大值的根结点移走,并且再次调整为大顶堆。具体实现代码如下:
void HeapSort(SqList *L){
int i; for(i=L->length/2;i>0,i--) //将L中的r构建成一个大堆顶 HeapAdjust(L,i,L->length) //HeaPAdjust是将待排序数据调整为大顶堆的过程 for(i=L->length;i>1;i--) {
swap(L,1,i); //将堆顶记录和当前未经排序子序列的最后一个记录进行交换 HeapAdjust(L,1,i-1); }}HeapAdjust函数实现代码/已知L->r[s...m]中记录的关键字除L->r[s]之外均满足堆的定义//本函数调整L->r[s]的关键字,使L->r[s...m]成为一个大顶堆/void HeapAdjust(SqList *L,int s,int m){
int temp,j; temp = L->r[s]; for(j=2*s;j<=m;j*=2) //沿关键字较大的孩子结点向下筛选 {
if(jr[j]r[j+1]) ++j; //j为关键字中较大的记录的下标 if(temp>=L->r[j]) break; //rc应插入的位置s L->r[s]=L->r[j]; s=j; } L->r[s]=temp; //插入}
归并排序
归并有序是一种分而治之的算法,归并排序有多路归并,我们以最简单的二路归并进行讲解:先将整个序列分成两半,对每一半内分别再进行归并排序,这样将得到两个有序序列,然后将这两个有序序列归并成一个序列即可。具体实现代码如下:
void mergeSort(int A[],int low,int high){
if(low < high) {
int mid = (low + high)/2; mergeSort(A,low,mid); //归并排序前半段 mergeSort(A,mid+1,high);//归并排序后半段 merge(A,low,mid,high);//合并两个归并排序后的有序序列 }}
End.
作者:俊红的数据分析之路
· 零基础入职数据分析就业班 ·
课程形式主为 “直播+录播” 课程专享: 月考测试通关+课程项目作业+1v1职场生涯规划+班主任辅导学习+资深讲师答疑 课程结束后能熟练掌握 SQL、Python、Excel、PPT等数据分析工具 抓住4月复工热潮,爱数据助你拿理想offer!