一种比较基础的排序方法。其核心思想是:在数列中从左向右(或从右向左)依次两两比较,若前者大于后者则两者交换位置(最终结果为从小到大排列)。
思考:由于该方法在一次扫描后无法完成排序,所以需要一级循环来控制它的扫描次数;在每一次扫描时需要从左到右遍历一次因此还需要一级循环来控制扫描时的进度。(我个人喜欢将这类问题想象成一个二维坐标,一层循环代表x轴,一层循环代表y轴,通过这两层循环,就可以扫描到二维坐标上的每一个整数点了,然后只需在特定地点的特定条件下做特定的动作即可。)
代码实现:
#include
int a[82]={
1};// 储存需要排序的数列。
int main ()
{
int n,i,j,k;
scanf("%d",&n);//确定数列中数的个数。
for(i=0;i<n;i++){
scanf("%d",&a[i]);//将待排列的数列录入数组中。
}
for(i=0;i<n-1;i++) {
//控制扫描次数
for(j=0;j<n-1-i;j++){
//控制每次扫描的进程
if(a[j]>a[j+1]){
k=a[j];
a[j]=a[j+1];
a[j+1]=k;
}
}
}
for(i=0;i<n;i++){
printf("%d ",a[i]);
}
return 0;
}
冒泡排序算法的弊端:如果数列早在前几次扫描后便以有序,但上述代码依然会继续执行扫描,这样就会使代码效率变差。
冒泡排序算法的优化:可以利用引入一标志变量来解决。当一次扫描完成后,数列未进行交换及证明其以有序(可通过标志变量来判断),当其有序后跳出循环即可。
代码实现:
#include
int a[82]={
1};// 储存需要排序的数列。
int main ()
{
int n,i,j,k,m;
scanf("%d",&n);//确定数列中数的个数。
for(i=0;i<n;i++){
scanf("%d",&a[i]);//将待排列的数列录入数组中。
}
for(i=0;i<n-1;i++) {
//控制扫描次数 。
m==0;//初始化标志变量 (极其重要) 。
for(j=0;j<n-1-i;j++){
//控制每次扫描的进程 。
if(a[j]>a[j+1]){
k=a[j];
a[j]=a[j+1];
a[j+1]=k;
m=1;//若交换则改变标志变量 。
}
}
if(m==0) break;//判断数列是否发生交换。
}
for(i=0;i<n;i++){
printf("%d ",a[i]);
}
return 0;
}
与冒泡排序非常相似的一种排序算法。其核心思想是在扫描的每一轮中都找出剩余数中的最小值(或最大值)放在未排号数列的最左边(最终结果为从小到大排序)。
思考:与冒泡排序相似用两重循环来分别控制行和列,只是其中部分细节不同。
代码实现:
#include
int a[82]={
0};//储存需要排序的数列。
int main ()
{
int n,i,j,k,m;
scanf("%d",&n);//确定数列中数的个数。
for(i=0;i<n;i++){
scanf("%d",&a[i]);//将待排列的数列录入数组中。
}
for(i=0;i<n-1;i++) {
//控制扫描次数 。
k=i;//用于记录未排序的数列最左边的序号。
for(j=i;j<=n-1;j++){
//控制每次扫描的进程 。
if(a[j]<a[k]){
k=j;//找出剩余数中最小的。
}
}
if(k!=i){
//当最小值非最左边的数时,使其与最左边的数交换。
m=a[i];
a[i]=a[k];
a[k]=m;
}
}
for(i=0;i<n;i++){
printf("%d ",a[i]);
}
return 0;
}
快速排序的基本思想:
1.在数列中找到一个基准数。
2.在数列中找到比该数大的数放在其右侧,找到比该数小的数放在其左侧。
3.在基准数的两端放别再进行2步骤直到分别只剩一个数时为止。
思考:看着这个基本思想看起来到还好像挺轻松的,那我们再来看看到底具体怎样实现它呢。
实际上快速排序可以拆分为两个函数:挖空填数法+分治法
以数列中的第一个数为基准数用x来保存它,即第一个数的位置被我们挖了一个空(该位置我们用i记录)。首先从数列右侧开始找到第一个比x小的数(这个数的位置我们用j记录),将其放在我们挖的空中(也就是第一个数的位置)之后i++,此时j的位置就又变成了我们的空;之后从i的位置向左找,找到第一个比x大的数放在我们j的位置之后j–;…重复以上做法直到i与j相当时即可(也就代表了整个数列已经被扫描完了)
代码实现:
int wakong(int a[],int b,int c)
{
int i,j,x;
i=b;j=c;x=a[b];//分别记录左端,右端,基准数 。
while(i<j){
//直到i=j 时停止循环
while(i<j&&a[j]>=x) j--;//从右向左找比基准数小的数并用j记录其位置
if(j>i){
a[i]=a[j];//将找到的数放在i的空中
i++;
}
while(i<j&&a[i]<x) i++;//从左到又找比基准数大的数并用i记录其位置
if(j>i){
a[j]=a[i];//将找到的数放在j的空中
j--;
}
}
a[i]=x;//当扫描完整个数列后i=j(此时在数列的中间位置),此时将 x(基准数) 填入该空即可
return i;//返回中间的位置将数列分为左区和右区以便分治法时确定端点
}
分治法的精髓:
分–将问题分解为规模更小的子问题;
治–将这些规模更小的子问题逐个击破;
合–将已解决的子问题合并,最终得出“母”问题的解;
类似于递归的思想。(关于递归我会再写一篇博客在其中详细说明)。
代码实现:
void quck(int a[],int b,int c)
{
if(b<c){
int i=wakeng(a,b,c);//调用挖空法调整端点的位置;
quck(a,b,i-1);//左区递归
quck(a,i+1,c);//右区递归
}
}
将两种函数结合起来就是我们所说的快速排序法了。
代码实现:
#include
int wakong(int a[],int b,int c)
{
int i,j,x;
i=b;j=c;x=a[b];//分别记录左端,右端,基准数 。
while(i<j){
//直到i=j 时停止循环
while(i<j&&a[j]>=x) j--;//从右向左找比基准数小的数并用j记录其位置
if(j>i){
a[i]=a[j];//将找到的数放在i的空中
i++;
}
while(i<j&&a[i]<x) i++;//从左到又找比基准数大的数并用i记录其位置
if(j>i){
a[j]=a[i];//将找到的数放在j的空中
j--;
}
}
a[i]=x;//当扫描完整个数列后i=j(此时在数列的中间位置),此时将 x(基准数) 填入该空即可
return i;//返回中间的位置将数列分为左区和右区以便分治法时确定端点
}
void quck(int a[],int b,int c)
{
if(b<c){
int i=wakeng(a,b,c);//调用挖空法调整端点的位置;
quck(a,b,i-1);//左区递归
quck(a,i+1,c);//右区递归
}
}
int main ()
{
int n,i,j,k,m,x;
int num[82];
scanf("%d",&n);//确定数列中数的个数。
for(i=0;i<n;i++){
scanf("%d",&num[i]);//将待排列的数列录入数组中。
}
quck(num,0,n-1);
for(i=0;i<n;i++){
printf("%d ",num[i]);
}
}
插入排序的基本思想:将一个数放在在一个有序数列里的正确位置中。
思考:开始看到它的基本思想是时,我心想难不成要拿一个数组存储有序数列,从另一个数组中拿数往里面放?实际上并不用这么麻烦,用我们在快速排序里讲到的挖空填数法便可以很好的解决这个问题了。
代码实现:
1 #include<stdio.h>
2 int main ()
3 {
4 int a[82];
5 int n,i,j,temp;
6 scanf("%d",&n);
7 for(i=0;i<n;i++){
8 scanf("%d",&a[i]);
9 }
10 for(i=1;i<n;i++){
//这里假设第一个数已经有序,从第二个开始。
11 temp=a[i];//将要找位置的数对应的值保存起来。
12 j=i-1;//从该数以左分别与其比较。
13 while(j>=0&&a[j]>temp){
14 a[j+1]=a[j];//当其后的数比它大时,其后的数前移。
15 j--;//继续向后继续判断。
16 }
17 a[j+1]=temp;//此时其后的数比其小则在该位置讲保存的值放下。
18 }
19 for(i=0;i<n;i++){
20 printf("%d ",a[i]);
21 }
22 }
代码讲解:这里我们假设第一个数即为有序数列,我们从第二个数开始给它找位置(我们用一个temp暂存它的值),我们从第二个数以后依次与其进行比较,倘若其后的数比其大,则其后的数向前移动,继续向后进行比较;倘若其后的数比其小,则证明以找到合适的位置,则将该值(temp)放在比它小的前一个数即可。然后开始找第三个数,以此类推。直到找到数列最后一个数的正确位置,则证明数列以有序。
希尔排序实际上是插入排序的升级版,所以熟练掌握了插入排序那么希尔排序也是非常简单的。
算法基本思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
思考:首先我们可以把每次的增量设置为n/2(下一次设为n/4,再下一次设为n/8…直到增量为1)(n为数列的总数)假设数列共有8位,那么第一次增量便为4,则整个数列被分为四组其中(0,4)(1,5)(2,6)(3,7)各为一组,然后在每一组中进行插入排序;接下来增量变为2,则整个数列被分为2组其中(0,2,4,6,)(1,3,5,7)各为一组,再在每一组中进行插入排序;接下来增量变为1,则整个数列为1组,对这一组进行插入排序后整个数列就变有序了,自此希尔排序便结束了。(提示:希尔排序在进行时并不是分别对每一组进行插入排序,而是各组交叉进行,这一点在代码中可以很容易看出)
补充:看到这里可能有的读者会想希尔排序这么麻烦才把数列变的有序,那我为什么不直接用插入排序呢?事实上是应为直接插入排序的优势是:插入排序在对比较短小的数列或几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;但其缺点在于:面对比较混乱的数列时,它的效率会非常低,实际上插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。所以才有了希尔排序,将一个数列分为不同的组,在这些组内的数列一般都比较短可以提高插入排序的效率,并且在增量等于1时经过前几轮的排序数列以变的基本有序了,这样也可以提高插入排序的效率。所以说希尔排序是插入排序的升级版。
代码展示:
#include
void fun(int a[82],int i,int j,int n)//按增量分组进行的插入排序函数
{
int m;
int z=a[j];
for(m=j-i;m>=0&&a[m]>z;m=m-i) a[m+i]=a[m];
a[m+i]=z;
}
int main()
{
int n;
int a[82];
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
for(int i=n/2;i>0;i=i/2){
//控制每次分组的增量,当i=1时即可完成排序
for(int j=0;j<n;j++){
//从这里就可以看出,在插入排序时是对各组分别进行的
fun(a,i,j,n);
}
}
for(int i=0;i<n;i++) printf("%d ",a[i]);
}
题外话:从直接插入排序和希尔排序两个算法中我们便可以看出事实上算法其实也是有优劣之分的,而一个算法的优劣是如何判断的呢?一般情况下我们从以下几个角度判断:
1.时间复杂度。2.空间复杂度。3.正确性。4.稳定性(也叫容错性)。5.算法思路是否简单(也可以叫做可读性)。
(这一块的知识就不详细阐明了,之后我也会单独写篇博客来详细说明)
桶排序是我个人认为最简单粗暴的一种排序方法,也是比较容易实现的一种排序算法。
算法基本思想:在一个数列中必有其最大值和最小值,而我们可以拿俩值差(max-min)个空盒子,那么这些盒子会将每一个数都覆盖,然后我们给这些盒子按照从min到max的顺序标上号,最后我们遍历数列,遍历到的数其对应的盒子都放上一个小球。那么每一个数都有了其对应的位置,然后我们按照盒子的标号顺序及其中小球的数量将其代表的数输出即可得到有序数列。(其中标号代表数字大小,小球个数代表该数字有多少)
思考:我们可以拿出一个很大的数组(事实上如果读者学习了动态内存申的请,那么就可以申请数列最大值长度的数组了)让每一个数组值都是0(代表数组小标数字的个数为0个),将数列中的每一个数都放在对应的位置上(例:数字8,则我们给arr[8]++)代表数字8有一个。将数列里的所有数都遍历完后,我们只需从小到大依次输出数组的下标及其数组值对应的含义即可。(数组下标代表数字的大小,数组值代表了该数字的多少)
代码展示:
#include
int a[100000]={
0};//一个用来储存数列,一个用来当盒子
int b[100000]={
0};
int main ()
{
int n,i;
scanf("%d",&n);
for(i=0;i<n;i++) scanf("%d",&a[i]);
for(i=0;i<n;i++){
b[a[i]]++;//给每个数列的值找对应的盒子
}
for(i=0;i<100000;i++){
if(b[i]!=0){
while(b[i]--){
//按照对应含义输出即可
printf("%d ",i);
}
}
}
}
基数排序的原理类似与桶排序,但并没有桶排序那么简单粗暴。
算法基本思想:排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序。
步骤详解:先定义一个二维数组用来暂存数据,其次在无序的数列之中找到最大值,并判断它是几位数,以此来确定循环次数。第一次循环先对个位进行分配,把每个数按照个位对应的数依序存放在二维数组中,然后是收集,把二维数组中的数据按照个位数由小到大的顺序依序依次重新赋值给数列。第二次循环再对十位数重复上述操作 …直到把每一位都“分配”和“收集”过后(也就是达到了循环次数时)原先数列就变为有序数列了。
举例说明:数列:58 89 1 56 895 32 5 647
首先该数列中最大值为895为三位数,所以循环三次。
第一次循环:
分配
0:
1: 1
2: 32
3:
4:
5: 895 , 5
6: 56
7: 647
8: 58
9: 89
收集:1 32 895 5 56 647 58 89(收集后的数列)
第二次循环:
分配
0: 1 , 5
1:
2:
3: 32
4: 647
5: 56 , 58
6:
7:
8: 89
9: 895
收集: 1 5 32 647 56 58 89 895(收集后的数列)
第三次循环:
分配
0: 1 , 5 , 32 , 56 , 58 , 59
1:
2:
3:
4:
5:
6: 647
7:
8: 895
9:
收集:1 5 32 56 58 59 647 895(收集后的数列)
自此三次循环都结束了最后一次得到的数列也变为了有序数列。
代码展示:
1 #include<stdio.h>
2 int fun(int a,int b)//笔者在这里自己写了一个计算a的b次的函数,功能类似于pow函数
3 {
4 int sum=1;
5 for(int i=0;i<b;i++){
6 sum=sum*a;
7 }
8 return sum;
9 }
10 int main ()
11 {
12 int a[82];
13 int b[10][82]={
0};//用二维数组来暂存数据
14 int n,sum=0,m=0;
15 scanf("%d",&n);
16 for(int i=0;i<n;i++){
//找到数列中的最大值
17 scanf("%d",&a[i]);
18 if(i==0) sum=a[i];
19 if(a[i]>sum) sum=a[i];
20 }
21 while(sum>0){
//按照最大值位数控制循环次数
22 for(int i=0;i<n;i++){
23 for(int k=0;k<n;k++){
24 if(b[(a[i]/fun(10,m))%10][k]==0){
25 b[(a[i]/fun(10,m))%10][k]=a[i];//将数据按照对应关系分配到二维数组中
26 break;
27 }
28 }
29 }
30 int i=0;
31 for(int q=0;q<10;q++){
32 for(int j=0;j<82;j++){
33 if(b[q][j]!=0){
34 a[i]=b[q][j];//收集二维数组中的数据,获得收集后的数列
35 i++;
36 }
37 }
38 }
39 sum=sum/10;//与while中的条件一同控制循环次数
40 m++;
41 for(i=0;i<10;i++){
42 for(int j=0;j<82;j++){
43 b[i][j]=0;//将二维数组清零,以便下一次存放数据
44 }
45 }
46 }
47 for(int i=0;i<n;i++){
48 printf("%d ",a[i]);
49 }
50 printf("\n");
51 }
归并排序采用的主要方法是我们在快速排序中讲的分治法,所以理解好了快速排序,实际上归并排序也并不难理解。
算法思想:归并排序主要分两个步骤=归+并。
一个无序数列我们可以将它分为两个部分,其次设法将这两个部分变为有序,然后把这两个有序部分归为一个数列即可。
那么问题来了,我们怎样将两个部分的无序数列变为有序呢?
事实上我们可以把这两个部分继续分下去,直到每个部分都只剩一个元素的时候,那么每个部分也就默认有序了(分的过程其实就是归)。所以从这里我们可以看出归并的主要问题就落在了如何把两个有序数列合并为一个有序数列。(这个过程就是并)
并:
我们可以另外开辟一个数组来暂存数据。由于两个数列都已经有序,我们只需从两个数列的低位轮番拿出各自最小的数来PK就就行了,输的一方为小值,将这个值放入临时数组,然后输的一方继续拿出一个值来PK,直至有一方没有元素后,将另一方的所有元素依次接在临时数组后面即可。
代码展示:
3 int b[82];//暂时存储数据的数组
4 int a[82];
5 void fun(int low,int mid,int high)//一个有序数列在数组中的位置在low到mid;另一个在mid+1到high
6 {
7 int i=low,j=mid+1,k=low;
8 while(i<=mid&&j<=high){
//两个数列轮番pk
9 if(a[i]<a[j]){
10 b[k++]=a[i++];
11 }else {
12 b[k++]=a[j++];
13 }
14 }
15 while(i<=mid){
//将赢的数列的剩下的部分依次接在临时数组中
16 b[k++]=a[i++];
17 }
18 while(j<=high){
19 b[k++]=a[j++];
20 }
21 for(i=low;i<=high;i++){
//把整合好的数列放会原来的数列中
22 a[i]=b[i];
23 }
24 }
归:
归的方法实质上就是二分法,我们在快速排序中已经讲过了,这里就不再赘述了。直接上代码。
代码展示:
25 void mergesort(int n,int m)
26 {
27 if(n<m){
28 int mid=(n+m)/2;
29 mergesort(n,mid);
30 mergesort(mid+1,m);
31 fun(n,mid,m);
32 }
33 }
自此归并排序就已经写完了,接下来我们就来看看完整代码吧。
代码展示:
1 #include<stdio.h>
2 #include<string.h>
3 int b[82];//暂时存储数据的数组
4 int a[82];
5 void fun(int low,int mid,int high)//一个有序数列在数组中的位置在low到mid;另一个在mid+1到high
6 {
7 int i=low,j=mid+1,k=low;
8 while(i<=mid&&j<=high){
//两个数列轮番pk
9 if(a[i]<a[j]){
10 b[k++]=a[i++];
11 }else {
12 b[k++]=a[j++];
13 }
14 }
15 while(i<=mid){
//将赢的数列的剩下的部分依次接在临时数组中
16 b[k++]=a[i++];
17 }
18 while(j<=high){
19 b[k++]=a[j++];
20 }
21 for(i=low;i<=high;i++){
//把整合好的数列放会原来的数列中
22 a[i]=b[i];
23 }
24 }
25 void mergesort(int n,int m)//二分法函数
26 {
27 if(n<m){
28 int mid=(n+m)/2;
29 mergesort(n,mid);
30 mergesort(mid+1,m);
31 fun(n,mid,m);//直接调用并的函数
32 }
33 }
34 int main ()
35 {
36 int n;
37 scanf("%d",&n);
38 for(int i=1;i<=n;i++) scanf("%d",&a[i]);
39 mergesort(1,n);
40 for(int i=1;i<=n;i++) printf("%d ",a[i]);
41 printf("\n");
42 }