常见排序算法总结及C语言实现
一直没有好好的扎扎实实的算法的基础,要找工作了,临时抱下佛脚,顺便把学的东西整理下,以应对比较健忘的大脑。。。
废话不说,直接主题,其实整理这个,借鉴了不少这个blog,http://www.cppblog.com/shongbee2/archive/2009/04/25/81058.html 在此再次感谢这个博主,但愿有一天,自己也能请博主喝杯咖啡 哈哈~
先从最熟悉的冒泡排序开始吧:
冒泡排序(BubbleSort)
冒泡排序也是我接触到的最早的排序,其基本思想是把数组中每个元素都看成是有质量的气泡,每个气泡的质量就是数组对应位置的元素的大小。排序的过程就是不停的把质量较轻(元素较小)的气泡上浮的过程。第一次循环最小的元素起泡到第一个位置,第二次循环,次小的元素被放到正确的位置,这样n-1次循环之后,数组就是有序的了。值得说明的是,冒泡排序是原地排序的过程,即不需要额外的内存空间,时间复杂度为O(N^2),且是稳定的排序。
源代码:
int BubbleSort(int *pData,int len) {//冒泡排序,pData 是待排序的数组,len是数组的长度 bool isOk=false;//这是一个优化,标志数组是否有序 for(int i=0;i<len-1&&!isOk;i++)//外层循环控制起泡循环的次数 { isOk=true;//初始化为有序的 for(int j=len-1;j>i;j--)//从最后一个元素开始,一次循环的结果是使第i小的元素出现在第i个位置上 { if(pData[j]<pData[j-1])//如果第j-1个元素比第j个元素小,则交换. { int temp=pData[j]; pData[j]=pData[j-1]; pData[j-1]=temp; isOk =false;//有交换发生,说明数组是无序的,即排序没有完成. } } } return 1; }
快速排序(QuickSort)
快速排序和冒泡排序一样,都是基于交换的排序。但是值得注意的是快速排序用到了分治的思想。
利用分治的思想可以这样描述快排:对于待排序的无序的数组pData[1,2,...n],分解:任选其中一个元素做为键值进行划分,找到划分位置i,使得i左边的元素都小于等于找到的键值,i右边的元素都大于等于键值。 求解:递归的对i左边的元素和i右边的元素进行快速排序;组合:由于求解过程中递归步骤结束后,左右两部分都已经有序,这样对于排序来说,组合的过程就不需要操作了。可以看成是空操作。同样说明的是快速排序也是原地排序,且时间复杂度比冒泡排序低最好的情况是O(nlog(n)),最坏情况是O(N^2),是不稳定的排序。
源代码
int Partition(int *pData,int begining,int end) {//划分的过程 pData是待排序的无序数组,begining是开始位置,end是结束位置,函数返回以起始位置元素做为键值的划分的位置 int i = end,j;//i初始化为最后一个位置+1 int nd=pData[begining-1];//选定起始位置为键值 int tmp; for (j=end-1;j>begining-1;j--)//j从最后一个元素开始,到第二个元素结束 { if(pData[j]>=nd)//如果pData[j]比选的键值大,那么i-1,同时交换pData[j]和pData[i],这样保证i右边的元素永远比键值大,同时i-1位置元素比键值小 { --i; tmp=pData[i]; pData[i]=pData[j]; pData[j]=tmp; } } --i;//循环结束后i左边的元素都比键值小,而且i右边的元素(包括i位置元素)都比键值大,所以i值减一,同时和键值交换 pData[begining-1]=pData[i]; pData[i]=nd; return i+1;//考虑到数组下标是从零开始的 } int QuickSortRecursion(int *pData,int begining,int end) { { if (begining>=end)//终止条件 return 1; } int i=Partition(pData,begining,end);//划分 QuickSortRecursion(pData,begining,i);//递归的排序左半部分 QuickSortRecursion(pData,i+1,end);//递归的排序右半部分 return 1; } int QuickSort(int *pData,int len) { QuickSortRecursion(pData,1,len); return 1; }
选择排序(SelectSort)
选择排序的基本思想和冒泡排序有一点像,都是依次选择最小元素,次小元素…… ,但是和冒泡排序不同的是,冒泡排序发现小的就进行互换,但是选择排序是直接把小的放到应该出现的位置。选择排序的过程如下:
n个元素的数组可以经过n-1趟选择排序后得到有序的结果:初始状态:无序区pData[1,2,...n],有序区间为空。第一趟排序:在无序区pData[1,2,...n]中选择最小的元素pData[k],将他与第一个元素pData[1]交换,使得pData[1...1]和pData[2,3,...n],分别表示有序区和无序区。每一次循环,有序区的长度加一,同时无序区长度减一。这样n个元素的序列可以通过n-1趟排序得到有序的序列。选择排序的时间复杂度为O(N^2),是原地排序,且不稳定排序。
int SelectSort(int *pData,int num) {//选择排序,pData是待排序数组,num是数组的长度 for (int i=0;i<num-1;i++)//控制循环次数 num-1次循环即可得到有序序列 { int index=i;//初始化最小元素的下标 for(int j=i+1;j<num;j++)//从i+1个位置开始 { if(pData[j]<pData[index])//如果有比当前最小值更小的元素,更新指向最小元素的索引 index=j; }//循环结束后,index指向的是无序区最小元素的索引 if(index!=i)//如果第i个元素不是当前循环最小元素,则交换 { int temp=pData[i]; pData[i]=pData[index]; pData[index]=temp; } } return 1; }
插入排序(InsertionSort)
1、基本思想
假设待排序的记录存放在数组R[1..n]中。初始时,R[1]自成1个有序区,无序区为R[2..n]。从i=2起直至i=n为止,依次将R[i]插入当前的有序区R[1..i-1]中,生成含n个记录的有序区。
2、第i-1趟直接插入排序:
通常将一个记录R[i](i=2,3,…,n-1)插入到当前的有序区,使得插入后仍保证该区间里的记录是按关键字有序的操作称第i-1趟直接插入排序。
排序过程的某一中间时刻,R被划分成两个子区间R[1..i-1](已排好序的有序区)和R[i..n](当前未排序的部分,可称无序区)。
直接插入排序的基本操作是将当前无序区的第1个记录R[i]插人到有序区R[1..i-1]中适当的位置上,使R[1..i]变为新的有序区。因为这种方法每次使有序区增加1个记录,通常称增量法。
插入排序与打扑克时整理手上的牌非常类似。摸来的第1张牌无须整理,此后每次从桌上的牌(无序区)中摸最上面的1张并插入左手的牌(有序区)中正确的位置上。为了找到这个正确的位置,须自左向右(或自右向左)将摸来的牌与左手中已有的牌逐一比较。
插入排序的时间复杂度是O(N^2),是原地排序,且是稳定的排序。
int InsertionSort(int *pData,int num) {//插入排序 int i,j,key; for(j=1;j<num;j++)//循环次数 { key=pData[j];//把第j个元素插入到前j-1个有序的序列中 i=j-1;//循环的初始 while(i>=0&&pData[i]>key) { pData[i+1]=pData[i];//从后往前遍历,若比key值大,则元素后移(为后面的插入做准备) i--; }//找到第一个比key值小的位置 pData[i+1]=key;//key插入到第i一个比key小的元素的位置之后 } return 1; }插入排序的一个变种是希尔排序,用到了分治的思想,其实这个我也不是很懂,附上源代码,供大家参考
int SortGroup(int *pnData,int nlen,int nBegin,int nStep) { for(int i=nBegin+nStep;i<nlen;i+=nStep) { for (int j=nBegin;j<i;j+=nStep) { if(pnData[i]<pnData[j]) { int nTemp=pnData[i]; for(int k=i;k>j;k-=nStep) { pnData[k]=pnData[k-nStep]; } pnData[j]=nStep; } } } return 1; } int ShellSort(int *pnData,int nLen) { for(int nStep=nLen/2;nStep>0;nStep/=2) { for(int i=0;i<nStep;++i) { SortGroup(pnData,nLen,i,nStep); } } return 1; }
归并排序(MergeSort)
归并排序的主要思想是分治
1、算法基本思路
设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。
(1)合并过程
合并过程中,设置i,j和p三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较R[i]和R[j]的关键字,取关键字较小的记录复制到R1[p]中,然后将被复制记录的指针i或j加1,以及指向复制位置的指针p加1。
重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),此时将另一非空的子文件中剩余记录依次复制到R1中即可。
(2)动态申请R1
实现时,R1是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。
归并排序的时间复杂度是O(nlog(n))的,但是是需要额外的空间的O(N),同时是稳定的排序
int Merge(int *pData,int nP,int nM,int nR) {//归并pData中nP到nM和nM到nR的元素 int nLen1=nM-nP; int nLen2=nR-nM; int *pnD1=new int[nLen1]; int *pnD2=new int[nLen2]; int i,j; for(i=0;i<nLen1;++i) { pnD1[i]=pData[nP+i];//复制pData中nP到nM的元素 } for (j=0;j<nLen2;j++) { pnD2[j]=pData[nM+j];//复制pData中nM到nR的元素 } i=j=0; while(i<nLen1&&j<nLen2)//归并过程 { if (pnD1[i]<pnD2[j]) { pData[nP]=pnD1[i]; ++i; } else { pData[nP]=pnD2[j]; j++; } nP++; } if (i<nLen1)//如果j先结束,复制i剩下的元素 { while(nP<nR) { pData[nP++]=pnD1[i++]; } } else//复制j剩下的元素 { while(nP<nR) { pData[nP++]=pnD2[j++]; } } delete pnD1; delete pnD2; return 1; } int MergeRecursion(int *pData,int nBegin,int nEnd) {//递归调用 if (nBegin>=nEnd-1) { return 0; } int nMid=(nBegin+nEnd)/2;//中间位置 MergeRecursion(pData,nBegin,nMid);//递归求解前半部分 MergeRecursion(pData,nMid,nEnd);//递归求解后banbuf Merge(pData,nBegin,nMid,nEnd);//组合,归并求解 return 1; } int MergeSort(int *pData,int Len) { return MergeRecursion(pData,0,Len); }堆排序(HeapSort)
堆的定义
n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):(1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤(n/2) )满足(1)的叫做最小堆,满足(2)的称作最大堆。
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
(1)用大根堆排序的基本思想
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
③ 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
……
直到无序区只有一个元素为止。
(2)大根堆排序算法的基本操作:
① 初始化操作:将R[1..n]构造为初始堆;
② 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
注意:
①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。
堆排序是原地进行的,时间复杂度为O(nlog(n)),是不稳定的排序。
void swap(int &i,int &j) {//交换两个元素的值,纯属装X,从别地看到的不用第三个变量就能互换的方法 if (i!=j) { i^=j; j^=i; i^=j; } } int MaxHeapIfy(int *pData,int nPose,int nLen) {//保证以nPose 为根的子树为最大堆,算法的前提是以Left[nPose]和Right[nPose]为根的子树都是最大堆 int nMax=-1;//最大位置的索引 int lChild=2*nPose+1;//考虑到数组下标从零开始,左子树的根的位置 int rChild=2*nPose+2;//右子树根的位置 if(lChild<nLen&&pData[lChild]>pData[nPose]) nMax=lChild;//如果左孩子比根大,违反最大堆的定义,设定nMax为左孩子的索引 else nMax=nPose; if (rChild<nLen&&pData[rChild]>pData[nMax]) nMax=rChild;//同理 若右孩子比根大,设定nMax为右孩子的索引 if(nMax!=nPose)//如果nPose不是最大 { swap(pData[nPose],pData[nMax]);//交换nPose和nMax的值 MaxHeapIfy(pData,nMax,nLen);//交换后,可能以你年Max位置为根的子树不满足最大堆条件,递归的调用 } else { return 1; } return 1; } int BuildMaxHeap(int *pData,int nLen) {//建立最大堆 for(int i=nLen/2;i>=0;i--)//由完全二叉树的性质知,后n/2的元素都是叶子节点,自成一个最大堆 MaxHeapIfy(pData,i,nLen);//自底向上的建堆 return 1; } int HeapSort(int *pData,int nLen) { BuildMaxHeap(pData,nLen);//建堆 while(nLen>=1) { swap(pData[0],pData[nLen-1]);//当堆建好后,第一个元素肯定是最大的元素,交换第一个元素与最后一个元素,这样最大的元素就放在了最后 --nLen;//nLen-1.对前nLen个元素重复上述过程 MaxHeapIfy(pData,0,nLen);//交换后,可能第一个元素不满足最大堆条件,调整 } return 1; }