【C++研发面试笔记】19. 常用算法-排序算法

【C++研发面试笔记】19. 常用算法-排序算法

19.1 排序算法分类

比较排序和非比较排序:
常见的排序算法都是比较排序,非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置。
比较排序的时间复杂度通常为O(n^2)或者O(nlogn),比较排序的时间复杂度下界就是O(nlogn),而非比较排序的时间复杂度可以达到O(n),但是都需要额外的空间开销。
【C++研发面试笔记】19. 常用算法-排序算法_第1张图片


19.2 冒泡排序

冒泡排序通过重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,直到没有再需要交换的元素为止(对n个项目需要O(n^2)的比较次数)。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

19.2.1 实现步骤

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

19.2.2 实现性能

最差时间复杂度O(n^2)
最优时间复杂度O(n) 
平均时间复杂度O(n^2)
最差空间复杂度:总共O(n),需要辅助空间O(1)

19.2.3 具体代码

【C++研发面试笔记】19. 常用算法-排序算法_第2张图片


19.3 简单选择排序

常用的选择排序方法有简单选择排序和堆排序,这里只说简单选择排序,堆排序后面再说。
假设所排序序列的记录个数为n,i 取 1,2,…,n-1。
从所有n-i+1个记录(Ri,Ri+1,…,Rn)中找出排序码最小(或最大)的记录,与第i个记录交换。执行n-1趟 后就完成了记录序列的排序。

19.3.1 选择排序性能

  1. 在简单选择排序过程中,所需移动记录的次数比较少。最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。 
  2. 最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。
  3. 即进行比较操作的时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。 
  4. 简单选择排序是不稳定排序。

19.3.2 排序算法稳定性

通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。
排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。
不稳定排序算法口诀:快希选堆(快些选堆)

19.3.3 具体实现

【C++研发面试笔记】19. 常用算法-排序算法_第3张图片


19.4 插入排序

插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。

  1. 比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。
  2. 如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。
  3. 所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

19.4.1 插入排序性能

将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,是稳定的排序方法。
空间复杂度O(1)。 
平均时间复杂度O(n^2)。
最差情况:反序,需要移动n*(n-1)/2个元素 ,运行时间为O(n^2)。
最好情况:正序,不需要移动元素,运行时间为O(n)。

19.4.2 折半插入排序

直接插入排序中要把插入元素与已有序序列元素依次进行比较,效率非常低。 
折半插入排序,使用使用折半查找的方式寻找插入点的位置, 可以减少比较的次数,但移动的次数不变, 时间复杂度和空间复杂度和直接插入排序一样,在元素较多的情况下能提高查找性能。

19.4.3 具体实现

【C++研发面试笔记】19. 常用算法-排序算法_第4张图片


19.5 堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值。
由于堆中每次都只能删除第0个数据,通过 取出第0个数据再执行堆的删除操作、重建堆(实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。),然后再取,如此重复实现排序。

19.5.1 堆排序性能

空间复杂度O(1)。 
平均时间复杂度O(nlogn)。
最差情况:运行时间为O(n^2)。
最好情况:运行时间为O(n)。
不稳定。

19.5.2 具体实现

【C++研发面试笔记】19. 常用算法-排序算法_第5张图片


19.6 归并排序

归并排序,是创建在归并操作上的一种有效的排序算法该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
即先使每个子序列有序,再将两个已经排序的序列合并成一个序列的操作。若将两个有序表合并成一个有序表,称为二路归并。

19.6.1 实现步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

19.6.2 递归实现

  1. 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素。
  2. 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
  3. 重复步骤2,直到所有元素排序完毕

19.6.3 归并排序性能

归并排序速度仅次于快速排序,为稳定排序算法(即相等的元素的顺序不会改变),一般用于对总体无序,但是各子项相对有序的数列。
时间复杂度为O(nlogn)  
空间复杂度为 O(n) 
归并排序比较占用内存,但却是一种效率高且稳定的算法。

19.6.4 归并排序具体实现

【C++研发面试笔记】19. 常用算法-排序算法_第6张图片


19.7 快速排序

快速排序(Quicksort)是对冒泡排序的一种改进,又称划分交换排序。快速排序使用分治法策略来把一个序列分为两个子序列。

19.7.1 实现步骤

  1. 从数列中挑出一个元素,称为”基准”(pivot)
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)(通过交换)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
    【C++研发面试笔记】19. 常用算法-排序算法_第7张图片

19.7.2 快速排序性能

事实上,快速排序通常明显比其他Ο(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
最差时间复杂度 Ο(n^2) 
最优时间复杂度 Ο(n log n) 
平均时间复杂度Ο(n log n) 
最差空间复杂度 根据实现的方式不同而不同

19.7.3 一趟快速排序的算法

  1. 设置两个变量i、j,排序开始的时候:i=0,j=N-1;
  2. 以第一个数组元素作为关键数据,赋值给key,即key=A[0];
  3. 从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
  4. 从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
  5. 重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

19.7.4 具体实现

【C++研发面试笔记】19. 常用算法-排序算法_第8张图片


19.8 希尔排序

希尔排序法(缩小增量法)属于插入类排序,是将整个无序列分割成若干小的子序列分别进行插入排序的方法。
1. 把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;
2. 随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

19.8.1 实现效率

希尔排序是一个不稳定的排序,其时间复杂度受步长(增量)的影响。
空间复杂度: O(1)
时间复杂度: 平均 O(n^1.3),最好 O(n) ,最坏 O(n^2)

19.8.2 具体实现

【C++研发面试笔记】19. 常用算法-排序算法_第9张图片


19.9 桶排序

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法(属于分而治之)。
假设有一组长度为N的待排关键字序列K[1….n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。
桶排序与归并排序、快速排序看起来好像很类似,都用到了分而治之的方法,而桶排序的重点在于通过关键函数将数据放到有序排列的桶中。
比如求0~1间的小数排序,可以分成10个桶,分别存入0~0.1, 0.1~0.2….,之后先在0.1的桶中排序,再归并的时间也是常数了,所以说桶数越多,所花的时间也越少。

19.9.1 桶排序的步骤:

  1. 设置一个定量的数组当作空桶子。
  2. 寻访序列,并且把项目一个一个放到对应的桶子去。
  3. 对每个不是空的桶子进行排序。
  4. 从不是空的桶子里把项目再放回原来的序列中。

19.9.2 性能

最差时间复杂度 O(n^2)
平均时间复杂度 O(n+k)
最差空间复杂度 O(n*k)
平均情况下桶排序以线性时间运行,桶排序是稳定的,排序非常快,但是同时也非常耗空间,基本上是最耗空间的一种排序算法。
对N个关键字进行桶排序的时间复杂度分为两个部分:

  1. 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。
  2. 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

很显然,第2部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点: 

  1. 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。 
  2. 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。
    【C++研发面试笔记】19. 常用算法-排序算法_第10张图片

19.10 基数排序

基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

  1. 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
  2. 然后,从最低位开始,依次进行一次排序。
  3. 这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

19.10.1 例子

我们可以按照下面的一组数字做出说明:12、 104、 13、 7、 9
(1)按个位数排序是12、13、104、7、9
(2)再根据十位排序104、7、9、12、13
(3)再根据百位排序7、9、12、13、104
(4)如果数据在这个位置的余数相同,那么数据之间的顺序根据上一轮的排列顺序确定;

19.10.2 效率

基数排序的时间复杂度是O(k·n),其中n是排序元素个数,k是数字位数。注意这不是说这个时间复杂度一定优于O(n·log(n)),k的大小取决于数字位的选择和待排序数据所属数据类型的全集的大小;k决定了进行多少轮处理,而n是每轮处理的操作数目。
基数排序基本操作的代价较小,k一般不大于logn,所以基数排序一般要快过基于比较的排序,比如快速排序。
最差空间复杂度是O(k·n)

19.10.3 实现

【C++研发面试笔记】19. 常用算法-排序算法_第11张图片
【C++研发面试笔记】19. 常用算法-排序算法_第12张图片


19.11 计数排序

计数排序是一类基于非比较的排序算法,主要用于对一定范围内的整数排序(特别是出现大量重复的情况)时,它的复杂度为Ο(n+k)(其中k是整数的范围)。
假设输入的线性表L的长度为n,表中元素为L1,L2,..,Ln,线性表的元素属于有限偏序集S={S1,S2,..Sk}。则计数排序可以描述如下:

  1. 对于线性表元素Ln,扫描整个集合S,若Ln小于或等于Si,则将Si的元素的个数T(Si);
  2. 扫描整个线性表L,对L中的每一个元素Li,进行上面步骤
#include 
using namespace std;
const int MAXN = 100000; // 待排序线性表长度
const int k = 1000; // 数据范围
int a[MAXN], c[MAXN], ranked[MAXN];

int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; ++i) {
        cin >> a[i];  //输入数据
        ++c[a[i]]; // 记录重复的数据
    }
    for (int i = 1; i < k; ++i)
        c[i] += c[i-1]; // 得到计数表
    for (int i = n-1; i >= 0; --i)
        ranked[--c[a[i]]] = a[i]; // 得到排序后的表
    for (int i = 0; i < n; ++i)
        cout << ranked[i] << endl;
    return 0;
}

这篇博文是个人的学习笔记,内容许多来源于网络(包括CSDN、博客园及百度百科等),博主主要做了微不足道的整理工作。由于在做笔记的时候没有注明来源,所以如果有作者看到上述文字中有自己的原创内容,请私信本人修改或注明来源,非常感谢>_<

你可能感兴趣的:(C++,C++研发面试笔记)