《算法》第四版algs4:sort排序算法C++实现

具体代码:
https://github.com/Nwpuer/algs4-in-cpp/blob/master/sort.h

这一章的实现,相比于书上我做了轻微的改变,主要目的是把代码写的更加简洁易懂,更加关注算法是如何实现的,换言之,更关注算法的本质,而不是如何去设计一个C++类。
做出的改动如下:
1.没有再将每个排序算法分别写成一个类,而是将每个排序算法都写成一个函数,放在"sort.h"文件中
2.为了将算法适用于不同类型,书上是只要该类型实现了Comparable接口就行,而我将算法实现为了范型函数
3.书上使用的less()函数,因为C++中类一般都要求实现operator<,也就是可以用<来比较,所以我们直接用<,而不再去实现less()
4.书上使用的exch()函数,直接使用swap()。值得注意的是,每次使用swap之前我们都声明了using std::swap,然后我们调用的时候是swap(),这样如果排序的类型自己实现了swap,那么会优先调用它,而不是std::swap。
5.书上实现的SortCompare,而我这里改成了直接计算排序的时间,在"sort_test.cpp"中。这样虽然功能没有SortCompare强,但是实现更简单了。

下面是关于各个算法的一些笔记:

选择排序:在数组中找到最小的那个元素,将它和第一个元素交换;然后在剩下的元素中找到最小的,将它和第二个元素交换。如此往复。所以选择排序左边的排序完成的部分是已经确定好位置的,不用再去动它,只需要在右边没有排序的部分中找到最小的然后和该部分的第一个交换就行了。可以在脑子中想象出那个排序的画面。
选择排序的特点:运行时间与输入无关,一个有序的数组和一个元素随机排列的数组所用时间一样长

插入排序:和整理牌的方法类似,拿到一张牌,将它插入到已经有序的牌中的适当位置。也就是,当前索引左边的元素都是有序的,但是它们的最终位置还没有确定,有可能为了给更小的元素腾出空间而被移动。取下一个未排序的元素,将它放到前面元素的适当位置中。也可以在脑子中想象出排序的画面。
插入排序的特点:对于部分有序的数组很有效。当倒置的数量很少时,插入排序可能比其他算法都要快。也很适合小规模数组。这很重要,因为这些类型的数组在实际应用中经常出现,并且也是高级排序算法的中间过程。所以高级排序算法中经常会用到插入排序。

希尔排序:基于“插入排序”的算法。为什么要基于插入排序呢?因为排序之初,每个子数组都很短(因为间隔很大),排序之后的子数组都是部分有序的,这两种情况都适合插入排序。所以希尔排序更高效的原因是它权衡了子数组的规模和有序性。
希尔排序的特点:与选择和插入排序不同,希尔排序可以用于大型数组,并且数组越大,优势越大。
什么时候用希尔排序:对于中等大小的数组它的速度可以接受,并且优点是代码量很小,不需要使用额外内存。对于之后的高效的排序算法,除了对于很大的N,它们可能只会比希尔排序快两倍(可能还达不到),而且更复杂。

归并排序:优点是时间复杂度为O(NlgN),所以可用于大型数组的排序;缺点是所需额外空间和N成正比。
共有两种实现,自顶向下以及自底向上。和我们通常所理解的一样,自顶向下是用的递归方法,实现起来更加容易理解;虽然自底向上的实现方法代码量更少,但是其中的细节比较容易让人困惑。

快速排序:优点:实现简单,原地排序,将长度为N的数组排序所需时间和NlgN成正比。缺点:比较脆弱,需要非常小心才能避免低劣的性能。
和归并排序一样,快排也是分治的算法。将一个数组分成两个子数组,将两部分独立的排序。快排和归并排序的区别可以在脑子中很形象的想象出来:
归并排序是将数组分为两个子数组分别排序,再将有序的子数组归并以将整个数组排序;
快速排序则是先将小于某个值的元素放到左边,大于某个值的元素放到右边,这样的话将左右两个子数组分别排序后,这个数组就自然有序了。
快速排序在最好情况下所用比较次数满足分治递归的C(N)=2C(N/2)+N,该公式的解C(N)~NlgN。不是最好的情况下的结果也是类似的。
快排的一个缺点是:在切分不平衡时这个程序可能会极为低效。比如第一次从最小的元素开始切分,第二次从第二小的元素开始切分,等等。每次调用只移除一个元素,会导致一个大的子数组需要切分很多次。最多需要N+(N-1)+(N-2)+…+1=(N+1)N/2~N^2/2次比较。所以我们在一开始就将数组打乱,就是为了使产生糟糕的切分的可能性降到最低。
关于快排的改进:1.和归并排序一样,排序小的子数组时切换到插入排序。
2.对于含有大量重复元素的数组(现实应用中经常出现),之前的实现还有大量的改进空间,可将线性对数级别的性能提高到线性级别。使用三向切分(参见代码中的QuickSort3way_helper),简单的说,就是将元素分为小于,等于,大于三个部分,而不是原来的小于大于,大于等于两个部分,即将等于切分元素的单独提取出来。

优先队列:
适用情形:有些时候程序需要处理有序的元素,但不要求全部有序,或不要求一次将它们排序。比如说我们想要其中最大的一个,或者其中最大的M个,这时候如果将它们全部排序就是没有必要的了。
二叉堆:一组能够用堆有序的完全二叉树排序的元素,并且在数组中按照层级存储(不使用数组的第一个元素),简称为堆。
在一个堆中,位置为k的节点的父节点的位置为k/2(取整),它的两个子节点的位置为2k和2k+1。

堆排序:
1.注意下标是从1开始的,位置为0的元素未参与排序,一般作为哨兵
2.堆排序主要是两步,一是堆的构造,二是反复删除最大的元素。
关于这里代码在堆的构造中为什么是用sink()来从右到左地来构造子堆,而不是我们直觉上的从左到右的使用swim()类似插入元素一样来构造子堆:
1)数组中从右向左至少有一半是完全二叉树的叶子节点,就算二叉树的最下面一层不满也是这样的。这些叶子节点本身就是大小为1的堆,因此可以跳过这一半的元素,而如果使用swim()来构造的话,则必须把所有元素都给扫描了。一般来说,会快上20%
2)因为没有用到swim(),所以整个的代码更少,更简洁。

堆排序的优缺点:
优点:最坏的情况下也能保证使用O(NlgN)的时间复杂度和恒定的额外空间,所以空间十分紧张的时候很有用。
缺点:很致命,无法利用缓存。数组元素很少和相邻的其他元素相比较,因此缓存未命中的次数远远高于大多数比较都在相邻元素间进行的算法,如快速排序,归并排序,甚至是希尔排序,所以在现代系统的许多应用很少使用它。
但是另一方面,用堆来实现的优先队列在现代应用程序中越来越重要,因为它能在插入操作和删除最大元素操作混合的动态场景中保证对数级别的运行时间。

各个排序算法的比较(来自algs4):
在这里插入图片描述

该选用哪种排序算法:
大多数情况下,快排是最佳选择:之所以快是因为它的内循环指令很少,并且能够利用缓存,因为它总是顺序地访问数据,所以运行时间的增长数量级为~cNlgN,这里的c比其他的线性对数级别的排序算法的相应常数都要小。
但是如果稳定性很重要而且空间不是问题,归并排序可能最好。
其他情况,比如数组已经部分有序时,或者对于小数组排序(作为复杂排序算法的基础)时,插入排序可能更好;堆排序可能用处不大,但是优先队列的却很重要。

猜你喜欢

转载自blog.csdn.net/weixin_43462819/article/details/83685927