20150310_经典排序算法
@(数据结构)
1.双向冒泡排序
双向冒泡排序算法的运作如下:
传统冒泡气泡排序的双向进行,先让气泡排序由左向右进行,再来让气泡排序由右往左进行,如此完成一次排序的动作
使用left与right两个旗标来记录左右两端已排序的元素位置。
一个排序的例子如下所示:
排序前: 45 19 77 81 13 28 18 19 77 11
往右排序:19 45 77 13 28 18 19 77 11 [81]
向左排序:[11] 19 45 77 13 28 18 19 77 [81]
往右排序:[11] 19 45 13 28 18 19 [77 77 81]
向左排序:[11 13] 19 45 18 28 19 [77 77 81]
往右排序:[11 13] 19 18 28 19 [45 77 77 81]
向左排序:[11 13 18] 19 19 28 [45 77 77 81]
往右排序:[11 13 18] 19 19 [28 45 77 77 81]
向左排序:[11 13 18 19 19] [28 45 77 77 81]
如上所示,括号中表示左右两边已排序完成的部份,当left >= right时,则排序完成。
附传统冒泡排序 与双向冒泡排序
#include "stdio.h"
#include "stdlib.h"
int tmp;
#define SWAP(a,b) tmp=a,a=b,b=tmp //宏定义SWAP,预编译,省去调用函数的时间,比是myswap速度快。
void myswap(int *x,int *y)
{
int tmp;
tmp=(*x);
*x=*y;
*y=tmp;
}
//双向冒泡排序
void Dbubblesort(int n,int a[])
{
int i,j;
int left,right;
int count;
left=count=0; //设置左右旗标,标志每次单向冒泡的边界,边界外的数据已经有序
right=n-1;
while(left<right)
{
for(i=left;i<right;i++) //单向向右冒泡
{
if(a[i]>a[i+1])
{
SWAP(a[i],a[i+1]);
count=i;
}
}
right=count;
for(j=right;j>left;j--) //单向向左冒泡
{
if(a[j]<a[j-1])
{
SWAP(a[j],a[j-1]);
count=j;
}
}
left=count;
//不同于传统冒泡程序,它没有设置flag,但其实right,left起到了flag的作用。所有数有序时,正好left=right,因为两者代表可排序边界
}
}
//冒泡排序
void bubblesort(int n,int a[])
{
int i,j;
int flag;
for(i=0;i<n;i++)
{
flag=1;
for(j=0;j<n-1-i;j++)
{
if(a[j]>a[j+1])
{
myswap(&a[j],&a[j+1]);
flag=0;
}
}
if(flag)
break;
}
}
int main(int argc,char * argv[])
{
int num[10]={45,9,77,81,13,28,18,19,77,11};
int n;
scanf("%n",&n);
if(1==n)
bubblesort(10,num);
else
Dbubblesort(10,num);
return 0;
}
2.直接插入排序(Insertion Sort)
基本思想:
每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。
设数组为a[0…n-1]
1 ) 初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1
2 ) 将a[i]并入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。
3 ) i++并重复第二步直到i==n-1。排序完成。
代码实现
void Insertsort1(int a[], int n)
{
int i, j, k;
for (i = 1; i < n; i++)
{
//为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置
for (j = i - 1; j >= 0; j--)
if (a[j] < a[i])
break;
//如找到了一个合适的位置
if (j != i - 1)
{
//将比a[i]大的数据向后移
int temp = a[i];
for (k = i - 1; k > j; k--)
a[k + 1] = a[k];
//将a[i]放到正确位置上
a[k + 1] = temp;
}
}
}
这样的代码太长了,不够清晰。现在进行一下改写,将搜索和数据后移这二个步骤合并。即每次a[i]先和前面一个数据a[i-1]比较,如果a[i] > a[i-1]说明a[0…i]也是有序的,无须调整。否则就令j=i-1,temp=a[i]。然后一边将数据a[j]向后移动一边向前搜索,当有数据a[j]
代码改进
void Insertsort2(int a[], int n)
{
int i, j;
for (i = 1; i < n; i++)
if (a[i] < a[i - 1])
{
int temp = a[i];
for (j = i - 1; j >= 0 && a[j] > temp; j--)
a[j + 1] = a[j];
a[j + 1] = temp;
}
}
3.直接选择排序
直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,所不同的是直接播放排序是将无序区的第一个元素直接插入到有序区以形成一个更大的有序区,而直接选择排序是从无序区选一个最大的元素直接放到有序区的最后。
设数组为a[0…n-1]。
1 ) 初始时,数组全为无序区为a[0..n-1]。令i=0
2 ) 在无序区a[i…n-1]中选取一个最小的元素,将其与a[i]交换。交换之后a[0…i]就形成了一个有序区。
3 ) i++并重复第二步直到i==n-1。排序完成。
代码实现
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
int tmp;
#define SWAP(a,b) tmp=a,a=b,b=tmp
#define N 10
int main(int argc,char * argv[])
{
int num[N];
int i,j,max_index;
srand(time(NULL));
for(i=0;i<N;i++)
{
num[i]=rand();
}
for(i=0;i<N;i++)
{
max_index=i;
for(j=i+1;j<N;j++)
{
if(num[max_index]<num[j])
max_index=j;
}
SWAP(num[max_index],num[i]);
}
for(i=0;i<10;i++)
printf("%d ",num[i]);
return 0;
}
4.快速排序(挖坑+分治)
快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用.
该方法的基本思想是:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
代码实现
#include "stdio.h"
void mysort(int count,int i,int a[]) //数组挪位
{
int k;
int tmp;
tmp=a[i];
for(k=i;i>count;i--)
{
a[i]=a[i-1];
}
a[count]=tmp;
}
void quicksort(int l,int r,int a[])
{
int i,count;
int tmp;
count=l;
if(l>=r)
return;
else
{
for(i=l+1;i<=r;i++)
{
if(a[count]>a[i])
{
mysort(count,i,a);
count++;
}
}
quicksort(l,count-1,a);
quicksort(count+1,r,a);
}
}
int main(int argc,char *argv[])
{
int num[10]={45,9,77,81,13,28,18,19,77,11};
quicksort(0,9,num);
return 0;
}
上网仔细看了看,发现这并不是典型的快排。理解上有偏差。
代码实现
#include "myfunc.h"
void quick_sort(int low,int high,int *s)
{
int pivot;
if(low<high)
{
pivot=Partition(low,high,s); //找出枢轴点,将数组一分为二,分治。
quick_sort(low,pivot-1,s);
quick_sort(pivot+1,high,s);
}
}
Partition函数实现寻找枢轴点的功能,将数组一分为二,下面附上具体的实现
#include "myfunc.h"
int Partition(int low,int high,int *s)
{
int pivotkey;
pivotkey=s[low]; //将基准数取出,数组该处挖坑
while(low<high) //从数组的两侧交替向中间(不是基准点,也不是枢轴点!!!)扫描
{
while(low<high&&pivotkey<=s[high]) //从左向右扫描,发现比基准数小的点则将其填到前次的坑中,数组中该点的原来位置形成新的坑。
high--;
s[low]=s[high];
while(low<high&&pivotkey>=s[low]) //从右向左扫描,发现比基准数小的点则将其填到前次的坑中,数组中该点的原来位置形成新的坑。
low++;
s[high]=s[low];
}
s[low]=pivotkey; //填坑
return low;
}
5.希尔排序
希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
基本思想:
先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
–
以n=10的一个数组49, 38, 65, 97, 26, 13, 27, 49, 55, 4为例
第一次 gap = 10 / 2 = 5
1A,1B,2A,2B等为分组标记,数字相同的表示在同一组,大写字母表示是该组的第几个元素, 每次对同一组的数据进行直接插入排序。即分成了五组(49, 13) (38, 27) (65, 49) (97, 55) (26, 4)这样每组排序后就变成了(13, 49) (27, 38) (49, 65) (55, 97) (4, 26),下同。
第二次 gap = 5 / 2 = 2
第三次 gap = 2 / 2 = 1
第四次 gap = 1 / 2 = 0 排序完成得到数组:
代码实现
#include "myfunc.h"
void shell_sort(int n,int a[])
{
int i,j,k;
int gap;
int tmp;
for(gap=N/2;gap>0;gap/=2) //设置步长
{
for(i=0;i<gap;i++) //组数
{
for(j=i+gap;j<n;j+=gap) //对单个数组直接插入排序
{
if(a[j]<a[j-gap])
{
tmp=a[j];
for(k=j-gap;a[k]>tmp&&k>=0;k-=gap)
a[k+gap]=a[k];
a[k+gap]=tmp;
}
}
}
}
}
6.堆排序
代码实现
#include "myfunc.h"
void heap_sort(int a[],int size)
{
int i;
for(i=size/2;i>=1;i--) //通过不断调整构造大顶堆,初始化大顶堆。
heap_adjust(a,i-1,size);
for(i=size;i>0;i--) //堆排
{
swap(&a[i-1],&a[0]); //交换根节点和末尾元素
heap_adjust(a,0,i-1); //重新调整为大顶堆
}
}
从代码中可以看出,整个排序分为两个循环,第一个循环要完成的就是将现在的待排序序列构建成一个大顶堆。第二个循环要完成的就是逐步将每个最大值的根结点与末尾元素交换,并且再调整其成为大顶堆。
#include "myfunc.h"
void heap_adjust(int a[],int j,int n) //第k个元素到第n个元素
{
int i;
int tmp;
tmp=a[j];
for(i=2*j+1;i<n;i=i*2+1)
{
if(i+1<n&&a[i]<a[i+1])//注意在满足i<n时要满足i+1<n。
i++;
if(a[i]<tmp)
break;
a[j]=a[i];
j=i;
}
a[j]=tmp;
}
7.归并排序
代码实现
#include "myfunc.h"
void merge_sort(int a[],int n)
{
int *p=(int *)malloc(n*sizeof(int)); //分配动态数组用来存放排放有序后的数组,vs不支持变长数组,尝试动态分配数组。
if(p==NULL)
return;
Merge_sort(a,p,0,n-1);
free(p);
}
#include "myfunc.h"
void Merge_sort(int s[],int tmp[],int first,int last)
{
int mid;
if(first==last)
return ;
mid=(last-first)/2+first;
//(first+last)/2的话可能溢出,如果两者都很大,再相加。
Merge_sort(s,tmp,first,mid); //左边有序
Merge_sort(s,tmp,mid+1,last); //右边有序
merge(s,tmp,first,mid,last); //合并,归并
}
#include "myfunc.h"
void merge(int s[],int tmp[],int first,int mid,int last)
{
int i,j,k;
for(k=0,i=first,j=mid+1;i<=mid&&j<=last;) //两个有序数组合并的常规方法
{
if(s[i]<s[j])
tmp[k++]=s[i++];
else
tmp[k++]=s[j++];
}
while(i<=mid)
tmp[k++]=s[i++];
while(j<=last)
tmp[k++]=s[j++];
for(i=0;i<k;i++)
s[i+first]=tmp[i];
}
8. 计数排序
说明:计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
特征:
当输入的元素是n个0到k之间的整数时,它的运行时间是Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
–
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有10个年龄不同的人,统计出有8个人的年龄比A小,那A的年龄就排在第9位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去1的原因。算法的步骤如下:
找出待排序的数组中最大和最小的元素
统计数组中每个值为i的元素出现的次数,存入数组C的第i项
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
反向填充目标数组:将每个元素i放在新数组的第C[i]项,每放一个元素就将C[i]减去1
**计数排序**
最差时间复杂度 O(n + k)
最优时间复杂度 O(n + k)
平均时间复杂度 O(n + k)
最差空间复杂度 O(n + k)
代码实现
#include "myfunc.h"
void count_sort(int num[],int sort_num[],int maxsize,int len)
{
int * count_num;
int i;
count_num=(int *)malloc(sizeof(int)*(maxsize+1));
memset(count_num,0,(maxsize+1)*sizeof(int)); //count_num 全部置零,用来统计0到maxsize出现的次数
for(i=0;i<len;i++) //统计次数
{
count_num[num[i]]++;
}
for(i=1;i<maxsize+1;i++)
count_num[i]+=count_num[i-1]; //count_num[i]存储的是i出现的次数,该代码块表示有多少个数小于等于i
for(i=len;i>0;i--)
{
sort_num[count_num[num[i-1]]-1]=num[i-1]; //初始,num[]中最后一个数,有count_num[num[i-1]]个数小于等于它(num[i-1]),所以有序时它的位置在第count_num[num[i-1]]个,下标为count_num[num[i-1]]-1.
count_num[num[i-1]]--; //找到了它的下标count_num[num[i-1]]-1,那么就将num[i-1]赋给它,它就到达了有序的位置。此后,count_num[num[i-1]]中就少了一个num[i-1].
}
free(count_num);
}
#include "myfunc.h"
int main(int argc,char * argv[])
{
int num[N]={0};
int sort_num[N]={0};
int i;
srand(time(NULL));
for(i=0;i<N;i++)
{
num[i]=rand()%(MAX_NUM+1); //数组最小元素为0,最大元素为MAX_NUM,一共N个元素
}
count_sort(num,sort_num,MAX_NUM,N);
system("pause");
}
8.基数排序
基数排序(Radix sort )是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字和日期)和特定格式的浮点数,所以基数排序也不是只能用于整数。
实现:将所有待比较数值(正整数)统一为同样的位数长度,位数较短的数前面补零。然后,从最低位开始,依次进行依次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
MSD 比较复杂,分配收集后仍需排序,所以下面用LSD实现。
代码实现
#include "myfunc.h"
int get_time(int a[],int maxsize)
{
int i;
int max_time,count;
int tmp;
max_time=-1;
for(i=0;i<maxsize;i++)
{
count=0;
tmp=a[i];
while(tmp)
{
tmp=tmp/10;
count++;
}
if(max_time<count)
max_time=count;
}
return max_time;
}
void radix_sort(int a[],int maxsize)
{
int time,i,j,k,m;
int ratio,index;
int bit_val;
int * bucket[BASE]; //BASE底数,这里以10为底
ratio=1;
time=get_time(a,maxsize);
for(i=0;i<BASE;i++)
{
bucket[i]=(int *)malloc(sizeof(int)*(maxsize+1));
bucket[i][0]=0;
}
for(m=0;m<time;m++)
{
for(j=0;j<maxsize;j++) //分配
{
bit_val=(a[j]/ratio)%10;
index=++bucket[bit_val][0];
bucket[bit_val][index]=a[j];
}
for(j=0,k=0;j<BASE;j++) //收集
{
for( i=1;i <= bucket[j][0];i++)
{
a[k++]=bucket[j][i];
}
bucket[j][0]=0; //置零
}
ratio*=10;
}
}
如何获得分配收集次数
int get_time(int a[],int maxsize)
{
int i;
int max_time,count;
int tmp;
max_time=-1;
for(i=0;i<maxsize;i++)
{
count=0;
tmp=a[i];
while(tmp)
{
tmp=tmp/10;
count++;
}
if(max_time<count)
max_time=count;
}
return max_time;
}