目录
一、排序定义
二、插入排序——直接插入排序
三、插入排序——折半插入排序
四、插入排序——希尔排序(缩小增量排序)
五、交换排序——冒泡排序
六、交换排序——快速排序
七、选择排序——简单选择排序
八、选择排序——堆排序
九、堆的插入删除
十、归并排序
十一、基数排序
十二、排序算法的比较
1 排序定义
排序,就是把一堆数据元素,按照关键字的递增或者递减的关系把它们排列,即经过排序之后,数据元素的关键字要么递增要么递减(即“有序”)。有时候关键字会相同,这时候就会引出算法的稳定性的问题。
2 稳定性
带排序表中关键字相同的元素,其相对次序在排序前后不变,这称这个排序算法是稳定的。算法是否稳定并不能衡量一个算法的优劣。如果带排序表中的关键字均不重复,则排序结果是唯一的,算法的稳定性就无关紧要。
大部分的内部排序都需要执行比较和移动操作。通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。
在基于比较的排序方法中,每次比较两个关键字大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以得出结论:当文件的 n 个关键字随机分布时,任何借助比较的排序算法,至少需O(nlog 2n)的时间。——这个log 2n是以2为底的对数,全篇都是这么表示。
算法的稳定性总结见十一。
3 排序分类:
(1)插入排序(直接插入排序、折半插入排序、希尔排序)
(2)交换排序(冒泡排序、快速排序)
(3)选择排序(简单选择排序、堆排序)
(4)归并排序(二路归并、多路归并)
(5)基数排序
1. 插入排序的描述
插入排序是一种简单直观的排序方法,其基本思想是将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成。
插入排序通常采用就地排序,在从后向前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素腾出插入空间。
2. 代码和示例
下面是直接插入排序的代码
#include
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=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=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
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]=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
运行 结果:
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
//核心算法部分。
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
运行的结果:
3. 空间效率
仅使用了常数个辅助单元,空间复杂度为O ( 1 ) 。
4. 时间效率
目前无法用数学手段证明它的时间复杂度,但是当n在某个特定范围时,希尔排序的平均时间复杂度约为O(n^{1.3})。在最坏情况下,希尔排序的时间复杂度为O(n²)。不过总体而言,这个算法还是比较优化的。
5. 稳定性
当相同关键字的记录被划分到不同子表时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序方法。
6. 适用性
希尔排序对较大规模的排序都可以达到很高的效率。
仅适用于顺序存储的线性表。因为我们需要用增量d快速找到与之相邻的、从属于同一个子表的各元素,所以必须要有随机访问的个性。
1. 描述
所谓交换,是指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。
冒泡排序的基本思想:从后往前(或从前往后)两两比较相邻元素的值,若为逆序则交换,直到序列比较完,这称为一趟冒泡。
冒泡排序中所产生的有序子序列一定是全局有序的,每趟排序都会将一个元素放在其最终位置。
举例子,目标是进行从小到大排序,从后往前冒泡,那么一趟冒泡的结果是将最小的元素放到最前面,第二趟冒泡把第二大的元素放到第二个位置,从第二趟开始,已确定位置的元素不再参与比较,所以第二趟最多做n-1次移动。总体冒泡的趟数最多是n-1。
结束冒泡的时机:某一趟冒泡结束后,元素的相对位置都不变。
2. 代码和示例
需要写一个交换的函数swap。
冒泡排序算法代码如下
#include
#include
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
运行结果
3. 空间效率
仅使用了常数个辅助单元,空间复杂度为O ( 1 )。
4. 时间效率
时间复杂度为O(n²)。
5. 稳定性
冒泡排序时一种稳定的排序方法。
如果把代码中判断是否逆序的条件由“>”改为“≥”,则算法变得不稳定。
1. 描述
快速排序的基本思想是基于分治法的:在待排序表中选取一个元素,称为枢轴(或称基准,常取首元素)。通过一趟排序,将待排序表分成两部分,一部分中所有元素均小于枢轴,另一部分元素均大于枢轴,两部分分别位于枢轴元素的两侧,这个过程称为一趟快速排序(或一次划分)。然后递归地分别对两个子表重复上述过程,直到每部分只有一个元素或空为止,此时所有元素都放在了最终位置。
因为27比49小,所以要把27放到low的位置。即代码:A[low]=A[high]。如下图:
然后low指针开始向右边扫描。扫描到65的时候发现65比49大,如图:
于是把65移动到high的位置。即代码A[high]=A[low]。移动后如下图:
接着high指针向左边扫描,寻找比49更小的。然后找到了13,于是把13放到low的位置,形成下面这图。
然后low指针向右边移动。需找比49更大的。找到了97,然后97填到空白处,如图:
然后切成high向左移,寻找比49更小的。但是没有找到,于是把49放到low的地方,形成如下:
一次划分的过程到此结束。
快速排序并不产生有序子序列,但每趟排序后会将枢轴元素放在最终位置上。
2. 代码和示例
快速排序的代码题是408考试中频率最高的一个,必须好好看。
一趟快速排序是一个交替搜索和交换的过程,算法如下
#include
#include
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
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
#include
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
运行效果:
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(iA[i]) // i= 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进行互换,互换后如下图:
接着堆顶和堆底元素互换,完成第一趟排序,就变成如下图:
根据上个截图我们可以看到,剩余的元素依然是个大根堆,于是进行堆底和堆顶互换,就变成如下图:
到此完成了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复制到B
for(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++];
else
A[k]=B[j++];
}
//把没有扫描完的直接复制进入A
while(i<=mid) A[k++]=B[i++];
while(j<=high) A[k++]=B[j++];
}
//归并排序主体函数
void MergeSort(int A[],int low,int high){
if(low
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 较大,则应采用时间复杂度为n*(log n)的排序方法:“快些归队”。快速排序、希尔排序、归并排序、堆排序;
5 常考知识点
(1)当关键字随机分布时,快速排序平均时间最短;
(2)堆排序所需的辅助空间少于快速排序,且不会出现快速排序可能出现的最坏情况;
(3)冒泡排序和堆排序每趟处理后都能产生当前的最大值或最小值。
(4)快速排序一趟处理就能确定一个元素的最终位置。
(5)当记录本身信息量较大时,为避免耗费大量时间移动记录,可以采用链表作为存储结构
(6)每一趟都至少能确定一个元素的最终的位置的算法有:“快选一堆帽子”。快速排序、简单选择排序、堆排序、冒泡排序。