软件正在统治世界,而软件的核心是算法;互联网即将统治世界,其管理、使用的核心也是算法;算法统治着软件和互联网,所以说“算法统治世界”这句话是有一定道理的。
在统治世界的十大算法中 归并排序,快速排序和堆排序 排在首位。
同时排序算法属于最常见,使用最普遍的算法。在我们生活的这个世界中到处都是被排序过的东东。站队的时候会按照身高排序,考试的名次需要按照分数排序,网上购物的时候会按照价格排序,电子邮箱中的邮件按照时间排序……总之很多东东都需要排序,可以说排序是无处不在。
又重要、又用得多,而且不管是初赛还是复赛,都是测试的重点内容,所以要好好学习。
也因为如此,这部分的资料很多,以下内容部分为摘抄整理。
排序的相关概念
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
一、计数排序(Counting Sort)
计数排序是非比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数或其他有序类型。
算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 反向填充有序数组:将每个元素i依次放在新数组中,每放一个元素就将对应计数的C(i)减去1。
动画演示:
练习:
请同学们根据算法描述和动画演示编写计数排序的代码。
#include<iostream>
#include<cstring>
#define N 5005
using namespace std;
int main(){
int i,j,n,a,c[N],d[N],Max,Min;
memset(c,0,sizeof(c));
cin>>n>>a;
Max=Min=a;
c[a]++;
for(i=1;i<n;i++){ //计数排序
cin>>a;
c[a]++;//对每一个元素计数
if(a>Max)Max=a;//找出数据的具体范围
if(a<Min)Min=a;
}
for(i=Min,j=0;i<=Max;i++)//将元素顺序放到d数组中
while(c[i]){
d[++j]=i;
c[i]--;
}
for(i=1;i<=j;i++)
cout<<d[i]<<" ";//输出排序后的数组
return 0;
}
例1:1184 明明的随机数
明明想在学校中请一些同学一起做一项问卷调查,为了实验的客观性,他先用计算机生成了N个1到1000之间的随机整数(N≤100),对于其中重复的数字,只保留一个,把其余相同的数去掉,不同的数对应着不同的学生的学号。然后再把这些数从小到大排序,按照排好的顺序去找同学做调查。请你协助明明完成“去重”与“排序”的工作。
【输入】
有2行,第1行为1个正整数,表示所生成的随机数的个数:N;
第2行有N个用空格隔开的正整数,为所产生的随机数。
【输出】
也是2行,第1行为1个正整数M,表示不相同的随机数的个数。第2行为M个用空格隔开的正整数,为从小到大排好序的不相同的随机数。
【输入样例】
10 20 40 32 67 40 20 89 300 400 15
【输出样例】
8
15 20 32 40 67 89 300 400
思路:
本题符合计数排序条件,待排序的值在一个明显有限的范围内(随机数在1到1000以内)
memset(num,0,sizeof(num));
cin>>n;
for(i=1;i<=n;i++) //桶排序
{
cin>>k;
if(num[k]==0) result++;//去重
num[k]++;
}
cout<<result<<endl;
for(i=1;i<=1000;i++)
if(num[i]) cout<<i<<" ";//桶号(下标)即随机数
例2:1187:统计字符数
【题目描述】
给定一个由a-z这26个字符组成的字符串,统计其中哪个字符出现的次数最多。
【输入】
输入包含一行,一个字符串,长度不超过1000。
【输出】
输出一行,包括出现次数最多的字符和该字符出现的次数,中间以一个空格分开。如果有多个字符出现的次数相同且最多,那么输出ascii码最小的那一个字符。
【输入样例】
abbccc
【输出样例】
c 3
思路:
本题符合计数排序条件,待排序的值在一个明显有限的范围内(要统计的只有a..z这26个字符,其对应的整数值在128以内,可将字符直接作为下标来处理)
int main(){
string s;
int i,j,n,m,a[130];
char c,d;
memset(a,0,sizeof(a));//对a数组清零
cin>>s;
n=s.size();
for(i=0;i<n;i++)
a[s[i]]++;//将字符直接作为下标来处理,统计该字符出现的次数
m=0;
for(c='a';c<='z';c++)
if(a[c]>m){
m=a[c];
d=c;
}
cout<<d<<' '<<m;
return 0;
}
练习
一本通1177:奇数单增序列
【题目描述】
给定一个长度为N(不大于500)的正整数序列,请将其中的所有奇数取出,并按升序输出。
【输入】
第1行为 N;
第2行为 N 个正整数,其间用空格间隔。
【输出】
增序输出的奇数序列,数据之间以逗号间隔。数据保证至少有一个奇数。
【输入样例】
10
1 3 2 6 5 4 9 8 7 10
【输出样例】
1,3,5,7,9
一本通1184
一本通1186
一本通1187
二、选择排序(Selection Sort)
基于比较的排序方式,打擂台的思想
【算法描述】
•将一组数存放在a[1],a[2],…a[n]中
•先用a[1]和其他各个元素进行比较,凡比它小的进行交换,一直比到a[n], a[1]中存放的便是n个数中最小的元素;
• 然后用a[2]和a[3],a[4],…,a[n]进行比较,凡比它小的进行交换,这样a[2]中 存放的便是n个数中次小的元素;
•以此类推,直到直到第n-1元素与第n个元素比较排序为止;
于是a[1]~a[n]便成为一组从小到大排列的队列。
for(i=0;i<n-1;i++)
for(j=i+1;j<n;j++)
if(a[j]<a[i])swap(a[j],a[i]);
动画演示
练习
请根据动画演示写出代码
for(i=1;i<n;i++){//选择排序
k=i;
for(j=i+1;j<=n;j++)
if(a[j]<a[k])k=j;
if(k!=i)swap(a[i],a[k]);
}
选择排序的思路和代码比较简单,打擂台可能是最容易理解的,所以对运行速度要求不高的可以采用该排序方法。
一本通1176
一本通1178
一本通1179
一本通1180
一本通1181
一本通1182
一本通1183
一本通1185
三、冒泡排序(Bubble Sort)
将相邻的两个数两两进行比较,将小的调到前面,大的调到后面,一直循环到最前面的两个数。内循环执行完毕一次之后,最前面的一个数存储的就是最小数。
这种排序方式下小的数就像气泡一样不断地从底部往上升,所以称之后冒泡排序
for(i=0;i<n-1;i++)
for(j=n-1;j>i;j--)
if(a[j]<a[j-1])swap(a[j],a[j-1]);
感觉没有选择排序好理解,从排序效率上略高于选择排序,在比较的时候可以设置一个标记,发现这次内循环没有数据做交换了就说明已经排好序了
for(i=0;i<n-1;i++){
flag=true;//flag作为是否做了交换的标记
for(j=n-1;j>i;j--)
if(a[j]<a[j-1]){
swap(a[j],a[j-1]);
flag=false;//有数据交换就说明还没排好序,标记设为flase
}
if(flag) break;//flag为真表示没有做数据交换,表示已排好序
}
对于那种需要相邻之间两个数两两比较的情况就可以选择使用冒泡排序
算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
动图演示
练习
请根据动画演示和算法描述写出代码
for(i=1;i<=n;i++)cin>>a[i];
for(i=1;i<n;i++)
for(j=i;j<=n-i;j++)
if(a[j+1]<a[j])swap(a[j],a[j+1]);
例:1310:【例2.2】车厢重组
【题目描述】
在一个旧式的火车站旁边有一座桥,其桥面可以绕河中心的桥墩水平旋转。一个车站的职工发现桥的长度最多能容纳两节车厢,如果将桥旋转180度,则可以把相邻两节车厢的位置交换,用这种方法可以重新排列车厢的顺序。于是他就负责用这座桥将进站的车厢按车厢号从小到大排列。他退休后,火车站决定将这一工作自动化,其中一项重要的工作是编一个程序,输入初始的车厢顺序,计算最少用多少步就能将车厢排序。
【输入】
有两行数据,第一行是车厢总数N(不大于10000),第二行是N个不同的数表示初始的车厢顺序。
【输出】
一个数据,是最少的旋转次数。
【输入样例】
4
4 3 2 1
【输出样例】
6
思路:
这道题符合相邻之间两个数两两比较的情况,需要一定的想象力。
旋转的次数就是两两比较后需要交换的次数
代码就是前面的讲解代码+旋转计数器
四、插入排序(Insertion Sort)
也是基于比较的排序,需要用到数组元素的移动操作
算法描述
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
动画演示
练习
请根据动画演示和算法描述写出相应代码
for(i=1;i<=n;i++) // 在前面的有序区间寻找a[i]的位置
{
for(j=i-1;j>=0;j--)//找到比a[i]小的数就退出
if(a[j]<a[i]) break;
if(j!=i-1)
{
temp=a[i];
for(k=i-1;k>j;k--) a[k+1]=a[k]; //将比a[i]大的都往后移
a[k+1]=temp;
}
}
前面选择排序的都可以用插入排序来写一遍
五、快速排序(Quick Sort)
快速排序是最重要的算法之一,属于统治世界的基本算法之一 ☺ :-D。
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
动画演示
快速排序算法模板
void qsort(int s,int t)
{ int i,j,mid,tem;
i=s;j=t;mid=a[(s+t)/2]; //将当前序列在中间位置的数定义为关键值
while(i<=j)
{ while(a[i]<mid)i++; //在左半部分寻找比关键值大的数
while(a[j]>mid)j--; //在右半部分寻找比关键值小的数
if(i<=j){swap(a[i],a[j]);i++;j--;}
}
if(i<t)qsort(i,t); //若未到右边界,则递归搜索右区间
if(s<j)qsort(s,j); //若未到左边界,则递归搜索左区间
}
练习
请根据动画演示和算法描述写出相应代码
void quicksort(int b[],int s,int t){
int i=s,j=t,x=b[i];
do{
while (b[j]>=x && j>i) j--;
if (j>i){
b[i]=b[j];
i++;
}
while (b[i]<=x && i<j) i++;
if (i<j){
b[j]=b[i];
j--;
}
}while(i!=j);
b[i]=x;
i++;
j--;
if(s<j) quicksort(b,s,j);
if(i<t) quicksort(b,i,t);
}
排序
【问题描述】给出n(1<=n<=50000个整数,每个整数都是长整数范围内,请你将他们由小到大排序后输出。
【样例输入】
5
3 2 1 5 4
【样例输出】
1 2 3 4 5
【2007提高】统计数字
【问题描述】某次科研调查时得到了n个自然数,每个数均不超过1500 000000(1.5*10^9)。已知不相同的数不会超过10000个,现在需要统计这些自然数各自出现的个数,并按照从小到大的顺序输出统计结果。
【文件输入】输入文件包含n+1行;第一行是一个整数n,表示自然数的个数,第2~n+1行每一行一个自然数。
【文件输出】输出文件包含m行(m为n个自然数中不相同数的个数)a,按照自然数从小到大的顺序输出。每行输出两个整数,分别是自然数和该数出现的个数,其间用一个空格隔开。
【样例输入】 | 【样例输出】 |
|
|
【数据范围】
40%的数据满足1<=n<=1000
80%的数据满足1<=n<=50000
100%的数据满足1<=n<=200000,每个数均不超过1500000000
六、归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
动图演示
练习
根据算法描述和动画演示写代码
void Merge(int s, int t){
if(s==t)return;
int mid=(s+t)/2;
Merge(s,mid);
Merge(mid+1,t);
int l=s,r=mid+1,k=s;
while(l<=mid&&r<=t)
if(a[l]<=a[r]) b[k++]=a[l++];
else b[k++]=a[r++];
while(l<=mid)b[k++]=a[l++];
while(r<=t) b[k++]=a[r++];
for(int i=s;i<=t;i++)a[i]=b[i];
return;
}
1311:【例2.5】求逆序对
各种算法的比较
前面其中算法是基于比较的排序,后面三种则否
1. 若n较小(如n≤50),可采用直接插入或选择排序;
2. 若序列初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
3. 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:归并排序。
排序算法的原理需要大致搞懂,实际使用的时候大部分时间是用的sort,包含在库文件 algorithm 中,关于sort的使用,详见 sort
练习:
6
5 3 5 2 4 8
选择
快排
归并
比较次数、内存空间
请写出快排、归并排序的过程
9
12 90 25 4 90 78 60 89 15
十四届noip初赛题
与排序的最小交换次数有关
将数组{8,23,4,16,77,-5,53,100}中元素从大到小按顺序排序,每次可以交换任意两个元素,最少要交换( )次.
时间上的优化在于少做运算、做耗时短的运算等。有几个规律需要注意:
1、整型运算耗时远低于实型运算耗时。
2、+、-运算非常快(减法是将减数取补码后与被减数相加,其中位运算更快一些,但是减法也比加法稍慢些。)
3、*运算比加法慢得多,/运算比乘法慢得多,比较运算慢于四则运算,赋值运算慢于比较运算(因为要写内存) ,这些规律我们可以从宏观上把握。
事实上,究竟做了几步运算、几次赋值、变量在内存还是缓存等多数由编译器、系统决定。 但是,少做运算(尤其在循环体、递归体中)一定能很大程度节省时间。 下面来举一个例子:
计算组合数 C(m,n)——n 件物品取出 m 件的组合数。 C(m,n)可用公式直接计算。C(m,n)=n!/((n-m)!m!),C(m,n)=n(n-1)(n-2)...(n-m+1)/(n-m)!。显然,有时所作的乘法少很多,会快一些。
可是这样算真的最快吗?另一条思路是 C(m,n)=C(m,n-1)+C(m-1,n-1),递推下去,直到可利用 C(1,k)=k=C(k-1,k)为止。这种只用加法的运算会快些,尽管加法多一些。
空间上的优化主要在于减小数组大小、降低数组维数等。常用的节省内存的方法有:
1、压缩储存——例:数组中每个数都只有0、1两种可能,则可以按位储存,空间压缩为原来的八分之一。
2、覆盖旧数据——例:矩阵中每一行的值只与上一行有关,输出只和最末行有关,则可将奇数行储存在第一行,偶数行储存在第二行,降低空间复杂度。
要注意的是,对空间的优化即使不改变复杂度、只是改变 n 的系数也是极有意义的。空间复杂度有时对时间也有影响,要想方设法进行优化。