这里我们开始复习排序的一些面试题。
首先我们来看一下各个排序方法的时间复杂度和稳定性的比较,见下面表格:
排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
冒泡 | O(n2) | O(n2) | 稳定 | O(1) | n小时较好 |
交换 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
选择 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
插入 | O(n2) | O(n2) | 稳定 | O(1) | 大部分已排序时较好 |
基数 | O(logRB) | O(logRB) | 稳定 | O(n) |
B是真数(0-9), R是基数(个十百) |
Shell | O(nlogn) | O(ns) 1<s<2 | 不稳定 | O(1) | s是所选分组 |
快速 | O(nlogn) | O(n2) | 不稳定 | O(nlogn) | n大时较好 |
归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
堆 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好 |
选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法, 冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
1.冒泡排序:
算法原理:比较相邻的两个元素,如果第一个比第二个大,就交换他们两个,持续一直比下去,直到比完一轮,最后一个元素应该是最大的元素;之后再重新从首部开始比较,重复上述步骤,数字越来越少,直到没有足够一对的数字可以进行比较。
代码实现:
1 #include<iostream> 2 using namespace std; 3 void bubble_sort(int a[],int n){ 4 for(int i=0;i<n;i++) 5 for(int j=0;j=n-1-i;j++){ 6 if(a[j]>a[j+1]){ 7 int temp=a[j]; 8 a[j]=a[j+1]; 9 a[j+1]=temp; 10 } 11 } 12 }
算法分析:
-
-
-
倒序(最糟情况)
第一轮:10,9,8,7->10,9,7,8->10,7,9,8->7,10,9,8(交换3次)
第二轮:7,10,9,8->7,10,8,9->7,8,10,9(交换2次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:6次其他:
第一轮:8,10,7,9->8,10,7,9->8,7,10,9->7,8,10,9(交换2次)
第二轮:7,8,10,9->7,8,10,9->7,8,10,9(交换0次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:3次上面我们给出了程序段,现在我们分析它:这里,影响我们算法性能的主要部分是循环和交换,
显然,次数越多,性能就越差。从上面的程序我们可以看出循环的次数是固定的,为1+2+...+n-1。
写成公式就是1/2*(n-1)*n。
现在注意,我们给出O方法的定义:若存在一常量K和起点n0,使当n>=n0时,有f(n)<=K*g(n),则f(n) = O(g(n))。(呵呵,不要说没
学好数学呀,对于编程数学是非常重要的!!!)现在我们来看1/2*(n-1)*n,当K=1/2,n0=1,g(n)=n*n时,1/2*(n-1)*n<=1/2*n*n=K*g(n)。所以f(n)
=O(g(n))=O(n*n)。所以我们程序循环的复杂度为O(n*n)。
再看交换。从程序后面所跟的表可以看到,两种情况的循环相同,交换不同。其实交换本身同数据源的
有序程度有极大的关系,当数据处于倒序的情况时,交换次数同循环一样(每次循环判断都会交换),
复杂度为O(n*n)。当数据为正序,将不会有交换。复杂度为O(0)。乱序时处于中间状态。正是由于这样的
原因,我们通常都是通过循环次数来对比算法。
-
-
2.交换排序:
算法原理:每次用当前的元素同之后的元素一一的做比较。
代码实现:
1 #include<iostream> 2 using namespace std; 3 void exchange_sort(int a[],int n){ 4 //这个算法和冒泡排序差不多,但是它是用当前的元素同之后的所有元素作比较 5 //而冒泡是一对一对之间做比较,还是有差别的 6 for(int i=0;i<n;i++) 7 for(int j=i+1;j<n;j++){ 8 if(a[j]<a[i]){ 9 int temp=a[j]; 10 a[j]=a[i]; 11 a[i]=temp; 12 } 13 } 14 }
算法分析:
-
-
- 倒序(最糟情况)
第一轮:10,9,8,7->9,10,8,7->8,10,9,7->7,10,9,8(交换3次)
第二轮:7,10,9,8->7,9,10,8->7,8,10,9(交换2次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:6次其他:
第一轮:8,10,7,9->8,10,7,9->7,10,8,9->7,10,8,9(交换1次)
第二轮:7,10,8,9->7,8,10,9->7,8,10,9(交换1次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:3次从运行的表格来看,交换几乎和冒泡一样糟。事实确实如此。循环次数和冒泡一样
也是1/2*(n-1)*n,所以算法的复杂度仍然是O(n*n)。由于我们无法给出所有的情况,所以
只能直接告诉大家他们在交换上面也是一样的糟糕(在某些情况下稍好,在某些情况下稍差)。所以它是不稳定的。
- 倒序(最糟情况)
-
3.选择排序:
选择排序算法就是每一趟从待排序的记录中选出关键字最小(最大)的记录,顺序放在已排好序的子文件的最后(最前),直到全部记录排序完毕。常见的选择排序有直接选择排序(Selection Sort),堆排序(Heap Sort),平滑排序(Smooth Sort),笛卡尔树排序(Cartesian Sort),锦标赛排序(Tournament Sort),循环排序(Cycle)。下面介绍前两种:堆排序和选择排序。
直接选择排序:
算法原理:这是一种简单直观的排序算法。它首先在未排序序列中找到最小(大)元素,存放到排序序列的其起始位置,然后再从剩余未排序的序列元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素排序完毕。
算法的实现举例:
算法代码:
1 #include<iostream> 2 using namespace std; 3 void select_sort(int a[],int n){ 4 int min;//记录最小值元素的下标 5 for(int i=0;i<n-1;i++){ 6 min=i;//赋予初值 7 //寻找最小值,循环一遍 8 for(int j=i+1;j<n;j++){ 9 if(a[j]<a[min]) 10 min=j; 11 } 12 //如果循环一遍之后的最小值不为a[i],则交换元素 13 if(min!=i){ 14 int temp=a[i]; 15 a[i]=a[min]; 16 a[min]=temp; 17 } 18 } 19 }
/此处转载博客:http://blog.csdn.net/left_la/article/details/8648582
堆排序:是利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。堆排序的时间复杂度是O(nlgN),与快速排序达到相同的时间复杂度。但是在实际应用中,我们往往采用快速排序而不是堆排序。这是因为快速排序的一个好的实现,往往比堆排序具有更好的表现。堆排序的主要用途,是在形成和处理优先级队列方面。另外,如果计算要求是类优先级队列(比如,只要返回最大或者最小元素,只有有限的插入要求等),堆同样是很适合的数据结构。
算法原理:
-
- 通常堆是通过一维数组来实现的,在起始数组为0的情形中,对于节点i:
其左子节点的下标为 (2*i+1);
其右子节点的下标为 (2*i+2);
其父节点的下标为 floor((i-1)/2)。
在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义一下三个操作:
1.最大堆调整(Max Heapify):在假定节点i的左右子节点为根的两颗二叉树都是最大堆的前提下,确保父节点大于子节点,否则下降原父节点,最终使以i为根的子树成为最大堆。
2.创建最大堆(Build Max Heap):将堆所有数据重新排序,对所有非叶子节点调用一次Max Heapify。
3.堆排序(Heap Sort):首先创建最大堆,然后依次将堆的根节点与末节点交换、剔除末节点、对根节点进行最大堆调整,直到堆中的节点数为1,排序结束。
算法示意图:
- 通常堆是通过一维数组来实现的,在起始数组为0的情形中,对于节点i:
算法代码:
1 // 最大堆调整 2 void MaxHeapify(int *a, int i, int heapSize) 3 { 4 int l = (i+1)*2-1; 5 int r = (i+1)*2; 6 int largest; 7 8 if (l<=heapSize && a[l]>a[i]) 9 largest = l; 10 else 11 largest = i; 12 13 if (r<=heapSize && a[r]>a[largest]) 14 largest = r; 15 16 if (largest!=i) 17 { 18 swap(a[i], a[largest]); 19 MaxHeapify(a, largest, heapSize); 20 } 21 } 22 23 // 创建最大堆 24 void BuildMaxHeap(int *a, int len) 25 { 26 for (int i=len/2-1; i>=0; i--) 27 { 28 MaxHeapify(a, i, len-1); 29 } 30 } 31 32 // 堆排序 33 void HeapSort(int *a, int len) 34 { 35 BuildMaxHeap(a, len); 36 for (int i=len-1; i>0; i--) 37 { 38 swap(a[0], a[i]); 39 MaxHeapify(a, 0, i-1); 40 } 41 }
4.插入排序:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中的适当位置,直到全部记录插入完成为止。插入排序有很多,这里我么讲两种:直接插入排序和希尔排序。
直接插入排序:
算法思想:是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对未排序的数据,在已排序序列中从后向前扫描,找到相应位置并插入。
//此处转载: http://blog.csdn.net/left_la/article/details/8656425
插入排序算法的一般步骤:
1.从第一个元素开始,该元素可以认为已被排序;
2.取出下一个元素,在已经排序的元素序列中从后向前扫描;
3.如果该元素(已排序)大于新元素,将该元素移到下一个位置;
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5.将新元素插入到该位置后,重复2~5
算法示意图:
算法代码:
1 #include<iostream> 2 using namespace std; 3 void insert_sort(int a[],int n){ 4 for(int j=1;j<len;j++){ 5 int key=a[j];//该元素为未排序的部分的第一个元素 6 int i=j-1;//已经排序的元素下标,从后向前扫描 7 while(i>=0 && a[i]>key){//如果该元素大于新元素key 8 a[i+1]=a[i];//将该元素移到下一个位置 9 i--; 10 } 11 a[i+1]=key;//找到插入位置,插入新元素到排序数组中 12 } 13 }
希尔排序:也称为递减增量排序算法,是插入排序的一种高速而稳定的改进版本。希尔排序是基于插入排序的以下两点性质而提出改进方法的:1.插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;2.但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位。
算法步骤:
1.先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中,在各组内进行直接插人排序。
2.取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长串行都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
算法示意图:
算法代码:
1 #include<iostream> 2 using namespace std; 3 void shellsort(int a[],int n){ 4 int h=1; 5 while(h<n) 6 h=3*h+1;//这个是希尔的增量 7 while(h>0){ 8 //对每一个增强里面的元素都用插入排序进行排序 9 for(int j=h;j<n;j++){ 10 int key=a[j]; 11 int i=j-h; 12 while(i>=0 && a[i]>key){ 13 a[i+1]=a[i]; 14 i=i-h; 15 } 16 a[i+h]=key; 17 } 18 h=h/3;//下一轮的希尔增量 19 } 20 }
5.快速排序:详情查看:http://blog.csdn.net/liuchen1206/article/details/6954074
6.归并排序:详情查看:http://blog.csdn.net/left_la/article/details/8656953