目录

一、排序定义

二、插入排序——直接插入排序

三、插入排序——折半插入排序

四、插入排序——希尔排序(缩小增量排序)

五、交换排序——冒泡排序

六、交换排序——快速排序

七、选择排序——简单选择排序

八、选择排序——堆排序

九、堆的插入删除

十、归并排序

十一、基数排序

十二、排序算法的比较



一、排序定义

1 排序定义

排序,就是把一堆数据元素,按照关键字的递增或者递减的关系把它们排列,即经过排序之后,数据元素的关键字要么递增要么递减(即“有序”)。有时候关键字会相同,这时候就会引出算法的稳定性的问题。

2 稳定性

带排序表中关键字相同的元素,其相对次序在排序前后不变,这称这个排序算法是稳定的。算法是否稳定并不能衡量一个算法的优劣。如果带排序表中的关键字均不重复,则排序结果是唯一的,算法的稳定性就无关紧要。

大部分的内部排序都需要执行比较和移动操作。通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。

在基于比较的排序方法中,每次比较两个关键字大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以得出结论:当文件的 n 个关键字随机分布时,任何借助比较的排序算法,至少需O(nlog 2n)的时间。——这个log 2n是以2为底的对数,全篇都是这么表示。

算法的稳定性总结见十一。

3 排序分类:

(1)插入排序(直接插入排序、折半插入排序、希尔排序)

(2)交换排序(冒泡排序、快速排序)

(3)选择排序(简单选择排序、堆排序)

(4)归并排序(二路归并、多路归并)

(5)基数排序

二、插入排序——直接插入排序

1. 插入排序的描述
插入排序是一种简单直观的排序方法,其基本思想是将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成

插入排序通常采用就地排序,在从后向前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素腾出插入空间。

2. 代码和示例
下面是直接插入排序的代码

#include <stdio.h>
int main()
{//建立一个原始的乱序的数组int A[]={3,4,6,2,1};printf("排序之前: \n");int m;for(m=0;m<5;m++){printf("%d",A[m]);}printf("\n");//核心算法思想。每个数字都和已经排好序的进行比较int i ,j ,temp,len;len=sizeof(A)/sizeof(0);//得到数组的长度for(i=1;i<len;i++){if(A[i]<A[i-1]){  //如果它比前面一个数字小,就进入排序的算法temp=A[i];  //先用temp保存它for(j=i-1;j>=0 && A[j]>temp;j--){  //循环的条件是数值比temp大A[j+1]=A[j];  //比temp大的数值统统往后移动}A[j+1]=temp; //数值没有比temp大的时候,就插入temp}}//检验是否排序成功:printf("排序之后: \n");int n;for(n=0;n<5;n++){printf("%d",A[n]);}return 0;
}
//以下是王道原代码。
void InsertSort(int A[], int n){int i ,j ,temp;for(i=1;i<n;i++){//从数组第二个数开始比较,比较n-1次if(A[i]<A[i-1]){//如果它小于它前面的数,就开始处理temp=A[i]; //先用temp暂时保存for(j=i-1;j>=0 && A[j]>temp;j--){ //j指针从i-1处开始往前面移动,如果j所指大于temp,就继续向前移动A[j+1]=A[j];  //路上的元素往后移动}A[j+1]=temp;  //找到位置,存好temp的值}}}

代码逻辑:

(1)i指针指向第二个元素,意思是从第二个元素开始处理。循环n-1次。

(2)比较i位置和i-1位置的元素,如果i位置的更小,那么就用temp暂时缓存i的值,然后查找插入的位置。

(3)设立j指针指向i前面的元素,如果j的值比temp大, 那么j的值就向后移动,移动完数值再把j指针往前移动,一直循环,直到j的值小于等于temp的值,然后把temp的值插入到j+1的位置。

运行结果:

3. 空间效率
仅使用了常数个辅助单元,空间复杂度为O ( 1 )

4. 时间效率
排序过程中,向有序子表中逐个插入元素的操作进行了n-1趟;每趟操作都分为比较关键字和移动元素,次数取决于待排序表的初始状态。

在最好情况下,表中元素已经有序,此时每插入一个元素,都只需一次比较而不需要移动元素。时间复杂度为O ( n ) ;
在最坏情况下。表中元素的顺序刚好与排序结果相反(逆序),总的比较次数达到最大为O(n²)
在平均情况下,考虑待排序表中的元素是随机的,此时取最好与最坏情况的平均值作为平均情况的时间复杂度。

5. 稳定性
在每次查找插入位置的时候,是temp的值比前面的值小,才会移动,所以如果是相同的两个数值,查找的指针是不会移动的,所以算法是稳定的。

6. 适用性
直接插入排序算法适用于顺序储存(大部分排序算法仅适用于顺序储存的线性表)和链式储存。当采用链表的时候,查找的次数和顺序表相同,但是移动的次数减少了,因为只需要修改几个指针就可以。

其更适用于基本有序、数据量不大的排序表。但是,它有缺点,也就是每次查找的次数会多很多。下面的算法是对直接插入排序进行的优化。

三、插入排序——折半插入排序

1. 描述
上个直接插入排序当中,因为子表已经排好序了,所以没有必要从头到尾比较来查找,在查找插入点的时候可以采用折半插入,可以减少查找的次数。

2 代码如下:

#include <iostream>
using namespace std;void InsertSort(int A[],int n){ //n是数组长度int i,j,low ,high,mid;for(i =2;i<=n;i++){           //i是从待排序序列中拿出来的数值。   依次将A[2]~A[n]插入前面的已排序序列(注意这里是从2开始,这里是用哨兵的方法)A[0]=A[i];             //A[0]处暂时存放A[i]low=1;high=i-1;         //设值折半查找的范围(默认递增有序)//用一个while循坏,用折半查找方法查找插入位置while(low<=high){mid=(low+high)/2; //取中间点if(A[0]<A[mid]) high=mid-1; //查找左半子表(记忆:先左后右)else low=mid+1; //查找右半子表。} //重点:最终,high指向了小于等于A[0]的位置。//把high后的元素统一后移for(j = i-1;j>=high+1;j--){A[j+1]=A[j];//统一后移元素,空出插入位置}//把A[0]插入high+1处A[high+1]=A[0];  }
}int main()
{   //1 建立一个原始的乱序的数组int A[]={0,7,9,2,1};//0第一个元素作为哨兵没有用处printf("排序之前: \n");int len_1=sizeof(A)/sizeof(0);//得到数组的长度int m;for(m=1;m<len_1;m++){ //第一个元素(也就是数组下标为0),只是作为哨兵,所以不打印printf("%d",A[m]);}printf("\n");//2 调用插入排序方法InsertSort(A,4);//3 校验是否排序成功printf("排序之后: \n");int n;for(n=1;n<5;n++){printf("%d",A[n]);}return 0;}

运行 结果:

3.空间效率

和直接插入排序一样,仅使用了常数个辅助单元,空间复杂度为O ( 1 ) 。

4. 时间效率
相对于直接插入排序,折半插入排序仅仅减少了比较元素的次数,没有减少移动的次数,所以时间复杂度仍为O(n²)

但对于数据量不是很大的排序表,折半插入排序往往表现出很好的性能。

5. 稳定性
和折半插入一样,在移动的过程中, 只有前面一个数大于temp,才会移动,所以如果遇到相等的情况,就不会移动。所以折半插入排序也是一种稳定的排序方法。

6 适用性

和直接插入排序不一样,折半插入只适用于顺序表,不能用于链表因为链表不支持随机查找,它不能随意定位到low mid high这些点处的数值。

四、插入排序——希尔排序(缩小增量排序)

备注:408考试中,对代码考察频率不高,通常考法是给出增量序列,分析每一趟排序后的状态。
1. 描述


基本思想:先设值一个增量——一般最开始是等于总数量的一半。根据增量将待排序表划分成若干特殊子表,然后对各个子表进行直接插入排序;再缩小增量,重复上述过程,直到增量=1为止。当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。

希尔排序和直接插入排序有很大的关系,希尔排序可以看作是为了让直接插入排序能更好的发挥作用而做的优化。总结来说,希尔排序是先追求表中元素部分有序,再逐渐逼近全局有序。

2. 过程和代码

希尔排序的过程如下:

第一步:取d=n/2;

第二步:待排序表分割成若干形如[ i , i + d , i + 2 d , ⋯   , i + k d ] ;

第三步:在各个组内进行直接插入排序;

第四步:缩小d,即让d=d-1;重复二、三的步骤;

第五步:不断缩小d,直到d= 1 ,即所有记录已放在同一组中,再进行一次直接插入排序。

由于此时已经具有了较好的局部有序性,故可以很快得到最终结果。

希尔排序的代码如下:

#include <stdio.h>
//核心算法部分。void ShellSort(int A[],int n){ //n是数组长度int i ,j ,temp,d;for(d=n/2;d>=1;d=d/2){  //每次最外的循环计算了一个d,d会不断变小。for(i=d+1;i<=n;i++){  //根据某一个d,进行分组排序if(A[i-d]>A[i]){  //在子表中,如果前面的数值大于后面的数值temp=A[i];  //用temp暂存后面的数值//移动前面的元素for(j=i-d;j>=0 && temp<A[j];j-=d){A[j+d]=A[j];}//插入A[j+d]=temp;}}}
}int main()
{//建立一个原始的乱序的数组int A[]={0,7,9,2,1,4,5,8,23,22,34,11,54};//第一个只是作为哨兵printf("排序之前: \n");int len_1=sizeof(A)/sizeof(0);//得到数组的长度int m;for(m=1;m<len_1;m++){ //不打印哨兵printf("%d ",A[m]);}printf("\n");int len;len=sizeof(A)/sizeof(0); //获得数组长度ShellSort(A,len);//检验是否排序成功:printf("排序之后: \n");int len_2=sizeof(A)/sizeof(0); //得到数组的长度for(int n=2;n<=len_2;n++){ //不知道为啥n=2的时候才正确printf("%d ",A[n]);}return 0;
}

运行的结果:

3. 空间效率
仅使用了常数个辅助单元,空间复杂度为O ( 1 )

4. 时间效率
目前无法用数学手段证明它的时间复杂度,但是当n在某个特定范围时,希尔排序的平均时间复杂度约为O(n^{1.3})。在最坏情况下,希尔排序的时间复杂度为O(n²)。不过总体而言,这个算法还是比较优化的。

5. 稳定性
当相同关键字的记录被划分到不同子表时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序方法。

6. 适用性
希尔排序对较大规模的排序都可以达到很高的效率。

仅适用于顺序存储的线性表。因为我们需要用增量d快速找到与之相邻的、从属于同一个子表的各元素,所以必须要有随机访问的个性。

五、交换排序——冒泡排序

1. 描述
所谓交换,是指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。

冒泡排序的基本思想:从后往前(或从前往后)两两比较相邻元素的值,若为逆序则交换,直到序列比较完,这称为一趟冒泡。

冒泡排序中所产生的有序子序列一定是全局有序的,每趟排序都会将一个元素放在其最终位置。

举例子,目标是进行从小到大排序,从后往前冒泡,那么一趟冒泡的结果是将最小的元素放到最前面,第二趟冒泡把第二大的元素放到第二个位置,从第二趟开始,已确定位置的元素不再参与比较,所以第二趟最多做n-1次移动。总体冒泡的趟数最多是n-1。

结束冒泡的时机:某一趟冒泡结束后,元素的相对位置都不变。

2. 代码和示例

需要写一个交换的函数swap。
冒泡排序算法代码如下

#include <stdio.h>
#include <stdbool.h>
void MaopaoSort(int A[],int len){int i , j;for(i=0;i<=len-1;i++){ //i每增加一次,说明一趟冒泡结束bool flag=false;   //用来标记算法是否要结束for(j=len-1;j>i;j--){ //j是要处理的数。在这一趟冒泡里,是从后面往前冒泡,所以是j=len-1。//如果两个数,如果前面一个比后面一个大,就对调if(A[j-1]>A[j]){   //对调int temp=A[j];A[j]=A[j-1];A[j-1]=temp;flag=true;  //只要有交换就改成true。说明算法还没有结束。}}if(flag==false) return; //算法结束的标志是false。意思是一趟结束后,flag都没有变成true。说明}
}int main()
{   //建立一个原始的乱序的数组int A[]={7,9,2,1,4,5,8,5,23,22,34,11,54};printf("排序之前: \n");int len_1=sizeof(A)/sizeof(0);//得到数组的长度int m;for(m=0;m<len_1;m++){printf("%d ",A[m]);}printf("\n");//核心代码int len =sizeof(A)/sizeof(0);MaopaoSort(A,len);//检验是否排序成功:printf("排序之后: \n");int len_2=sizeof(A)/sizeof(0);//得到数组的长度int n;for(n=0;n<len_2;n++){printf("%d ",A[n]);}return 0;
}

运行结果


3. 空间效率
仅使用了常数个辅助单元,空间复杂度为O ( 1 )

4. 时间效率
时间复杂度为O(n²)

5. 稳定性
冒泡排序时一种稳定的排序方法。
如果把代码中判断是否逆序的条件由“>”改为“≥”,则算法变得不稳定。

六、交换排序——快速排序

1. 描述
快速排序的基本思想是基于分治法的:在待排序表中选取一个元素,称为枢轴(或称基准,常取首元素)。通过一趟排序,将待排序表分成两部分,一部分中所有元素均小于枢轴,另一部分元素均大于枢轴,两部分分别位于枢轴元素的两侧,这个过程称为一趟快速排序(或一次划分)。然后递归地分别对两个子表重复上述过程,直到每部分只有一个元素或空为止,此时所有元素都放在了最终位置。

快速排序并不产生有序子序列,但每趟排序后会将枢轴元素放在最终位置上。

2. 代码和示例

快速排序的代码题是408考试中频率最高的一个,必须好好看。
一趟快速排序是一个交替搜索和交换的过程,算法如下

#include <stdio.h>
#include <stdbool.h>
int partition(int A[], int low, int high); //被调用的函数如果不在前面,必须先声明一下,否则报错。void quickSort(int A[], int low, int high){if(low < high){  //low和high初始的意义是指向待排序的数组的两头//一趟快排,将表划分为两个子表,返回枢轴位置int pivotpos = partition(A, low, high); //调用这个函数一次,就相当于把基准放到最终的位置quickSort(A, low, pivotpos-1);  //对左子表进行递归quickSort(A, pivotpos+1, high);  //对右子表进行递归}
}//partition是某一趟划分,处理基准元素。low和high指针不断向中间移动,直到low和high重合,然后把基准放到low/high的位置。
int partition(int A[], int low, int high){int pivot = A[low];  //设为基准while(low < high){  while(low < high && A[high] >= pivot) //把high指针不断往前移动,找到小于枢轴的元素high--;A[low] = A[high]; //把小于枢轴的元素放到左端while(low < high && A[low] <= pivot) //把low指针不断向后移动,找到大于枢轴的元素low++;A[high] = A[low]; //把大于枢轴的元素放到右端}A[low] = pivot;  //将枢轴元素置入交替搜索后留出的空位中。return low;  //返回枢轴位置
}int main()
{   //建立一个原始的乱序的数组int A[]={7,9,2,1,4,5,8,5,23,22,34,11,54};printf("排序之前: \n");int len_1=sizeof(A)/sizeof(0);//得到数组的长度int m;for(m=0;m<len_1;m++){printf("%d ",A[m]);}printf("\n");quickSort(A,0,len_1-1);//检验是否排序成功:printf("排序之后: \n");int len_2=sizeof(A)/sizeof(0);//得到数组的长度int n;for(n=0;n<len_2;n++){printf("%d ",A[n]);}return 0;
}

3. 空间效率
快排是递归地,需要借助一个递归工作栈来保持每层递归调用的必要信息(变量、地址),容量与递归调用的最大深度一致。空间复杂度=O(递归层数)。递归层数最大是n,最小是log2n。

所以:最好的空间复杂度是O(log2n),最坏的情况是O(n)

4. 时间效率
时间复杂度=O(n*递归层数)。递归层数最大是n,最小是log2n

所以:最好的时间复杂度是O(n*log2n),最坏的情况是O(n²)。

在实际中,它的时间复杂度是接近最好的情况,也就是O(n*log2n)

快速排序是所有内部排序中平均性能最优的排序算法

5. 稳定性
某一趟中,两个关键字相同的元素,从一个区间被交换到另一个区间的过程中,相对位置会发生变化。快速排序是一种不稳定的排序方法。

6.优化

根据二叉树的特性,如果数值刚好顺序或者逆序,那么时间复杂度就是O(n²)。为了提高性能,在选取枢轴的时候,最好能将序列划分成为均匀的两部分。所以,在选取枢轴的时候,可以选头、尾、中间值,再比较一下,然后采用数值大小居中的元素作为枢轴;也可以采用随机选择的办法,这样就可以避免刚好顺序或者逆序。

七、选择排序——简单选择排序

1. 描述

选择排序的基本思想:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序序列

简单选择排序的过程:

第一趟:把整个无序的序列扫描,找出最小的元素和第一个位置元素互换,待排序的元素减小一个。

第二趟:在待排序的序列中扫描最小的,和当前待排序序列的第一个元素互换,待排序的序列再减少;

重复上述操作,直到待排序的序列元素个数为0。

2. 代码

代码思路:变量i指向待排序序列的第一个元素,min指向最小的元素,一开始min和i相同。
从i+1开始从左往右扫描待排序序列,找到最小元素记录为min ,然后交换i和min的值。

简单选择排序代码如下

#include <stdio.h>
#include <stdbool.h>
int main()
{
//建立一个原始的乱序的数组int A[]={7,9,2,1,34,11,54};printf("排序之前: \n");int len_1=sizeof(A)/sizeof(0);//得到数组的长度int m;for(m=0;m<len_1;m++){printf("%d ",A[m]);}printf("\n");//核心代码int len =sizeof(A)/sizeof(0);for(int i =0;i<len;i++){int min =i;for(int j =i+1;j<len;j++){if(A[j]<A[min]) min = j;}if(min!=i){int temp=A[i];A[i]=A[min];A[min]=temp;}}//检验是否排序成功:printf("排序之后: \n");int len_2=sizeof(A)/sizeof(0);//得到数组的长度int n;for(n=0;n<len_2;n++){printf("%d ",A[n]);}return 0;
}

运行效果:

3. 空间效率
仅使用常数个辅助单元,空间效率为O(1)

4. 时间效率
这个算法,因为是全盘扫描找最小值,所以不管初始状态是什么(不管你是顺序还是逆序),它的时间复杂度始终是 O(n^2)

其他算法,初始状态不同,时间复杂度可能就不一样(比如快速排序)。

5. 稳定性
在第 i 趟把最小元素和第 i 个元素进行交换时,可能导致第 i 个元素与其后含有相同关键字元素的相对位置发生变化。简单选择排序是不稳定的

6.适用性

简单选择排序的思想是全部扫描,没有用到随机查找,所以顺序表和链表都适用。

八、选择排序——堆排序

堆排序是重要考点!!

注意:本章的代码和主要的逻辑以大根堆为例。

1. 描述

(1)堆的定义

堆分为大根堆和小根堆。数组下标为i的节点如果同时大于数组下标为2i和2i+1的节点,那么称为大根堆(1<=i<=n/2,即这个节点是分支节点);数组下标为i的节点如果同时小于数组下标为2i和2i+1的节点,那么称为小根堆。

堆,从内存视角、物理视角来看的话是连续存放的数组,但是其实从逻辑视角看的话是应该理解为一颗顺序存储的完全二叉树。编号为1的节点就是完全二叉树的根节点,数组下标为i的节点的左孩子是2i,右孩子是2i+1,因为我们规定了1<=i<=n/2,所以它一定是分支节点而不是叶子结点。所以从二叉树的视角看,在完全二叉树中,所有子树的根节点都大于它的左、右孩子节点的值,那么就称这个完全二叉树为大根堆。小根堆就是所有子树的根节点都小于左、右孩子节点的值。

回忆一下BST(二叉排序树):左≤根≤右。

(2)堆排序的思想

选择排序的思想就是从待排序的序列中找到最小或者最大值插入到有序序列,而堆这样的结构非常容易找到最大最小值。(大根堆最大值在根,小根堆最小值在根)。所以如果我们把待排序的序列做成堆,就非常容易排序。

所以总结堆排序的思路:一、建立堆;二、然后在每一趟排序中,将堆顶元素加入有序子序列,也就是将它与待排序序列末位互换;三、并将待排序序列再次调整为堆(元素下坠)(为啥要下坠?因为由于刚刚堆顶元素是最大的元素,而末端元素本来是挺小的,末端元素被换到堆顶之后,就不符合大根堆的性质,所以要让这个较小的元素不断下坠。)

基于大根堆的排序最终得到的是递增序列(因为每次都是将堆顶元素和待排序序列最后一个 元素交换),小根堆得到的是递减序列。

(3)建堆(构造初始堆)的思想

把非终端节点(一些人叫做非叶子节点)都检查一遍,是否满足大根堆的要求,如果不满足,就进行调整。(非终端节点的定义:在顺序存储的完全二叉树中,i≤n/2向下取整。)

具体步骤:
第一:n个结点的完全二叉树,从最后一个非叶结点开始往前检查(如下图中,从数组中下标i=4的位置,也就是09这个结点开始,),以大根堆为例,若根结点关键字小于左右孩子,将左右孩子中较大者与之交换(如果是小根堆,那就是把较小一个移动到根)。

第二:交换完i=4,再检查i=3,2,1。(这步骤称为“大元素上升”)

第三:交换后可能破坏下一级的堆,则采用相同的方法继续向下调整。(这步称为:“小元素下落")。

(备注:i的左孩子——2i;i的右孩子——2i+1;i的父节点——i/2 向下取整)

2.建立大根堆的代码实现

//建立大根堆
void buildMaxHeap(int A[], int len){for(int i = len/2; i > 0; i--)  // i指向要调整的非叶节点。从最小非叶结点开始,反复调整堆headAdjust(A, i, len);  //调用函数,调整为大根堆
}//将以k为根的子树调整为大根堆
void headAdjust(int A[], int k, int len){  //k是要调整的非叶节点。len是参与调整的界限A[0] = A[k];  // 暂存子树根节点for(int i = 2*k; i <= len; i *= 2){ // i指向左孩子。i *=2的意思是沿key值较大的结点往下,即小元素下坠。 i <= len是向下调整的终止条件if(i<len && A[i+1]>A[i])  // i<len的作用是保证k有右孩子i++;  // 这时i 指向左右孩子中较大的节点。if(A[0] >= A[i])  // 再拿较大的孩子和根节点对比,如果根节点更大,就结束这次循环break;A[k] = A[i];  // 否则就交换结点,把较大的孩子节点和根节点互换k = i;  // k指向了没有交换前i的位置,然后让i *=2,也就是进入第二次循环,以此来向下检查,让小元素不断下坠。}A[k] = A[0];//小元素下坠到最后,k指向了小元素最终下坠的位置,这步也是将被筛选的节点的值放入最终位置
}//堆排序的完整逻辑
void HeapSort(int A[],int len){buildMaxHeap(A,len);  //初始建堆(建堆函数中就有调整为大根堆)for(int i =len;i>1;i--){ //n-1趟将堆顶和堆底互换swap(A[i],A[1]);headAdjust(A,1,i-1);//互换结束后,要调整。为什么是i-1,因为i不参与调整了。}
}

3.空间复杂度=O(1)。

4.时间复杂度
(1)在建堆时,需要调用”调整函数”关键字的比较总次数不超过4n,建堆的时间复杂度为O(n)

(2)在排序时,每下坠一层,最多只需要对比关键字2次(一次是左右孩子对比找出最大值,第二次是最大值和根对比),所以是常数级别的对比,所以一趟下坠调整和高度相关。根节点最多下坠h-1层,根据二叉树的性质,下坠的高度h=log2n,所以每趟下坠的时间复杂度是O(log2n)。而总共需要n-1躺排序,两者相乘,即log2n*(n-1),堆排序的时间复杂度是是nlog2n。

(3)建堆的时间复杂度是n,排序是nlog2n,取最大值,所以堆排序的时间复杂度是nlog2n。

5.稳定性

(1)第一趟排序

最开始如图:

看代码,i指针最开始指向左孩子。如果左右孩子相等,那么就不会发生i++,所以根据代码,1和左孩子的2进行互换,互换后如下图:

接着堆顶和堆底元素互换,完成第一趟排序,就变成如下图:

(2)第二趟排序

根据上个截图我们可以看到,剩余的元素依然是个大根堆,于是进行堆底和堆顶互换,就变成如下图:

到此完成了n-1躺排序,到此结束。我们发现两个2互换,所以说堆排序是不稳定的。

九、堆的插入删除

注意:第八章以大根堆为例,本章以小根堆为例

1. 插入结点
对于小根堆,将新元素放在堆尾,与父节点相比,如果比父节点更小,则两者互换。新元素就这样一直上升,直到无法继续上升为止。(常考知识点:上升一次,关键字只对比1次

2. 删除结点
堆的删除通常在根节点处,在删除的位置用堆尾元素替代,然后让它不断调用“调整函数”来调整,让堆恢复成小根堆的性质。(常考知识点:下坠一次,关键字要对比2次

3. 算法与示例
下面给出堆排序算法,即依次删除根节点的算法

void heapSort(ElemType A[], int len){buildMaxHeap(A, len);  // 建立初始堆// 进行n-1趟交换和建堆过程。当i=1时,仅剩根节点,此时数组已经有序for(int i = len; i > 1; i--){  // 输出堆顶元素(和堆底元素进行交换),此时数组中i~len的元素已经是全局有序的了swap(A[i], A[1]);  headAdjust(A, 1, i-1);  // 把剩余i-1个元素元素整理成堆}
}

十、归并排序

也是一个重要的算法。
1. 描述

归并:把两个或者多个有序的序列合并称为一个有序序列。“二路归并”就是把2个有序合并成一个有序;“多路归并”就是把4个有序序列合并成一个有序序列。


二路归并的思想:假定待排序表含有n 个记录,则可将其视为n个有序的子表,每个子表长度为1。然后两两归并,得到长度为2的有序序列。再将得到的长度为2的有序表两两归并,如此重复,直到合并成一个长度为n的有序表为止。

2. 代码和示例

(1)口述代码:

主体函数使用了归并的方式:对一个无序的序列,从中间拆分成左右两个部分,对左右两个部分分别进行递归的归并排序,当左右两个部分都有序之后,就可以对左右两个有序的子序列进行归并。

归并函数Merge的代码逻辑:

(注意,归并函数的作用是把A数组中两个已经排好序的部分进行排序。这两个部分是low~mid和mid+1到high)。

第一步:A是要归并的数组,我们使用malloc函数做一个辅助数组B,用for循环把A复制到B。

第二步:用i,j指针指向B数组中low和mid+1的位置,用k指针指向A的开头。

第三步:i,j依次向后扫描,比较i和j的key值,把较小的值放到k,然后k指针向后移动,i和j中较小的指针也向后移动,直到i或者j扫描完自己的部分;

第四步:把i或j没有扫描完的部分用while循环复制到k中,边复制,边移动指针。

(2)代码如下:

//建立辅助数组B
int *B=(int *)malloc(n*sizeof(int));//归并函数
void Merge(int A[],int low, int mid, int high){int i ,j ,k; //三个指针,i j是B上的,K是A上的//把A复制到Bfor(k=low;k<=high;k++)B[k]=A[k];//比较i j的key值,把小的放入A[k]for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){if (B[i]<=B[j])A[k]=B[i++];elseA[k]=B[j++];}//把没有扫描完的直接复制进入Awhile(i<=mid) A[k++]=B[i++];while(j<=high) A[k++]=B[j++];
}//归并排序主体函数
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); //调用归并函数}
}

3. 空间效率
(1)递归工作站需要辅助空间,递归调用的深度不会超过递归的趟数=log2n向上取整。所以递归带来的空间复杂度是O(log2n);

(2)空间的开销来自构建的辅助数组B,B和A元素个数相同,都是n,所以是O(n).

(3)前两个比较,取最大值,所以归并排序的空间复杂度为O(n)。

4. 时间效率
(1)归并的趟数是h-1趟;二叉树中,第h层最多有2的(h-1)次方个节点,换句话说,如果有n个节点,那么要满足不等式:n<=2的(h-1),解不等式等到h-1=log2n向上取整。所以结论:二路归并的趟数=log2n向上取整。

(2)每趟归并的时间复杂度为O ( n ) :因为是用i j 扫描,一共要进行n-1次关键字对比。

(3)所以归并排序算法的时间复杂度为O(n*log2n)。

5. 稳定性
merge()操作的时候,如果i j 的key值相等,优先让靠左边的元素放入A,所以并不会改变相同关键字记录的相对次序,所以归并排序的算法是稳定的。

从单个记录起进行两两归并并不值得提倡,通常将它和直接插入排序结合。改进后的归并排序仍是稳定的。

十一、基数排序

注意:考试少靠代码,大多情况是考手动模拟。
1. 描述
基数排序是一种很特别的排序方法,它不基于比较和移动进行排序,而是基于关键字位的大小进行排序。以排序为递增为例,初始长度为n的线性表

(1)首先把每个元素想成由d组关键字组成,其中的关键字大于等于0,小于等于r-1,这个r称为“基数”

(2)初始化:设值r个空队列,起名为r-1,r-2...0    。然后按照关键字位权重递增的顺序(个、十、百),对关键字位分别做“分配”和“收集”

(3)分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾;

(4)收集:把各个队列中的结点依次出队列并连接。(最终要想得到递减序列,就先收集队列较大的;如果想得到递增序列,就先收集队列较小的)

3. 空间效率
一趟排序需要辅助空间为 r(r个队列,r个队头指针和队尾指针)。空间复杂度为O ( r ) 。

4. 时间效率
基数排序需要进行 d 趟分配和收集,一趟分配需要O ( n ) ,一趟收集需要O ( r )。所以基数排序的时间复杂度为O ( d *( n + r ) ) 。每个元素拆成d部分,n是元素个数,r是基数(每个部分可能取得r个值)。其与序列的初始状态无关。

5. 稳定性
基数排序是稳定的。口诀:基你太稳

注意:基数排序不是基于比较的算法。之前学习的算法都是基于“比较”的算法

6.适用性

十二、排序算法的比较

1 稳定性

不稳定的算法口诀:“快些选堆”。快速排序、希尔排序、简单选择排序、堆排序

2 时间复杂度

时间较快的算法的口诀:“快些归队”。快速排序、希尔排序、归并排序、堆排序

在实际应用中,快速排序往往可以优于其他算法,被认为是目前基于比较的内部排序中最好的方法。

3 空间复杂度
大部分的算法的空间复杂度都是常量。空间复杂度的定义是用到的额外的辅助空间,不包含自己的空间的。

4 适用性

若 n 较小,可以采用直接插入排序或简单选择排序;
若 n 较大,则应采用时间复杂度为O(log2n)的排序方法:“快些归队”。快速排序、希尔排序、归并排序、堆排序;

5 常考知识点
(1)当关键字随机分布时,快速排序平均时间最短;
(2)堆排序所需的辅助空间少于快速排序,且不会出现快速排序可能出现的最坏情况;
(3)冒泡排序和堆排序每趟处理后都能产生当前的最大值或最小值。
(4)快速排序一趟处理就能确定一个元素的最终位置。
(5)当记录本身信息量较大时,为避免耗费大量时间移动记录,可以采用链表作为存储结构

(6)每一趟都至少能确定一个元素的最终的位置的算法有:“快选一堆帽子”。快速排序、简单选择排序、堆排序、冒泡排序。

王道408排序算法总结相关推荐

  1. 九大经典排序算法(王道考研排序算法整理)

    6. 排序 算法稳定性:若待排序表有两个元素 R i R_i Ri​ 和 R j R_j Rj​,其对应的关键字相同即 k e y i = k e y j key_i=key_j keyi​=keyj ...

  2. (王道408考研操作系统)第三章内存管理-第二节3:页面置换算法2

    上接: (王道408考研操作系统)第三章内存管理-第二节2:页面置换算法1 文章目录 一:时钟置换算法(CLOCK) (1)简单时钟置换算法 (2)改进型时钟置换算法 二:页面置换算法总结 一:时钟置 ...

  3. 考研408笔试之排序算法代码实现汇总

    各种排序算法汇总 因为分开写太麻烦了QAQ,直接放在一起吧. 写在前面 本文不详细探讨各种排序的原理,只涉及具体的代码实现,了解算法原理,翻阅一些资料可以写出各种排序的代码实现.本文进行汇总,以题目的 ...

  4. 王道408数据结构——第八章 排序

    文章目录 一.排序定义 二.插入排序--直接插入排序 1. 描述 2. 代码和示例 3. 空间效率 4. 时间效率 5. 稳定性 6. 适用性 三.插入排序--折半插入排序 1. 描述 2. 时间效率 ...

  5. 【数据结构】【王道408】——PPT截图与思维导图

    自用视频PPT截图 视频网址王道B站链接 23考研 408新增考点: 并查集,红黑树 2023年408真题数据结构篇 408考纲解读 考纲变化 目录 第一章 绪论 第二章 线性表 顺序表 单链表 双链 ...

  6. 用python排序算法_Python - 八大排序算法

    1.序言 本文使用Python实现了一些常用的排序方法.文章结构如下: 1.直接插入排序 2.希尔排序 3.冒泡排序 4.快速排序 5.简单选择排序 6.堆排序 7.归并排序 8.基数排序 上述所有的 ...

  7. 王道408思维导图 marginnote 【4门科齐全】Xmind+脑图原件可下载 - 在线分享

    marginnote 王道思维导图[408 4门课] 包括数据结构.计算机组成原理.操作系统.计算机网络思维导图和脑图. 分享链接在最下面,帮忙点个赞+关注哦! [下面有一些资料界面展示.] 描述: ...

  8. 【排序算法】冒泡排序、简单选择排序、直接插入排序比较和分析

    [排序算法]冒泡排序.简单选择排序.直接插入排序比较和分析 写在前面: 本文简单介绍了冒泡排序.简单选择排序.直接插入排序,并对这三种排序进行比较,入参都是80000个随机数,比较算法耗时.进一步,我 ...

  9. 算法 64式 14、排序算法整理_1_1到15题

    1 算法思想 这里将寻找最小/大的前k个数,寻找逆序对,线性时间选择(寻找第k小/大的元素),奇偶/大小写字符分别放在前后部分等和排序相关类型的题目,放在了排序而不是查找中. 1.1含义 排序含义:重 ...

  10. 内部排序算法全面总结

    排序的概念 排序,就是重新排列表中的元素,使表中的元素按照关键字有序的过程. 我所了解的 表 多半是顺序表,因为顺序表实现较为简单,但是链表结构同样可以实现很多排序算法. 定义中的元素一般指什么元素呢 ...

最新文章

  1. django 用户管理相关的表
  2. .NET Framework源码研究系列之---Delegate
  3. 接口规范 6. 查看在线用户
  4. linux下串口工具minicom
  5. 汉印HPRT XT130 打印机驱动
  6. 华为手机连接电脑,无法使用USB调试或者无USB连接弹窗的问题
  7. 芝麻小程序客服移动版,一款更及时查看和回复消息的工具!
  8. POC_Jenkins
  9. 计算机 蓝牙鼠标卡顿,无线蓝牙鼠标为什么有时会卡顿发飘,不稳定?
  10. 简体字与繁体字互相转换
  11. uni-app 从本地相册选择图片或使用相机拍照
  12. 数据的编码类型及数据通信的工作方式
  13. stm32 ADC hal库实现
  14. 第四周.直播.03.论文带读+GAT
  15. 关于破解的十个基本功
  16. python gui 三维 pyqt5_【PyQt5-Qt Designer】在GUI中使用pyqtgraph绘图库
  17. Android微信抢红包插件原理和实现 适配微信6.6.1版本
  18. 8051单片机Proteus仿真与开发实例-74HCT4051多路选择器(分配器)驱动仿真
  19. Java八种基本数据类型
  20. Kubernetes学习(六)—— Pod控制器

热门文章

  1. 怎么设置计算机键盘数字键,电脑右边的数字键不能用怎么办_电脑右边数字键盘用不了的修复方法...
  2. mysql视图代码_mysql创建视图的实例代码
  3. vue 调微信jsapi
  4. 分享盘点9个可免费使用的网站CDN加速服务
  5. 如何在AD上重定向电脑加域后默认保存位置?
  6. 15.编写LED程序及反汇编工具
  7. php扩展引擎手册,模板引擎-THINKPHP 5.0 手册最新版
  8. 企业微信分享小程序功能
  9. 初中高中睡前必看古诗名句
  10. 华为防火墙双线路故障自动切换