1.快速排序
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
步骤为:
1.从数列中挑出一个元素,称为 "基准"(pivot), 2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分割之后,该基准是它的最后位置。这个称为分割(partition)操作。 3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
最坏时间复杂度: O(n^2),平均时间复杂度:O(nlogn) 快速排序是不稳定的排序算法。
#include <iostream> using namespace std; //快速排序算法中的一趟划分函数的实现,数组a[p..r],就地排序 int partition(int a[],int p,int r,int len) { int x=a[r]; int i=p-1; for(int j=p;j<=r-1;j++) { if(a[j]<=x){ i++; swap(a[i],a[j]); } } swap(a[i+1],a[r]); //打印每一次划分,以及每个划分的中间数。问题:如何获得数组长度,后来我选择传值 for(int j=p;j<=r;j++){ cout<<a[j]<<' '; } cout<<",返回的中间分隔数下标"<<i+1<<":"<<a[i+1]<<endl; for(int j=0;j<=len-1;j++){ cout<<a[j]<<' '; } cout<<endl; cout<<"*************************************"<<endl; return i+1; } //交换两个数的函数 void swap(int &p,int &q) { int t; t=p; p=q; q=t; } //递归调用的快排算法 void quickSort(int a[],int p,int r,int len) { cout<<"待排子序列,边界下标:"<<p<<" "<<r<<endl; int q; if(p<r) { q=partition(a,p,r,len); quickSort(a,p,q-1,len); quickSort(a,q+1,r,len); } } int main(){ int len,a[100]; cout<<"输入要排序的数组个数?"<<endl; cin>>len; cout<<"请输入要排序的数组元素?"<<endl; for(int i=0;i<len;i++){ cin>>a[i]; } quickSort(a,0,len-1,len); for(int i=0;i<len;i++){ cout<<a[i]<<' '; } return 0; }
2.堆排序
堆积排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法。堆积树是一个近似完整二叉树的结构,并同时满足堆积属性:即子结点的键值或索引总是小于(或者大于)它的父结点。
在堆积树的数据结构中,堆积树中的最大值总是位于根节点。堆积树中定义以下几种操作: 1.最大堆积调整(max_heapify):将堆积树的末端子结点作调整,使得子结点永远小于父结点 2.建立最大堆积(build_max_heap):将堆积树所有数据重新排序 3.堆积排序(heap_sort):移除位在第一个数据的根结点,并做最大堆积调整的递归运算
最坏时间复杂度 O(nlogn) 堆排序是不稳定的排序算法
#include <iostream> using namespace std; int A[100]; int heap_size; int length; //给定某个节点,返回其父节点,左儿子右儿子节点下标 int heapParent(int i) { return i/2;//int类型本身就是向下取整 } int heapLeft(int i) { return 2*i; } int heapRight(int i) { return 2*i+1; } //交换两个数的函数 void swap(int &p,int &q) { int t; t=p; p=q; q=t; } //调整为最大堆的函数,使以i为根的子树成为最大堆 void maxHeap(int A[],int i){ int largest; int l=heapLeft(i); int r=heapRight(i); if(l<=heap_size &&A[l-1]>A[i-1]) largest=l; else largest=i; if(r<=heap_size &&A[r-1]>A[largest-1]) largest=r; if(largest!=i) { swap(A[i-1],A[largest-1]); maxHeap(A,largest); } } //建堆 void heapBuild(int A[]){ for(int i=length/2;i>=1;i--) { maxHeap(A,i); } } //堆排序 void heapSort(int A[]) { heapBuild(A); cout<<"基于原数组建立的大根堆:"<<endl; for(int i=0;i<length;i++){ cout<<A[i]<<' '; } cout<<endl; for(int i=length;i>=2;i--) { swap(A[1-1],A[i-1]); heap_size--; maxHeap(A,1); cout<<"第"<<length-i+1<<"个数置换到队尾:"<<endl; for(int i=0;i<length;i++){ cout<<A[i]<<' '; } cout<<endl; } } //主函数 int main(){ cout<<"输入要排序的数组个数?"<<endl; cin>>length; cout<<"请输入要排序的数组元素?"<<endl; for(int i=0;i<length;i++){ cin>>A[i]; } heap_size=length; heapSort(A); for(int i=0;i<length;i++){ cout<<A[i]<<' '; } return 0; }
3 插入排序
插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
最坏时间复杂度 O(n^2)
插入排序是稳定的排序算法。
#include <iostream> using namespace std; void insertSort(int A[],int length) { for(int j=2;j<=length;j++) { int key=A[j-1]; int i=j-1; while(i>0&&A[i-1]>key){ A[i+1-1]=A[i-1]; i=i-1; } A[i+1-1]=key; //一次循环后,注意,此处我为了跟书上算法一致,考虑到数组下标从零开始,此处涉及数组下标的都-1. cout<<"第"<<j-1<<"趟循环后:"; for(int i=1;i<=length;i++){ cout<<A[i-1]<<' '; } cout<<endl; } } int main(){ int len,a[100]; cout<<"输入要排序的数组个数?"<<endl; cin>>len; cout<<"请输入要排序的数组元素?"<<endl; for(int i=0;i<len;i++){ cin>>a[i]; } insertSort(a,len); cout<<"最终排好的结果:"; for(int i=0;i<len;i++){ cout<<a[i]<<' '; } return 0; }
4 归并排序
归并排序(Merge sort)算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
归并算法的基本步骤如下所示: 1)把0~length-1的数组分成左数组和右数组 2)对左数组和右数组进行迭代排序 3)将左数组和右数组进行合并,那么生成的整个数组就是有序的数据数组
最坏时间复杂度 O(nlogn)
合并排序是稳定的排序算法
#include <iostream> #include <limits> using namespace std; int L[100];//做归并是多用的空间 int R[100]; //说明:此处的参数序号都是从1开始,便于理解,但是记住数组的特殊,默认从零开始,所以针对数字下边我都有个-1操作,这是唯一不同于课本上算法描述。 void merge(int A[],int p,int q,int r){ cout<<"中间数:"<<p<<","<<q<<","<<r<<endl; int n1=q-p+1; int n2=r-q; // for(int i=1;i<=n1;i++) { L[i-1]=A[p+i-1-1]; } for(int j=1;j<=n2;j++) { R[j-1]=A[q+j-1]; } L[n1+1-1]=INT_MAX ; R[n2+1-1]=INT_MAX ; int i=1; int j=1; for (int k=p;k<=r;k++) { if(L[i-1]<=R[j-1]){ A[k-1]=L[i-1]; i++; }else { A[k-1]=R[j-1]; j++; } } for(int i=p;i<=r;i++){ cout<<A[i-1]<<","; } cout<<endl; } // void mergeSort(int A[],int p,int r){ if(p<r) { int q=(p+r)/2; mergeSort(A,p,q); mergeSort(A,q+1,r); merge(A,p,q,r); } } int main(){ int len,a[100]; cout<<"输入要排序的数组个数?"<<endl; cin>>len; cout<<"请输入要排序的数组元素?"<<endl; for(int i=0;i<len;i++){ cin>>a[i]; } mergeSort(a,1,len); for(int i=0;i<len;i++){ cout<<a[i]<<' '; } return 0; }
5.计数排序
计数排序是一个非基于比较的线性时间排序算法。它对输入的数据有附加的限制条件: 1、输入的线性表的元素属于有限偏序集S; 2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。 在这两个条件下,计数排序的复杂性为O(n+k)。
计数排序算法的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改
#include <iostream> using namespace std; int B[100]={0}; //输入的带排序数组是A,length为数组A的长度,另外还需两个数组,存放排序结果的B数组,以及提供临时存储的数组C,其长度为k void countSort(int A[],int length){ int C[100]; for(int i=0;i<=length;i++){//注意数组C赋值的长度必须比A中最大元素长,理论上是介于0—k,都先初始化为0,其中K是带排序数组中最大元素。如输入:1,4,2,4,则初始化C[0..4] C[i]=0; } for (int j=1;j<=length;j++) { C[A[j-1]]=C[A[j-1]]+1;//C[i]的值等于A中元素i的个数,如,C[0]=2,表示带排序数组A中有2个0 } for (int i=1;i<=length;i++) { C[i]=C[i]+C[i-1];//C[i]代表小于或等于i的元素个数。 } for(int j=length;j>=1;j--){ B[C[A[j-1]]-1]=A[j-1]; C[A[j-1]]=C[A[j-1]]-1; //下面为测试代码 cout<<"第"<<length-j+1<<"次循环结果:"; for(int i=0;i<length;i++){ cout<<B[i]<<' '; } cout<<endl; } } int main(){ int len,a[100]; cout<<"输入要排序的数组个数?"<<endl; cin>>len; cout<<"请输入要排序的数组元素?"<<endl; for(int i=0;i<len;i++){ cin>>a[i]; } countSort(a,len); for(int i=0;i<len;i++){ cout<<B[i]<<' '; } return 0; }
6.基数排序
计数排序的缺点很明显,需要额外的空间C来作为计数数组,虽然时间复杂度为O(n+k),但当输入序列里元素取值很大的时侯,如k=O(n2),时,此时时间复杂度已经达到n2数量级了,空间的消耗也是让人无法承受的。这里介绍一种另一种线性排序算法——基数排序,可以应对数值很大的情况。
基数排序,即一个数位一个数位地进行排序,平常生活中我们经常使用的一种算法思想:如要对一个日期进行排序,日期中由年、月、日组成的,对于这个问题,我们经常使用的是先比较年份,如果相同再比较月份,如果还相同就比较日。
同理,我们比较一组数,也可以采取这种思想。例如我们使用这种思想对下面四个数进行排序:123、312、245、531,第一次按百位排序:123、245、312、531;第二次按十位排序:312、123、531、245;第三次按个位数排序:531、312、123、245。咦?为什么最后排出来的结果并不是预期的那样?原因是我们从高位开始排序,已经差不多整体有序之后,再到低位时,又全部被打乱了。如果我们之后这样做就不会乱了:高位相同的数,再将它们的低位进行排序….不过这个实现一起比较困难一些。(上面排日期时,已经默认是年相同的,再排月日,大范围的年顺序不再变化了。)
这里,我们换成从最低有效位到最高有效位进行排序,那么还是上面那个例子:
个位 => 十位 => 百位
531 312 123
312 123 245
123 531 312
245 245 531
可以看到结果正确。通俗地讲,之所以先排低位再排高位,是因为越是后排的数位,其对结果次序的影响越大,很显然是高位比低位对数的大小影响大!
7 桶排序
桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
例如要对大小为[1..1000]范围内的n个整数A[1..n]排序,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储(10..20]的整数,……集合B[i]存储((i-1)*10, i*10]的整数,i = 1,2,..100。总共有100个桶。然后对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 然后再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任何排序法都可以。最后依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是O(n+m*n/m*log(n/m))=O(n+nlogn-nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。