C++面试笔记--排序

    这里我们开始复习排序的一些面试题。

  首先我们来看一下各个排序方法的时间复杂度和稳定性的比较,见下面表格:

      

排序法 平均时间 最差情形 稳定度 额外空间 备注
冒泡 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 }
View Code

  算法分析:

      •    

        倒序(最糟情况)
        第一轮: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 }
View Code

  算法分析:

      •     倒序(最糟情况)
        第一轮: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 }
View Code

  /此处转载博客: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,排序结束。

      算法示意图:

  算法代码:

  

 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 }
View Code

    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 }
View Code

  希尔排序:也称为递减增量排序算法,是插入排序的一种高速而稳定的改进版本。希尔排序是基于插入排序的以下两点性质而提出改进方法的: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 }
View Code

    5.快速排序:详情查看:http://blog.csdn.net/liuchen1206/article/details/6954074

    6.归并排序:详情查看:http://blog.csdn.net/left_la/article/details/8656953

原文地址:https://www.cnblogs.com/Kobe10/p/5573415.html