★★★★★排序
当数据量非常大时,排序的效率就非常重要;所以以下学习的排序的数据起步量都是10000个以上
各种排序的统一函数名规范:
void X_Sort( ElementType A[],int N );
排序名称 待排元素 数组大小
放在数组里
元素类型包括所有能排大小的数据结构,比如数字,字符串等等,都可以用排序算法;
.默认为将整数从小到大排序:为了简单起见,所有排序算法的理解和代码都是以整数为例;
.输入的N为正整数;
.只讨论基于比较的排序(> = < 有定义):对于要排序的数据类型,有明确的大小相等的定义;
.只讨论内部排序:假设内存空间充足,所有数据能一次性的导入内存空间;排序过程在内存中完成;
外部排序:数据量大于内存,需要分若干次数据的导入导出才能完成的排序;
.稳定性:任意两个相等的数据,排序前后的相对位置不发生改变;
.没有一种排序时任何情况下都表现最好的:每一种算法都有它存在的理由
当数据具有某种特征的时候,某一种算法可能是最好的
测试用例说明:
给定N个(长整型范围内的)整数,要求输出从小到大排序后的结果。
本题旨在测试各种不同的排序算法在各种数据情况下的表现。各组测试数据特点如下:
- 数据0:只有1个元素;
- 数据1:11个不相同的整数,测试基本正确性;
- 数据2:10^3个随机整数;
- 数据3:10^4个随机整数;
- 数据4:10^5个随机整数;
- 数据5:10^5个顺序整数;
- 数据6:10^5个逆序整数;
- 数据7:10^5个基本有序的整数;
- 数据8:10^5个随机正整数,每个数字不超过1000。
输入格式:
输入第一行给出正整数N(≤105),随后一行给出N个(长整型范围内的)整数,其间以空格分隔。
输出格式:
在一行中输出从小到大排序后的结果,数字间以1个空格分隔,行末不得有多余空格。
输入样例:
11 4 981 10 -17 0 -20 29 50 8 43 -5
输出样例:
-20 -17 -5 0 4 8 10 29 43 50 981
代码长度限制 16 KB
时间限制 10000 ms
内存限制 64 MB
※简单排序:
▶♥ 冒泡排序
20 把数据想象成泡泡,大数据=>大泡泡,小数据=>小泡泡;
12 不断地做从上到下扫描,每当看到更大的大泡泡时,就把它往下压;
39 每次从上到下扫描一遍:如果A[i] > A[i+1];Swap(A[i],A[i+1]);
9 所以每一次扫描就是只将最大的泡泡交换到最底下
60 当扫描一遍发现已经都排好序了,即没有发生Swap,那就跳出循环
8
42 void Bubble_Sort( ElementType A[],int N )
3 {
28 int i,Tag = 1; //存在需要排序的情况
while(Tag){
Tag = 0; //假设已经都排好了
for(i=0;i<N;i++){
if(A[i] > A[i+1]){
Swap( &A[i],&A[i+1] );
Tag = 1; //有Swap发生,还需要扫描
}
}
}
}
代码优化:由于每一次扫描一定是将最大的泡泡压到了最底下,所以下一次扫描时,只需扫描到倒数第二的位置就可以啦
以此类推,每扫描一遍,下次需要扫描的长度就-1;
void Bubble_Sort( ElementType A[],int N ) void Bubble_Sort( ElementType A[],int N )
{ {
int i,Tag = 1; int i,P,flag;
while(Tag){ for(P=N-1;P>0;P--){
Tag = 0; flag = 0;
for(i=0;i<N-1;i++){ for(i=0;i<P;i++){
if(A[i] > A[i+1]){ if(A[i] > A[i+1]){
Swap( &A[i],&A[i+1] ); Swap( &A[i],&A[i+1]);
Tag = 1; flag = 1;
} }
} }
if(flag == 0){
N--; break;
} }
} }
}
时间复杂度:最好情况 T=O(N)
最坏情况 T=O(N^2)
冒泡排序的缺点:时间复杂度达到O(N^2)
优点:①如果待排元素放在链表里,同样适用
②具有稳定性
.
测试结果:
代码:
#include <stdio.h>
void Swap( int* a,int* b );
void Print( int A[],int N );
void Bubble_Sort( int A[],int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Bubble_Sort( A,N );
Print( A,N );
return 0;
}
void Bubble_Sort( int A[],int N )
{
int i,P,IsAllInOrder;
for(P=N-1;P>0;P--){
IsAllInOrder = 1;
for(i=0;i<P;i++){
if(A[i]>A[i+1]){
Swap( &A[i],&A[i+1] );
IsAllInOrder = 0;
}
}
if(IsAllInOrder){
break;
}
}
}
void Swap( int* a,int* b )
{
int Temp = *a;
*a = *b;
*b = Temp;
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
▶♥ 插入排序
将排序的过程模拟为抓牌的过程,
抓第一张,放在A[0],不动
抓第二张 和前面的比较 如果前面的比较大 那就后移一位 即A[1] = A[0];
抓第三张 和第二张比较 如果第二张比较大 那就后移一位 即A[2] = A[1];再和第一张比较 第一张大 那就A[1] = A[0];然后A[0] = 这张牌
... ... 手里这张牌大 那就A[1] = 这张牌
如此循环 直到抓完最后一张牌
抓第一张牌10 放手里 10
抓第二张牌8 对比 8 10
抓第三张牌J 对比 8 10 J
抓第四张牌3 一张张 对比 3 8 10 J
void Insertion_Sort( ElementType A[],int N )
{
for(P=1;P<N;P++){//从第二张牌来时比较 直到最后一张
Temp = A[P]; //抓起来放手里
for(i=P;i>0;i--){//依次和前面的牌比较
if(A[i-1] > Temp){ //如果不符合顺序
A[i] = A[i-1]; //后移一位
}else{ //否则
A[i] = Temp; //新牌落座
break; //不用再比了,跳出循环
}
}
}
}
代码优化:
void Insertion_Sort( ElementType A[],int N )
{
for(P=1;P<N;P++){
Temp = A[P]; /* 摸下一张牌 */
for(i=P;i>0 && A[i-1]>Temp;i--){
A[i] = A[i-1]; /* 移出空位 */
}
A[i] = Temp; /* 新牌落座 */
}
}
时间复杂度:最好情况 T=O(N)
最坏情况 T=O(N^2)
优点:程序简单,步骤少,具有稳定性
.
测试结果:
比冒泡好很多;
代码:
#include <stdio.h>
void Print( int A[],int N );
void Insertion_Sort( int A[],int N );
int main()
{
int N;
scanf("%d",&N);
int i,A[N];
for(i=0;i<N;i++){
scanf("%d",&A[i]);
}
Insertion_Sort( A,N );
Print( A,N );
return 0;
}
void Insertion_Sort( int A[],int N )
{
int P,i,Temp;
for(P=1;P<N;P++){
Temp = A[P];
for(i=P;i>=1 && A[i-1]>Temp;i--){
A[i] = A[i-1];
}
A[i] = Temp;
}
}
void Print( int A[],int N )
{
int i;
for(i=0;i<N-1;i++){
printf("%d ",A[i]);
}
printf("%d",A[N-1]);
}
※排序进阶:
※时间复杂度下界
.对于下标i<j,如果A[i]>A[j],则称(i,j)是一对逆序对(inversion)
.问题:序列{34,8,64,51,32,21}中有多少逆序对?
(34,8) (34,32) (34,21) (64,51) (64,32) (64,21) (51,32) (51,21) (32,21) 9对
简单排序(比如冒泡排序 插入排序 Low选择排序) 每次都只是交换一个逆序对,所以交换的次数都是9次
.交换2个相邻元素正好消去1个逆序对
.插入排序:T(N,I)=O(N+I) I是逆序对的个数
口 如果序列基本有序,则插入排序简单且高效
.定理:任意N个不同元素组成的序列平均具有N(N-1)/4个逆序对
.定理:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为Ω(N^2) Ω指的是下界
.这意味着:要提高算法效率,我们必须
==>每次消去不止1个逆序对!
==>每次交换相隔较远的2个元素!
.
▶♥ 希尔排序 (by Donald Shell)
利用插入排序的简单,同时克服插入排序每次只交换相邻两个元素的这个缺点
例如: 81 94 11 96 12 35 17 95 28 58 41 75 15
先做5-间隔排序 即(81 35 41) (94 17 75) (11 95 15) (96 28) (12 58)
35 17 11 28 12 41 75 15 96 58 81 94 95
再做3-间隔排序 28 12 11 35 15 41 58 17 94 75 81 96 95
最后1-间隔排序
.定义增量序列Dm>D(m-1)>...>D1=1
.对每个Dk进行"Dk-间隔"排序(k=M,M-1,...,1)
.注意到:更小间隔的排序没有将上一步的结果变坏
"Dk-间隔"有序的序列,在执行"Dk-1-间隔"排序后,仍然是"Dk-间隔有序"的
原始希尔排序 Dm=N/2,Dk=D(K+1)/2
void Shell_Sort( ElementType A[],int N )
{
for(D=N/2;D>0;D/=2){
/* 插入排序 */
for(P=D;P<N;P++){
Temp = A[P];
for(i=P;i>=D && A[i-D]>Temp;i-=D){
A[i] = A[i-D];
}
A[i] = Temp;
}
}
}
希尔排序不具有稳定性
最坏情况:T=Θ(N^2) Θ既是上界又是下界 O是一个上界(有可能达不到) Ω是一个下界
例如: 1 9 2 10 3 11 4 12 5 13 6 14 7 15 8 16
按8-间隔 4-间隔 2-间隔排序后没有任何变化
问题点:增量元素不互质,则小增量可能根本不起作用
更多增量序列:
◎Hibbard增量序列
.Dk = 2^k-1 ————————相邻元素互质
.最坏情况:T=Θ(N^3/2)
.猜想:Tavg = O(N^%/4)
◎Sedgewick增量序列
.{1,5,19,41,109,...} ——————9*4^i-9*2^i+1 或 4^i-3*2^i+1
.猜想:Tavg = O(N^7/6),Tworst = (N^4/3)
希尔排序本质上还是插入排序,只不过先用增量调整了不相邻的逆序对。
Sedgewick增量序列:int Sedgewick[] = {260609,146305,64769,36289,16001,8929,3905,2161,929, 505,209, 109, 41, 19, 5, 1, 0};
即使一个简单的算法,但是关于它的时间复杂度分析可能非常的难
void Shell_Sort( ElementType A[],int N )
{ /* 希尔排序—— 用Sedgewick增量序列 */
int Si,D,P,i;
ElementType Temp;
/* 这里只列出一小部分增量 */
int Sedgewick[] = {929,505,209,109,41,19,5,1,0};
for(Si=0;Swdgewick[Si]>=N;Si++);//初始增量Sedgewick[Si]不能超过待排序列长度
for(D=Sedgewick[Si];D>0;D=Sedgewick[++Si]){
for(P=D;p<N;P++){
Temp = A[P];
for(i=P;i>=D && A[i-D]>Temp;i-=D){
A[i] = A[i-D];
}
A[i] = Temp;
}
}
}
.
总过程:
分解步骤:
第一步:增量为4
第二步:增量为2
第三步:增量为1