一、排序的基本概念
二、插入排序
2.1插入排序
2.1.1算法思想
插入排序的思想很简单,就是不断的把一个个带排序的记录,按关键字的大小插入到前面已经排好序的子序列中。直到全部序列插入完成。
比如我们现在要对下面的序列进行排序,
刚开始我们从1号位开始
我们会认为当前处理的这个元素之前都是有序的
现在把当前元素38和前面已经排好序的元素依次进行对比,比当前处理元素38更大的元素,我们都需要将它依次后移。
目前只有49这个元素,我们把49后移,38插入到49前面
接下来要处理的是65,65比49更大,不用移动
接下来是97,97>69,不用移动
接下来是76,76<97,97右移
76>65,76放下
接下来是13,
13<97,97右移
13<76,76右移
13<65,65右移
13<49,49右移
13<38,38右移
再往下没有数可以比较了,13放下
接下来是27,
27<97,97右移
27<76,76右移
27<65,65右移
27<49,49右移
27<38,38右移
27>13,27放下
最后要处理的是49,这里我们49给了一个下划线,区分前面的49
处理方式和前面一样,把前面排好序的,小于49的右移,然后49放下去
注意,我们刚才是把比49更大的元素右移,和它相等的是没有右移的,这样保证了排序的稳定性。
2.1.2算法实现
代码如下:
//直接插入排序
void InsertSort(int A[],int n){
int i,j,tmp;
for(i=1;i<n;i++){//将各元素插入已经排好序的序列中
if(A[i]<A[i-1]){//若A[i]关键字小于前驱
tmp=A[i];//tmp暂存A[i]
for(j=i-1;j>=0&&A[j]>tmp;j--){//检查前面已经排好序的元素
A[j+1]=A[j];
}
A[j+1]=tmp;
//这里用j+1是因为上面第一次找到A[j]<=tmp时跳出了循环
//也就是说A[j]的位置是不需要动的,而A[j+1]当时是赋给了A[j+2]
//你需要的是把tmp赋给A[j+1]
}
}
}
还有一种带哨兵位的排序方式,大家可以自己看一下,我个人更偏向前一种,逻辑更清楚
带哨兵位就是把0号位空出来,然后用0号位作为哨兵暂存当前元素,和上面方法的tmp一样
2.1.3算法效率分析
对于空间复杂度,我们也就是定义了两个变量i,j用于循环。然后就是tmp(如果不用哨兵)。这些变量所需要的空间都是常数级的,所以空间复杂度O(1)
然后是时间复杂度,在进行插入排序时,我们都是从第二个元素开始,然后依次往后处理的。如果有n个元素,一共要往后进行n-1次遍历。
每次遍历时,每个元素需要进行一个往前的对比。
2.1.4算法优化——折半插入排序
前面在处理一个元素时,都是用的顺序查找的方式依次往前找,找到该插入的位置。
但是细心的同学会发现当前要处理的元素前面的都是有序的
而有序,我们就可以用折半查找来优化了,没必要顺序查找一个个对比
比如我们现在在A[0]保存当前要处理的元素
接下来在当前要处理的元素55前面的区域设low和high
low=1,high=i-1
mid=(low+high)/2
对比mid和当前元素
50<55,所以55应该插在50右边
low=mid+1
mid=(low+high)/2
对比mid和当前元素
70>55,55应该插在70左边
high=mid-1
low=(low+high)/2
对比mid和当前元素
60>55,55应该插在60左边
high=mid-1
low=(low+high)/2
到这里,low>high,所以折半查找停止
不难发现low所指元素及右边全部比i所指元素大,全部右移
然后把A[0]放到low的位置
后面的以此类推。
2.2希尔排序
2.2.1算法思想
希尔排序就是一个叫希尔的人发明的,其实就是基于上面介绍的插入排序的优化
其实就是把排序表里的元素再一个个拆分成子表
怎么拆分?就是你在每一趟的处理过程中,设置一个增量d
把相距为d的各个元素看成一个特殊的子表,把各个子表中的元素进行直接插入排序。
假设我们第一趟排序设d1=4,也就是d1=n/2=8/2=4
所有相距为d的元素都看成属于1个子表,也就是说对于49相距d=4的是76
而对于38,和它相距d=4的元素是13
后面以此类推,65和27是一个子表,97和49是一个子表
那在第一趟的处理中,我们会把相距距离=4的元素看成同一个子表中的元素,接下来就是对各个子表分别进行直接插入排序
到这里就完成了第一趟子表的排序,所以第一趟结束后数组元素如下
接下来进行第二趟的处理,第二趟我们所需增量d的值,我们取d2=d1/2=4/2=2
第二趟我们把相距距离为2的元素划分为同一个子表。
对于1号元素49,和它相距为2的是3号元素27,再往下是76,然后是65
对于2号元素13,和它相距为2的是4号元素49,再往下是38,然后是97
对两个子表进行插入排序
到这里完成第二趟的处理,表里的元素如下
再往下第三趟的处理,我们让增量d继续缩小,d3=d2/2=2/2=1
d=1时,所有元素都会被划分为同一个子表,进行一次插入排序就可以结束了
到这里,经过3趟排序就可以得到一个递增有序的序列
我们把刚才的几个过程进行一个罗列,如下图
上例中我们用的是d=4,2,1,也就是每次缩小一半,这个是希尔本人推荐的增量序列。
当然,你也可以用别的序列,考研中对希尔排序的考察,通常会出现其他各种各样的增量序列
比如我们规定第一趟d1=3,第二趟d2=1
这种情况下,第一趟的排序中,我们把相距距离为3的元素看成从属同一个子表的元素
然后分别对每个子表插入排序
接下来是第二趟d2=1,也就是对整体进行一次插入排序,得到最终序列
这也就是考试中关于希尔排序最常考的一种,它会给你一个元素序列,然后给你每一趟的增量d,让你求每一趟排序之后得到的序列是什么样
2.2.2代码实现
//希尔排序
void ShellSort(int A[],int n){
int d,i,j;//d为每一趟的增量
//A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置
for(d=n/2;d>=1;d=d/2){
for(i=d+1;i<=n;i++){
if(A[i]<A[i-d]){
A[0]=A[i];
for(j=i-d;j>0&&A[0]<A[j];j-=d){
A[j+d]=A[j];//后移
}
A[j+d]=A[0];
}
}
}
}
下面我们来举例说明上面的代码
一开始我们让d=n/2=8/2=4
接下来进行第一趟的处理,也就是i=d+1=5,
指向这个位置的原因是我们在处理第一个子表时,按照直接插入排序的规则,我们只需要从第二个元素开始,就是看这个元素是否需要插到它前面就行了。
所以刚开始我们会让i指向第一个子表中的第2个元素的位置。按照直接插入排序的规则,我们需要对比当前元素和前面的元素大小关系,76>49,A[i]<A[i-d]条件不满足,不需要变动
接下来是第二轮的for循环,我们让i++,也就是i=6
由于当前指向元素13比前面的元素38更小,也就是A[i]<A[i-d]满足,
那么我们把当前指向的元素放到A[0]的位置,然后更深层的for循环(插入排序),我们要检查当前元素前面的元素是否比当前元素更大,如果更大,那我们需要把j所指元素后移(这里的后移是你后移到子表的下一个位置)
比如这里的38后移到子表的下一个位置,也就是A[j+d]=A[j],即后移到6号位置
接下来根据最里层for循环(插入排序)的规则,我们要检查这个子表中当前还有没有其他元素需要处理。此时d=4,j-d=2-4=-2,这里-2<=0就说明前面已经没有需要再比较的元素了,那么最里层的for循环(插入排序)跳出。
然后就是把我们刚才保留在A[0]的数值放到j+d的位置,也就是把13放到2号位
到这里就完成了对第二个子表的插入排序。
下面i++,进行第三个子表的插入排序,这个子表原理也是类似的,大家直接看动图
再让i++,发现i已经超过8这个最大值了,那么第一趟处理结束
再往下,让d变为原先一半,也就是d2=d1/2=2,然后处理流程和第一趟基本一样,这里不再赘述。
2.2.3算法性能分析
显然对于空间复杂度,我们只需要常数级的空间
而对于时间复杂度,由于你d设置不同,你最后得到的趟数和每趟排序的元素对比都不同。
所以对于希尔排序时间复杂度,没法用数学求证
我们只能大致求出希尔排序的时间复杂度范围
如果你第一趟就用d=1,那么希尔排序会退化为插入排序,也就是时间复杂度为O(n2)
而如果数据元素数量不多,在某个范围内,希尔排序效率可达到O(n1.3)
而对于算法稳定性,希尔排序是不稳定的!
举个例子:
另外,由于希尔排序只能基于顺序表,因为你希尔排序要用增量d来快速找到从属与一个子表的各个元素。那么你必须要有随机访问的特性才能实现,所以必须是顺序表
ps:希尔排序代码考察频率不高,大多是给你一些元素和d,让你求每趟之后的元素序列。但你要学会算法思想,万一考到也可以现场写。
三、交换排序
3.1冒泡排序
3.1.1算法思想
从前往后(或者从后往前)两两比较相邻元素的值,如果两个元素为逆序,进行交换。比较完整个序列,称为一趟冒泡排序
举例说明,现要求对下面的序列进行排序,排序后元素递增
第一趟排序:(这里采用从后往前的方式,你用从前往后也可以)
我们会先对比最后两个元素,27<49,不需要交换
往前检查,13<27,不需要交换
往前检查,76>13,交换
往前检查,97>13,交换
往前检查,65>13,交换
往前检查,38>13,交换
往前检查,49>13,交换
每一趟排序,我们都可以把当前需要排的序列中的最小值“冒”到最前面
第二趟处理也是一样的,大家自己看动画:
需要注意的是,在第二趟排序到1号位时,已经不需要和0号位比较了,因为第一趟已经确定0号是最小的,你比较到1号位置就可以了。
第二趟排序之后,我们就确定了最小的两个元素,把它们放到最左边
第三趟排序,如下动图:
同理,第三趟时已经确定前面两个是最小了,对比到2号位置即可
后面以此类推,不再赘述。
需要注意的是,如果你的某一趟排序没有发生任何交换,那么就已经说明整体有序了,下面不用再浪费时间对比了,结束算法
3.1.2代码实现
//交换
void swap(int &a,int *b)
{
int tmp =a;
a=b;
b=tmp;
}
//冒泡排序
void BubbleSort(int A[],int n){//这里是进行递增排序
for(int i=0;i<n-1;i++){
bool flag=false;//表示本趟冒泡排序是否发生交换的标志
for(int j=n-1;j>i;j--){//一趟冒泡过程
if(A[j-1]>A[j]){//出现逆序
swap(A[j-1],A[j]);//交换两元素位置
flag=true;
}
}
if(flag==false){
return;//本趟遍历后没有发生交换,说明表已经有序
}
}
}
注意:我们这里是左边元素大于右边元素才交换,如果相等是不交换的,
这样就保证了算法的稳定性
3.1.3算法性能分析
试问:冒泡排序是否可以用于链表?
答案是可以,比如想让下面这个链表递增有序
我们用L指向链头元素
我们可以用i指向元素和它后面一个元素进行对比,如果当前i指向元素更大,就进行交换
这样就完成了一趟排序,
后面的几趟排序也是一样的,不再赘述。
3.2快速排序
3.2.1算法思想:
对于一个给定的待排序表,我们可以任选一个元素作为基准(或者枢轴),
比如在第一趟排序中,我们可以选49这个元素作为基准
在接下来经过一趟排序后,我们可以找到49这个元素的最终位置
显然,49这个元素在整个表中不是最大也不是最小的,我们需要把这个表划分为左右两个部分,左半部分所有元素<49,右半所有元素>=49。
这样的处理称为一次划分。
下面来看一下划分过程怎么做:
我们用low和high分别指向我们要处理的序列的头和尾两个位置,让49作为基准
我们用low和high把整个表的元素扫描一遍,在整个扫描过程中,我们要保证:
high所指的指针右边都是大于等于当前的基准元素49的
low所指的指针左边都是小于当前基准元素49的
当前low指向元素为空,先让high往左走,high所指元素49>=49,所以下标7的元素不需要移动
high–,high所指元素27<49,所有比49更小的元素都应该放到low指针所指位置
当前high指向元素为空,先让low往右走
此时low所指27<49不需要移动
low++,38<49不需要移动
low++,65>49,把65放到high所指位置
当前low指向元素为空,先让high往左走
high当前所指为65>49,不需要移动
high–,13<49,13放low位置
当前high指向元素为空,先让low往右走
low指向元素13<49,不需要移动
low++,97>49,97放到high位置
当前low指向元素为空,先让high往左走
high所指元素97,97>49不需要移动
high–,76>49不需要移动
high–,high和low到达同一位置,第一趟排序结束,把49放到low(high)所指位置
注意,此时既然已经确定了49的位置,那么我们下面排序就不需要再管49了,进行左右子表的排序,排序方法同上
先看左子表:
我们选择第一个27作为基准元素
当前low指向元素为空,先让high往左走
high所指元素13<27,把13换到low所指位置
当前high指向元素为空,先让low往右走
low所指元素13<27,不需要移动
low++,38>27,38放到high所指位置
当前low指向元素为空,先让high往左走
high所指元素38>27,不需要移动
high–,low和high指向同一位置,确定27所在位置,放27到low(high)的位置
而此时,下标0-2的子表又被我们划分为0-1和1-2两个部分
注意:这里两个部分仅仅只有1个元素了,所以也确定了13和38的最终位置
接下来是下标4-7子表处理,大家自己看动画,不再赘述:
3.2.2代码实现
快速排序算法代码实现是所有排序算法考察频率最高的,务必掌握
//用第一个元素将待排序序列划分为左右两个部分
int Partition(int A[],int low ,int high){
int pivot=A[low];//用第一个元素作为枢轴
while(low<high){
while(low<high&&A[high]>=pivot){
high--;
}
A[low]=A[high];//遇到A[high]<pivot的情况
while(low<high&&A[low]<=pivot){
low++;
}
A[high]=A[low];//遇到A[low]>pivot的情况
}
A[low]=pivot;
return low;
}
//快速排序
void QuickSort(int A[],int low,int high){
if(low<high){//递归跳出的条件
int pivotpos=Partition(A,low,high);//划分
QuickSort(A,low,pivotpos-1);//划分左子表
QuickSort(A,pivotpos+1,high);//划分右子表
}
}
3.2.3算法性能分析
四、选择排序
4.1简单选择排序
4.1.1算法思想
刚开始所有元素都是无序的,第一趟的处理我们会从左往右扫描,然后找到关键字值最小的一个元素。
显然,这里13是最小的,那我们把最小元素13和最前面一个位置进行交换
第一趟排序结束,接下来,我们就不需要管0号位置了
第二趟,从头到尾扫描各个元素,然后把关键字最小的元素放在最前面(当前子序列最前面是1号位置)
第二趟排序结束,前面两个位置就不用管了
从剩余元素中找到最小的元素,放到最前面
第三趟结束,前3个位置不用管了,从剩余元素中找最小的
这里需要注意,剩余元素中有2个最小的元素49,我们选中第一个扫描到的49
然后后面以此类推,大家自己看动画,不再赘述
对于最后一个元素,它就一个了,肯定就是它自己啊,不用额外考虑了
4.1.2代码实现
//交换函数
void swap(int &a,int &b){
int temp=a;
a=b;
b=temp;
}
//简单选择排序
void SelectSort(int A[],int n){
for(int i=0;i<n-1;i++){//一共进行n-1趟
int min=i;//记录最小元素位置
for(int j=i+1;j<n;j++){//遍历选择最小元素
if(A[j]<A[min]){//更新最小元素位置
min=j;
}
}
if(min!=i){
swap(A[i],A[min]);
}
}
}
4.1.3算法性能分析
空间复杂度不必多说,也就定义了几个变量,所以空间复杂度O(1)
而对于时间复杂度,无论给出的序列是“全部有序”,“全部逆序”,还是“乱序”
你都需要n-1趟的处理
总共需要比较(n-1)+(n-2)+…+1=n(n-1)/2次
所以时间复杂度为O(n2)
然后是算法的稳定性
4.2堆排序
4.2.1什么是堆?
堆排序是考研中又难又常考的
这种算法的实现基于一种叫做堆的数据结构,我们首先要探讨的就是什么是堆?
堆这种数据结构又分为“大根堆”、“小根堆”
如果直接看定义其实很难理解,我们还是具体举例子说明
比如说大根堆:L(i)>=L(2i)且L(i)>=L(2i+1)
假设我们现在i取1,那么这里87>78和32,
如果所有i都满足这个性质,则为大根堆
比如说小根堆:L(i)<=L(2i)且L(i)<=L(2i+1)
假设我们现在i取1,那么这里9<17和65,
如果所有i都满足这个性质,则为小根堆
到这里估计大家还是有点懵,那我们来复习一下以前讲过的二叉树的顺序存储
对于一个完全二叉树,如果按照层序一个节点一个节点的存到一个数组中,就是完全二叉树的顺序存储
并且这些结点在数组中的存储位置可以反映结点之间的逻辑关系
对于一个结点i,如果要找它的左孩子就是2i,如果要找右孩子就是2i+1
如果一个结点i,i>⌊n/2⌋,则说明i是叶子结点;i<=⌊n/2⌋,则说明i是非叶子结点
所有分支结点会存放在数组靠前的位置
所有叶子结点存放在比较靠后的位置
知道这个之后,我们再来看刚才介绍的大(小)根堆
所谓的“堆”,从内存上看像一个连续存放的数组
但其实从逻辑视角看,堆是一棵顺序存储的完全二叉树
编号为1的结点就是这棵完全二叉树的根节点
然后数组下标i的结点,左孩子为2i,右孩子为2i+1
(1<=i<=n/2)
大根堆简化来说就是在这棵完全二叉树中,如果所有子树的根节点>=左孩子,根节点>=右孩子,那么这棵顺序存储的完全二叉树就是一个大根堆
同理可得小根堆:
小根堆简化来说就是在这棵完全二叉树中,如果所有子树的根节点<=左孩子,根节点<=右孩子,那么这棵顺序存储的完全二叉树就是一个小根堆
到这里大家就可以明白什么是堆啦!,下面回到我们主线剧情:怎么进行堆排序?
4.2.2如何进行堆排序?
我们之前介绍过,堆排序大类是属于选择排序
选择排序的基本思想就是我们会在待排序的这些元素中选取关键字最大(最小)的元素加入有序子序列。
对于大根堆来说,你选关键字最大的元素其实就非常方便,因为最大元素肯定是在根节点(也就是堆顶元素)啊
而如果从数组的视角看,肯定是第一个元素的值最大
所以如果我们能把一个原始的数组整理成堆这种形式,那么我们想进行堆排序就很简单了
接下来要探讨的问题是:对于一个给定的初始序列,我们如何把它建立成大根堆,也就是根>=左、右
举个例子:现有如下初始序列和对应二叉树
既然要保证所有子树的根结点>=左、右
那么在这棵树中,我们应该检查所有的分支结点,因为所有的分支结点其实都是它所属的这个子树的根结点。
因此,接下来要做的就是检查所有上面画了红框的分支结点
对于任何一个结点来说,如果它不满足根结点>=左、右,那我们就需要进行调整
ps:对于顺序存储的完全二叉树,分支结点(非终端结点)下标i<=⌊n/2⌋
该例中共8个结点,也就是n=8,那么我们只需要检查i<=⌊n/2⌋=4的结点即可
接下来我们会从后往前,也就是从i=4一直处理到i=1
第一个被处理的是4号结点,该结点是所有分支结点中编号最大的一个
我们现在来检查以4号结点为根的子树,看它是否满足大根堆的要求
我们知道i的左孩子是2i,右孩子2i+1
那么4的左孩子是8号结点,没有右孩子(一共就8个结点)
但是这里9<32,也就是根<左孩子
所有不满足大根堆,进行调整
调整方式:把当前结点和更大的孩子进行互换
由于这里只有一个左孩子,那么就和左孩子互换位置
那么4号位置的子树就满足大根堆了
接下来处理3号结点
78>65,根>左孩子
78<87,根<右孩子
那么进行调整,根和更大的孩子互换,也就78和右孩子互换
这样,3号子树也满足了大根堆
接下来处理2号结点,17<32,17<45
根和更大的孩子交换,也就是17和45互换
最后是1号结点,显然53<45,53<87,应该把53和87互换
但是这里大家会发现,53换下去之后,53的位置又不满足大根堆了
出现这种情况处理方法和前面一样,你就继续把小的往下换就是了
53左右孩子更大的是78,那你把53和78换一下位置
所以,如果你在换位置过程中发现破坏了下一级的堆,那就让小的继续往下换,直到没法换(或者已经满足了)则调整结束
到此为止,就让一个初始序列变成大根堆了
下面是建立大根堆的代码
//建立大根堆
void BuildMaxHeap(int A[],int len){
for(int i=len/2;i>0;i--){
HeadAdjust(A,i,len);
}
}
//将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len){
A[0]=A[k];//A[0]暂存子树的根结点
for(int i=2*k,i<=len;i*=2){//沿key较大的子结点向下筛选
if(i<len&&A[i]<A[i+1]){//比较左孩子和右孩子哪个更大,这里额外判断i<len是防止没有右孩子
i++;//如果右孩子更大,i+1;左孩子更大则不会执行这一步
}
if(A[0]>=A[i]){//根节点值比最大的孩子还要大,那这棵子树就是满足大根堆的,不需要额外处理
break;//筛选结束
}
else{
A[k]=A[i];//将A[i]调整到双亲节点上
k=i;//修改k值,以便继续向下筛选
}
}
A[k]=A[0];//被筛选结点的值放到最终位置
}
现在已经知道了如何用代码建立大根堆,那么有了大根堆之后如何基于大根堆进行排序呢?
基于选择排序的思想,我们每一趟会把堆顶元素(最大的元素)加入到有序子序列中
具体做法:我们把堆顶元素和待排序序列中最后一个元素进行交换
举个例子:下面的大根堆中,堆顶元素就是87,和待排序序列中最后一个元素9进行交换
现在,就已经把最大的元素换到了数组的末尾,那么87这个位置就不需要再改了。
那么我们下面只要考虑上面的元素(除了87)就可以
在我们把9放到堆顶时,就已经不再是大根堆了,那我们要把9往下换
也就是调用我们前面讲的HeadAdjust函数
//将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len)
//传整个数组,k为当前要调整的结点编号,len是当前这个堆里还有多少元素
接下来就是让9不断向下换的过程,显然这里78更大,9和78换一下
这里继续向下换,9的左右孩子更大的是65,9和65换一下
到这里,9已经到底部了,没法往下换了,那么调整结束
此时,上面的部分又恢复成了大根堆
到处位置就完成了第一趟的处理
第一趟的处理,我们把最大的元素放到最后,然后把剩余的元素恢复成一个大根堆。
接下来是第二趟的处理:
原理和之前类似,我们把堆顶和堆底的元素,也就是待排序序列中最后一个元素进行一个交换。
然后和第一趟一样,对剩余元素进行调整(78不再考虑),变成大根堆
53现在在堆顶,不符合大根堆特性,我们把它往下换
53的左右孩子45和65,65更大,65和53换
到这里,53的左孩子9小于53,不用换了
这里78已结排好,不在考虑范围内
这样就把剩余的元素再次排成了一个大根堆
接下来是第三趟的处理:
把堆顶元素和堆底元素进行互换
换完之后要对剩余元素进行调整,变回大根堆
9的孩子45,53,显然53更大,让9和53互换
到这里9已经没法往下换了,调整结束
65和78已结排好,不需要考虑
接下来是第四趟
堆顶和堆底元素互换
然后对剩余元素进行调整,变回大根堆
17两孩子45、9,显然45更大,45和17互换
17一个左孩子32,32>17,32和17互换
53已结排好了,不需要考虑
到这里17已结到底部了,调整结束
87已结排好,不需要考虑
再往下的处理大家自己看动画,不再赘述:
ps:你根据大根堆,得到递增序列;如果是小根堆,则是递减序列
下面是基于大根堆进行排序的代码
//建立大根堆
void BuildMaxHeap(int A[],int len)
//将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int K,int len)
//堆排序的完整逻辑
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)//把剩余的待排序元素整理成堆
}
}
4.2.3算法效率分析
到目前为止,我们知道了整个堆排序总共需要分为两个大步骤,第一步是要建立一个初始的堆,你有了堆之后才能进行排序
而建堆时,我们需要调用下坠调整函数HeadAdjust
我们在排序时也同样要用到下坠调整函数HeadAdjust
所以,要分析算法性能,必须分析下坠调整函数
举个例子,假设现在要调整9这个元素,我们要把它往下换
根据代码逻辑,我们第一步应该是先对比9这个元素的左右孩子,看它们谁更大
所以这里涉及了一次关键字对比
而确定哪个孩子更大之后,要把根阶段和这个大孩子再做一次对比
这里又涉及一次关键字对比
所以,如果一个结点有左右孩子,那么它往下换一层,总共需要对比关键字两次
接下来,9这个元素还要继续往下换,此时总共有8个元素,9这个元素编号是4,它的
左孩子编号是8,由于此时左孩子的编号和元素总数相等,不满足i<len,这里就不用比左孩子右孩子谁大了,就一个左孩子
9左孩子45,45>9,交换
所以,如果一个结点只有左孩子,那么它往下换一层,只需要对比关键字一次
综上
而我们之前说过,对于一个完全二叉树来说,如果它有n个结点,那么它的树高h应该是h=⌊log2n ⌋+1
如果忘记了这个性质,可以看笔者的树这个数据结构讲解
另外,对于完全二叉树,第i层应该是要有2i-1个结点
而我们建立一个初始堆时,最下面一层的结点是不需要调整的
我们只需要调整上面的h-1层就可以了
对于一个完全二叉树:
第一层:1个结点,向下换调整最多需要比较2*(h-1)
第二层:2个结点,总共需要2 * 2 * (h-2)次调整
后面的以此类推
一直到h-1层,一共2h-2个结点,需要调整2h-2 * 2
对整个式子累和得到下图的公式
想偷懒的话,这个推导就不看了,你记住建堆时间复杂度是O(n)就行
4.3堆的插入删除
上小节我们学习了什么是堆?如何进行堆排序?
该小节我们将补充如何在堆中进行插入和删除操作
4.3.1在堆中插入新元素
假设我们现在是小根堆
假设我们现在要插入新元素13
我们先把新插入元素放到表尾位置,逻辑上看就是放在堆底
原先是小根堆,你现在插了新元素就不再是小根堆了(根<=左,右)
我们要做的就是把新插入的元素和父节点比较,如果新元素比父节点小,就往上换
要找父节点也很简单,当前节点位置i,那⌊i/2⌋就是父节点
当前13所在位置是9号,⌊9/2⌋=4,那就是和4号位的32进行交换
到了这层还不能结束,还要继续往上试探,看新插入的是否比父节点要小
这里13位置4号,父节点⌊4/2⌋=2,也就是2号位置的17
13<17,13和17互换
然后继续往上试探,13位置2号,父节点是⌊2/2⌋=1,也就是1号位置的9
13>9,不用换
到这里就符合小根堆的要求了,整个过程对比3次
再举个例子,我们现在要插46,同样的,先放在堆底
这里46是10号位置,父节点是⌊10/2⌋=5,也就是5号位置的45
46>45,不用换
到这里符合小根堆,整个过程发生一次对比
4.3.2在堆中删除元素
现有如下的小根堆
现要求把13删掉
删掉之后如何处理?
我们会用堆底的元素来代替被删元素
这里就是把46放到13以前的位置
然后为了让整体满足小根堆,我们让刚换上来的堆底元素不断往下换,直到没法换
这里也就是让46和它的两个孩子进行比较,让更小的上来
ps:关于对比关键字次数还是较常考的,这里注意一下
ps:关键字对比次数:先让左右孩子对比一次,然后选小的再和根对比一次。
再举个例子,现有如下小根堆,要求删除65
同样的,用堆底元素替换它,然后尝试往下换
换上去之后发现左右孩子都比46大,那就不用换了
ps:关键字对比次数:先让左右孩子对比一次,然后选小的再和根对比一次。
五、归并排序
5.1算法思想
什么是归并排序?比如你现在有两个或者多个已经有序的序列,要求合并成一个
我们设置三个指针i,j,k,每次对比i和j所指元素,哪个更小就放到新数组里面
到这里需要注意,如果i和j指向的元素一样,这里就可以根据你自己写的代码决定让i所指还是j所指的元素过来了。
那这里,如果元素大小相同,我们让i所指的优先进大数组
关于二路归并
其实就是我们刚才上面介绍的过程,把两个有序序列合并成一个
对比i和j所指元素谁更大(更小),然后放到k位置
5.2手算模拟
如果给你一个初始序列,让你进行二路归并(归并排序),那我们会一开始把这个初始序列看成一个个单独的元素。
第一趟归并排序,我们把相邻的两个部分进行二路归并
比如01,23,45,6
ps:如果元素不管,比如6号位置没有别的和它归并了,你就直接把它进行下一轮
第二趟归并排序,我们基于第一趟的结果,再次进行二路归并,比如现在01已经有序,23已经有序。我们把01这个组合和23这个组进行二路归并
第三趟归并排序,我们在第二趟的基础上进行,把1-3组和4-6组进行二路归并
5.3代码实现
int *B=(int *)malloc(n*sizeof(int));//辅助数组B
//A[low...mid]和A[mid+1...high]各自有序,将两个部分归并
void Merge(int A[],int low,int mid,int high){
int i,j,k;
for(k=low;k<=high;k++){
B[k]=A[k];//A中所有元素复制到B中
}
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(B[i]<=B[j]){
A[k]=B[i++];//较小值复制到A中
}
else{
A[k]=B[j++];
}
}
while(i<=mid){
A[k++]=B[i++];
}
while(j<=high){
A[k++]=B[j++];
}
}
文章来源:https://www.toymoban.com/news/detail-680261.html
5.4算法效率分析
文章来源地址https://www.toymoban.com/news/detail-680261.html
到了这里,关于考研数据结构:第八章 排序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!