转载自:http://blog.csdn.net/pleasecallmewhy/article/details/8466850
5.快速排序:
快速排序的核心思想是分而治之算法。所谓的分而治之,简单来说就是把复杂问题分成几个子问题,然后分别解决小问题,最后再将解组合起来,得到原问题的解。
那么分而治之如何应用到排序算法中呢?
在快速排序中,n个元素被分成了了三段。左端left,右端right,和中段middle。中段仅包含一个元素,作为基准元素,左段的各元素都小于等于中段元素,右段元素都大于等于中段元素。middle元素被称为支点。
基本的操作流程大致如下:在待排序的n个元素中任意选择一个作为基准元素(通常取第一个),把该元素放入最终的位置上,数据序列被此元素划分成两部分,所有关键字比该元素关键字小的元素放置在前一部分,所有比它大的元素放在后一部分,这个过程称为一趟快速排序。对分成的两部分重复上述过程,直到每部分只有一个元素或空为止。
快排的源码如下:
- #include <iostream>
- using namespace std;
- template <class T>
- void show(T arr,int n){
- for(int i =0;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- void QuickSort( int a[], int l, int r )
- {
- show(a,9);
- if (l>=r) return;
- int i, j, temp;
- temp = a[l];
- i = l; j = r;
- while (i<j) {
- while(i<j&&temp<a[j])
- j--;
- a[i] = a[j];
- while(i<j&&temp>a[i])
- i++;
- a[j] = a[i];
- }
- a[i] = temp;
- QuickSort( a, l, i-1 );
- QuickSort( a, i+1, r );
- }
- void main()
- {
- int inputNumber[]={2,7,5,9,1,4,6,3,8};
- int count = 9;
- cout<<"原始数组:"<<endl;
- show(inputNumber,count);
- cout<<"排序过程:"<<endl;
- QuickSort(inputNumber,0,count);
- cout<<"排序结果:"<<endl;
- show(inputNumber,count);
- }
程序运行结果的截图:
下面来谈一下快排的复杂度问题。
快速排序的时间性能取决于快速排序递归的深度,可以用递归树来描述递归算法的执行情况。
比如{50,10,90,30, 70,40,80,60,20}在快速排序过程中的递归过程。由于我们的第一个关键字是50,正好是待排序的序列的中间值,因此递归树是平衡的,此时性能也比较好。
在最优情况下,Partition每次都划分得很均匀,如果排序n个关键字,其递归树的深度就为.log2n.+1(.x.表示不大于x的最大整数),即仅需递归log2n次,需要时间为T(n)的话,第一次Partiation应该是需要对整个数组扫描一遍,做n次比较。然后,获得的枢轴将数组一分为二,那么各自还需要T(n/2)的时间(注意是最好情况,所以平分两半)。于是不断地划分下去,我们就有了下面的不等式推断。
T(n)≤2T(n/2) +n,T(1)=0
T(n)≤2(2T(n/4)+n/2) +n=4T(n/4)+2n
T(n)≤4(2T(n/8)+n/4) +2n=8T(n/8)+3n
……
T(n)≤nT(1)+(log2n)×n= O(nlogn)
也就是说,在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。
在最坏的情况下,待排序的序列为正序或者逆序,每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一棵斜树。此时需要执行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为
最终其时间复杂度为O(n2)。
平均的情况,设枢轴的关键字应该在第k的位置(1≤k≤n),数学归纳法可证明,其数量级为O(nlogn)。
再来看下快排的稳定性:
快速排序有两个方向,左边的i下标一直往右走,当a[i] <=a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j]> a[center_index]。如果i和j都走不动了,i <= j,交换a[i]和a[j],重复上面的过程,直到i>j。交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻。
下面来总结一下快速排序:
原理:不断寻找一个序列的中点,然后对中点左右的序列递归的进行排序,直至全部序列排序完成,使用了分治的思想。
要点:递归思想,分而治之。
6.归并排序:
下面来看一个和快速排序相似的算法,它们都用到了分而治之的思想,但是细节操作却不一样。
归并排序的核心思想是,合并两个已排序的表。两个已排序的表a、b, 另一个表c用来存放结果,第一次取出a表和b表的最顶端元素进行比较,把较小(较大)的取出放到c表中,第二趟,继续取出a,b表中的最顶端元素比较,把较小(较大)的取出放到c表的下一个位置,重复上述步骤,直到a,b表中有一个表的元素已经取完,接着把另一张表的剩余元素按顺序加到c表中,排序结束。
就像是AB两队小孩儿合成一个队C,每次都从AB两个队伍中比较选择个子最矮的小孩放到队伍C里。
归并排序的源码如下:
- #include <iostream>
- using namespace std;
- template <class T>
- void show(T arr,int n){
- for(int i =0;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- /* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */
- void Merge(int SR[],int TR[],int i,int m,int n)
- {
- int j,k,l;
- for(j=m+1,k=i;i<=m && j<=n;k++) /* 将SR中记录由小到大归并入TR */
- {
- if(SR[i]<SR[j])
- TR[k]=SR[i++];
- else
- TR[k]=SR[j++];
- }
- if(i<=m)
- {
- for(l=0;l<=m-i;l++)
- TR[k+l]=SR[i+l]; /* 将剩余的SR[i..m]复制到TR */
- }
- if(j<=n)
- {
- for(l=0;l<=n-j;l++)
- TR[k+l]=SR[j+l]; /* 将剩余的SR[j..n]复制到TR */
- }
- }
- void MSort(int SR[],int TR1[],int s, int t)
- {
- int m;
- int TR2[10];
- if(s==t)
- TR1[s]=SR[s];
- else
- {
- m=(s+t)/2; /* 将SR[s..t]平分为SR[s..m]和SR[m+1..t] */
- MSort(SR,TR2,s,m);/*递归将SR[s..m]归并为有序的TR2[s..m]*/
- MSort(SR,TR2,m+1,t);/*递归将SR[m+1..t]归并为有序TR2[m+1..t]*/
- Merge(TR2,TR1,s,m,t);/*将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t]*/
- }
- show(TR1,9);
- }
- void main()
- {
- int inputNumber[]={2,7,5,9,1,4,6,3,8};
- int inputNumber2[]={0,0,0,0,0,0,0,0,0};
- int count = 9;
- cout<<"原始数组:"<<endl;
- show(inputNumber,10);
- cout<<"排序过程:"<<endl;
- MSort(inputNumber,inputNumber2,0,9);
- cout<<"排序结果:"<<endl;
- show(inputNumber2,9);
- }
代码运行的结果显示:
我们来分析一下归并排序的时间复杂度,一趟归并需要将SR[1]~SR[n]中相邻的长度为h的有序序列进行两两归并。并将结果放到TR1[1]~TR1[n]中,这需要将待排序序列中的所有记录扫描一遍,因此耗费O(n)时间,而由完全二叉树的深度可知,整个归并排序需要进行log2n.次,因此,总的时间复杂度为O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。
由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为log2n的栈空间,因此空间复杂度为O(n+logn)。
另外,对代码进行仔细研究,发现Merge函数中有if (SR[i]<SR[j])语句,这就说明它需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法。
也就是说,归并排序是一种比较占用内存,但却效率高且稳定的算法。
再来看下归并算法的稳定性:
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
下面我们可以来总结一下分而治之实现排序的算法思想:若n为1,算法中止,否则,将这一元素集合分割成两个或更多个子集合,对每个子集合分别排序,然后将排好序的子集集合归并为一个集合。
下面来总结一下归并排序:
原理:将原序列划分为有序的两个序列,然后利用归并算法进行合并,合并之后即为有序序列。
要点:归并、分而治之
7.箱子排序
说明一点,箱排序实用价值不大,在此仅适用于作为基数排序的一个中间步骤,所以有必要介绍一下。
我们先来看一个场景需求:
数据库中存储了学生的姓名年龄和成绩,要求将学生按照成绩排序。如果是前面的几种简单排序,所需要花费的时间均为N方,所以介绍一种更快的排序算法:箱子排序。
简单点说,箱子排序就是把待排序元素分成几类,然后设置若干个箱子,依次扫描待排序的记录,把关键字等于k的记录全都装入到第k个箱子里(分配),然后按序号依次将各非空的箱子首尾连接起来(收集)。
对于实现箱子排序,需要能够:
1.从欲排序链表的首部开始,逐个删除每个节点,并把删除的节点放入合适的箱子中。
2.收集并链接每个箱子里的节点,产生一个排序的链表。
如果输入的链表是Chain类型,那么可以:
1.连续的删除链表的首元素,并将其插入到相应箱子链表首部。
2.逐个删除每个箱子中的元素(从最后一个箱子开始)并将其插入到一个初始为空的链表首部。
下面来总结一下箱子排序:原理:将元素分装到各个有序的箱子中,并且将箱子按照顺序生成新的序列。
要点:先分类,再排序
8.基数排序:
基数排序可以说是对上面箱子排序的扩充。箱子排序存在一个非常大的问题:如果箱子跨度range为次方级,那么箱子排序的箱子数目将会非常多。为了解决这个弊端,我们将箱子排序进行扩充。
基数排序的原理是不直接对元素进行排序,而是先将其分解,然后对每一部分进行排序。
比如对十进制三位数排序。123拆解为1,2,3;456拆解为4,5,6。先比较个位数,排好序后再比较十位数,最后再比较百位数排序,这样便可以得到最后的排序结果。
简而言之就是每次按照每个数字的一位进行排序,先按照最个位,再按照十位,依次类推至最高位。其中每一位的排序必须是稳定排序才能保证算法的正确性。
基数排序的源码:
- #include <iostream>
- using namespace std;
- template <class T>
- void show(T arr,int n){
- for(int i =0;i<n-1;i++){
- cout<<arr[i]<<",";
- }
- cout<<arr[n-1]<<endl;
- }
- int RadixCountSort(int* npIndex, int nMax, int* npData, int nLen){
- //这里就不用说了,计数的排序。不过这里为了是排序稳定
- //在标准的方法上做了小修改。
- int* pnCount = (int*)malloc(sizeof(int)* nMax); //保存计数的个数
- int i = 0;
- for (i = 0; i < nMax; ++i){
- pnCount[i] = 0;
- }
- for (i = 0; i < nLen; ++i){ //初始化计数个数
- ++pnCount[npIndex[i]];
- }
- for (i = 1; i < 10; ++i){ //确定不大于该位置的个数。
- pnCount[i] += pnCount[i - 1];
- }
- int * pnSort = (int*)malloc(sizeof(int) * nLen); //存放零时的排序结果。
- //注意:这里i是从nLen-1到0的顺序排序的,是为了使排序稳定。
- for (i = nLen - 1; i >= 0; --i){
- --pnCount[npIndex[i]];
- pnSort[pnCount[npIndex[i]]] = npData[i];
- }
- for (i = 0; i < nLen; ++i){ //把排序结构输入到返回的数据中。
- npData[i] = pnSort[i];
- }
- free(pnSort); //记得释放资源。
- free(pnCount);
- return 1;
- }
- //基数排序
- int RadixSort(int* nPData, int nLen){
- //申请存放基数的空间
- int* nDataRadix=(int*)malloc(sizeof(int) * nLen);
- int nRadixBase = 1; //初始化倍数基数为1
- int nIsOk = 0; //设置完成排序为0
- //循环,知道排序完成
- while (!nIsOk){
- nIsOk = 1;
- nRadixBase *= 10;
- int i = 0;
- for (i = 0; i < nLen; ++i){
- nDataRadix[i] = nPData[i] % nRadixBase;
- nDataRadix[i] /= nRadixBase / 10;
- if (nDataRadix[i] > 0){
- nIsOk = 0;
- }
- show(nPData,nLen);
- }
- if (nIsOk){ //如果所有的基数都为0,认为排序完成,就是已经判断到最高位了。
- break;
- }
- RadixCountSort(nDataRadix, 10, nPData, nLen);
- }
- free(nDataRadix);
- return 1;
- }
- void main()
- {
- int inputNumber[]={212,127,425,159,681,914,326,153,558};
- int count = 9;
- cout<<"原始数组:"<<endl;
- show(inputNumber,count);
- cout<<"排序过程:"<<endl;
- RadixSort(inputNumber,count);
- cout<<"排序结果:"<<endl;
- show(inputNumber,count);
- }
基数排序的过程:
下面来分析一下基数排序的复杂度:
时间效率:设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix))。
其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。
接下来是稳定性的讨论:
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
原理:将数字按位数划分出n个关键字,每次针对一个关键字进行排序,然后针对排序后的序列进行下一个关键字的排序,循环至所有关键字都使用过则排序完成。
要点:对关键字的选取,元素分配收集。
下面对前面讨论过的和未讨论的各种排序算法进行一下总结(注释:以下表格引用自明哥的博客):
名称 | 时间复杂度 | 原理 | 稳定性 | 举例(3,1,4,5,2) |
名次排序 | O(n2) | 先排出名次,再根据名次,将数据放入相应的 数组排序 | 稳定 | 名次是(3,1,4,5,2) 排序是(1,2,3,4,5) |
冒泡排序 | O(n2) | 一次冒泡是从前面开始两两比较,如果前面 大后面数小就互换,共冒n次泡 | 稳定 | 一次冒泡:1,3,4,2,5 二次冒泡:1,3,2,4,5 三次冒泡:1,2,3,4,5 四次冒泡:1,2,3,4,5 五次冒泡:1,2,3,4,5 |
插入排序 | O(n2) | 第一个数据不动,将第二个与之比较,并插 入,第三个与前两个比较并插入....直到n个 | 稳定 | 插入第二个:1,3,4,5,2 插入第三个:1,3,4,5,2 插入第四个:1,3,4,5,2 插入第五个:1,2,3,4,5 |
选择排序 | O(n2) | n个数中选择最大的放在最后,再在前n-1个 选择最大的放在倒数第二个...直到第一个. | 稳定 | 第一次:3,1,4,2,5 第二次:3,1,2,4,5 第三次:1,2,3,4,5 第四次:1,2,3,4,5 |
基数排序 | O(n) | 按照基数r分为r个盒子,将符合条件的数据 放入相应链表盒子里,直到排序完成 | 稳定 | 一次:1,2,3,4,5 |
堆排序 | O(nlogn) | 利用最大堆排序,将数据初始化为最大堆, 依次删除最大元素,直到删完,排序完成 | 不稳定 | 删除5,4,3,2,1 依次放入12345 |
拓扑排序 | O(n2) | 在由任务建立的有向图中,边(i,j)表示在装配序列 中任务i 在任务j 的前面,具有这种性质的序列称 为拓扑序列根据任务的有向图建立拓扑序列的过程 | 稳定 | 无 |
快速排序 | 平均O(nlog2n) 最坏O(n2) | 不断寻找一个序列的中点,然后对中点左右 的序列递归的进行排序,直至全部序列排序 完成,使用了分治的思想 | 稳定 | 第一次;1,3,4,5,2 第二次:1,3,4,2,5 第三次:1,2,3,4,5 |
归并排序 | 平均O(nlog2n) 最坏O(nlog2n) | 将原序列划分为有序的两个序列,然后利用 归并算法进行合并,合并之后即为有序序列。 | 稳定 | 3,1 4,5,2 1,3 2,4,5 1, 2, 3 ,4, 5 |