目录
前言
排序算法的重要性不言而喻在算法竞赛中经常考到因此我们要好好学习!
一.排序的基本概念
1.什么是就地排序
使用恒定的额外空间,只需要使用他给你的数据
一个就地排序算法使用恒定的的额外空间来产生输出(仅修改给定的数组)。它仅通过修改线性表中元素的顺序来对线性表进行排序。
例如,插入排序和选择排序是就地排序算法,因为它们不使用任何额外的空间来对线性表进行排序。而归并排序和计数排序的经典实现就不是就地排序算法
2.什么是内部排序和外部排序
待排序数据,是否可以一次性的载入到内存中
当所有待排序记录不能被一次载入内存进行处理时,这样的排序就被称为外部排序。
外部排序通常应用在待排序记录的数量非常大的时候。归并排序以及它的变体都是典型的外部排序算法。外部排序通常与硬盘、CD等外部存储器(辅存)关联在一起。当所有待排序记录可以一次载入内存时,则称为内部排序。
3.什么是稳定排序
判断相同的关键字,排序以后,相对位置的变化,处理键值对的时候
当我们对可能存在重复键的键值对(例如,人名作为键,其详细信息作为值)按照键对这些对象 进行排序时,稳定性就显得至关重要
4.判定一个排序算法的是稳定的
如果两个具有相等关键字的对象在排序前后的相对位置没有发生变化,则认为排序算法是稳定的。可以形式化地描述为:
设A表示一个待排序的数组, <表示数组A的一个严格的弱排序(即有重复元素)。一个排序算法稳定,当且仅当i < j^A[i]≡A[j] ,且隐含π(i) < π(j),其中π表示排序后的序列(排序算法将A[i]移动到了π(i)的位置,将A[j]移动到了π[j]的位置,但是i和j的相对位置保持不变)。
图中展示的就是稳定排序的例子,简单来讲,排序前,青色球 10 在蓝色球 10 的前面,那么排序后两者的相对位置并没有改变,青色球 10 还是在红色球的前面;排序前蓝色球 20 在青色球 20 的前面,则排序后两者的两者的位置没有发生变化,依旧是蓝色在青色的前面
简而言之,排序前后序列中键值相等的元素的相对位置没有发生变化的就是稳定排序
二.插入排序算法
1.直接插入排序
在玩扑克牌的时候,我们抽到一张牌的时候,都是将它插入到当前手中牌的合适位置的。直接插入排序也是这样的思想
1.1基本思想
插入排序的思想是:
将待排序序列分成两个序列,前面的序列保持有序,依次选取后面的序列的元素,在前面的序列中进行插入。初始时,有序序列的长度为1。
给定序列 [9 , 20 , 13 , 10 , 12 ] 。初始状态如下:
分成的两个序列如下:
也就是说,此时我们讲数组当中的第一个元素9当作有序元素。
第一次插入:将20和9做比较,20>9,顺序没有问题。不动。
第二次插入:将13与20比较,13<20,此时20就要先到13的位置。再跟9比较,13>9,那么此时
将13插入到9后面
第三次插入,将10和20比较,10<20,20去10的位置,再将10和13比较,10<13,则13也往下移动,再将10和9比较,10>9,则将10插入到9的后面
第四次插入:将12和20比较,12 <20,20后移,再将12和13比较,12<13,13后移,再将12和10比较,12 > 10,将12插入到10的后面
1.2复杂度
在排序前元素已经是按需求有序了,每趟只需与前面的有序元素序列的最后一个元素进行比较,总的排序码比较次数为n-1,元素移动次数为0。时间复杂度为 O(n);
而在最差的情况下,及第i趟时第i个元素必须与前面i个元素都做排序码的比较,并且每做一次就叫就要做一次数据移动,此时的时间复杂度为O(n^2) ;所以直接插入排序的时间复杂度为O(n^2) 插入排序不适合对大量数据进行排序应用,但排序数量级小于千时插入排序的效率还不错,可以考虑使用。插入排序在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。直接插入排序采用就地排序,空间复杂度为O(1)
1.3稳定性
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。如果碰见一个和插入元素相等的,那么将会把待插入元素放在相等元素的后面。所以,相等元素的相对的前后顺序没有改变,所以插入排序是稳定的
1.4代码演示
#include<stdio.h>
#include<stdlib.h>
/*
直接插入排序:是就地排序,是稳定的,时间复杂度:O(n^2)
*/
int a[105];
int n;
int main()
{
int t;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
//认为:a[1] 是有序区域,a[2---n]是乱序区
for(int i=2;i<=n;i++)
{
t=a[i];
int j;
for(j=i-1;j>=1;j--)
{
if(a[j]>t)
{
a[j+1]=a[j];
}
else{
break;
}
}
a[j+1]=t;
}
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
return 0;
}
2.折半插入排序
折半插入排序是一种插入排序算法,通过不断地将数据元素插入到合适的位置进行排序,在寻找插入点时采用了折半查找
2.1基本思想
折半插入排序的基本思想是:顺序地把待排序的序列中的各个元素按其关键字的大小,通过折半查找插入到已排序的序列的适当位置。
从名字就能看出来,运用了二分查找的插入排序。在上面标准的插入排序算法中,我们会将待插入关键字 key = arr[i] ,然后在数组 [0,i - 1] 的范围内查找待插入关键字 key 的正确位置,这里的查找操作的时间复杂度为O(n)量级。但是如果使用二分查找在数组 arr 的 [0,i - 1] 的范围内查找关键字 key ,那么就可以将查找操作的时间复杂度降到O(logn)量级
2.2性能
折半查找只是减少了比较次数,但是元素的移动次数不变。折半插入排序平均时间复杂度O(n^2);空间复杂度为O(1);是稳定的排序算法
3.2-路插入排序算法
二路插入排序算法是在折半排序的基础上对其进行了改进,减少其在排序过程中移动记录的次数从而提高效率。
具体实现思路为:另外设置一个同存储记录的数组大小相同的数组 d,将无序表中第一个记录添加进d[0]的位置上,然后从无序表中第二个记录开始,同 d[0] 作比较:如果该值比d[0] 大,则添加到其右侧;反之添加到其左侧。其实就是对于环形数组的插入
4.希尔排序
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序
4.1基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap =gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量
原始数组以相同的颜色为一组
初始增量gap=length/2=5,意味着整个数组被分为5组,[8,3],[9,5],[1,4],[7,6],[2,0]。
然后我们对这5组分别进行直接插入排序,结果就变成
可以看见例如3,5,6这些小元素就在前面了,然后缩小增量gap = 5/2 = 2,将数组分成两组
对以上的两组再分别进行直接插入排序,结果如下。此时整个数组的有序程度就更近一步了
再缩小增量,gap=2/2=1.此时整个数组为1组。[0,2,1,4,3,5,7,6,9,8]
此时,只需要对以上数列简单微调。无需大量的移动操作即可完成整个数组的排序
4.2 性能
希尔排序中对于增量序列的选择十分重要,直接影响到希尔排序的性能。我们上面选择的增量序列{n/2,(n/2)/2...1}(希尔增量),其最坏时间复杂度依然为O(n2)。这是为什么呢?
到底什么地方出了问题呢?我们再来看一个坏的例子,假设这是我们的初始序列,如果用shell的增量序列我们会一开始怎么做呢?这一共有16个数字
我们一开始就做8间隔的排序,8间隔的排序我们就从1开始,然后往后数7个数字,就是排1和5,发现本来就是有序的,什么都不用动,然后下一个,就是9和13,也是有序的,2和6,还是有序的,10和14还是有序的,继续往后看,就会发现,我做了一个8间隔的排序,结果哪个元素都没有动,大家保持原样的走下来了,下一步我要做4间隔的,结果还是全部都是有序的。
结果这趟白跑了,2间隔的排序,你应该猜到结果了,还是什么都没干,最后要达到有序,还是得靠我们1间隔的排序。结果这趟白跑了,2间隔的排序,你应该猜到结果了,还是什么都没干,最后要达到有序,还是得靠我们1间隔的排序。所以这其实是一个让人非常囧的情况,就是前面我白做了3趟排序,然后最后还是跟原始的插入排序一样,还不如一开始我就直接做原始的插入排序,那到底什么地方出了问题呢?我们通过仔细的分析会发现因为它的增量元素不互质,8是4的倍数,4是2的倍数,2是1的倍数,那么小的增量就有可能在后面的排序里面根本不起作用
4.3Hibbard增量序列
4.4更多的增量序列
4.5代码演示
#include<stdio.h>
#include<stdlib.h>
/*
希尔排序:取希尔增量序列时: 是就地排序,不是稳定的,时间复杂度:O(n^2)
*/
int a[105];
int n;
int main()
{
int t;
scanf("%d",&n);
int k=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for(int d=n/2;d>=1;d=d/2)
{
k++;//计算趟数
//以增量d分组,对每组进行直接插入排序
for(int i=1+d;i<=n;i++)
{
t=a[i];
int j;
for(j=i-d;j>=1;j=j-d)
{
if(a[j]>t)
{
a[j+d]=a[j];
}
else{
break;
}
}
a[j+d]=t;
}
printf("第%d趟,增量为%d,排好的结果:",k,d);
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
return 0;
}
三.交换排序
1.冒泡排序
1.1算法思想

第一轮冒泡排序:第一步:比较 5 和 1 ,5 > 1,则交换 5 和 1 的位置:

第二步,比较 5 和 4,5 > 4,交换 5 和 4 的位置:
<