高效排序之-堆排序、快速排序、归并排序
除了上一次介绍的希尔排序,堆排序,快速排序,也是经常用到的排序方式,其中快速排序可以说是一种性能十分优秀的排序。
1 堆排序:
针对堆排序,对于其代码实现不作阐述,因为太过于复杂,主要是堆处理的复杂。
在此,对其算法的核心步骤作一定描述:
堆排序,本质上,分为两步:
1 建立堆:
1 广度优先初始化堆
2大数上浮,使满足堆属性
2 堆排序:
1 取走树根的值
2 将树根的值与 最下层 最右侧(注意这个描述所代表的位置)的叶子节点交换
3 去掉经过步骤2交换后的最下层,最右侧的叶子节点(也就是原根节点)
4 将最大数上浮至根
5 重复1、2、3、4
堆排序使用了堆这种数据结构,对数据进行排序,尽管上述过程看起来并不复杂,但在实际写程序的时候,也并不是一个简单的过程。
2 快速排序:
快速排序是一种十分优秀的排序方式,甚至说是一种最优的排序都不为过。
快速排序采用分治递归的思想完成了对数组的排序,快速排序的核心思想是:给基准数据(也就是边界)找其正确索引位置。
什么叫:给基准数据找其正确索引位置呢? 举个例子:
对于原始数据: 8,2,6,我们知道其从小到大的顺序是:2,6,8;那么对于数据8而言,正确的位置是:3,;对于数据2而言,正确的位置是1。那么给基准数据找其正确索引的位置被理解成:假如现在选定基准数据6,我们要能把数据6 放到位置 2 ,就完成了 给基准数据(也就是边界)找其正确索引位置。那么如何做到这一点呢?也就是:我们怎么样才知道一个数据被放到了正确的位置呢? 其实核心在于:这个数左边的数据都比这个数小,这个数右边的数据都比这个数大,那么这个数就被放到了一个正确位置。这是一种十分朴素但是又十分重要的思想。而快速排序证实基于这种思想完成了快速的排序。下面我们看看,快速排序如何做到这一点:
假如对于一组数据[a1,a2,....,an]假设数据的个数为n,大小顺序任意。我们先选择这组数据 (1+n)/2位置的数据作为基准数据,也就是中间位置的数据,将这个数左边作为一个子数组v1,这个数右边作为另外一个子数组v2。也就是整个大的待排序的数组被分成了2个小数组。这个时候,我们需要清楚:我们到底要干什么?
没错,我们需要将这个位置处于 (1+n)/2的数据放在正确的位置。这个时候,我们就要用到上面的思想了:这个数左边的数据都比这个数小,这个数右边的数据都比这个数大,那么这个数就被放到了一个正确位置。更具体而言:
这个数左边的子数组v1的每个数据都与基准数据进行比较,也就是与位置(1+n)/2的数据进行比较,如果比基准数据大,就把这个这个数据交换到右边数组对称的位置去。同样的,对于右侧数组,也采用相同的操作。最终,使得基准数据的左侧数据都比 基准数据(边界)小,使得基准数据的右侧侧数据都比 基准数据(边界)大。
但是这仅仅将一个数据放在了正确的位置,那么剩下的数据该怎么办呢?,同样的,对于左侧的子数组v1,右侧的子数组v2;再次选取各自的中间位置的数据作为边界,将v1分为v1-1,v1-2,将v2分解成v2-1,v2-2。这个时候对于从v1中选取的基准数据,从v2中选取的基准数据,将他们放在正确的位置.....一直这样做下去,最后整个大的数组被分解成n个小数组,即每个数组只有一个元素,排序结束,得到正确的排序结果。
这就是快速排序,怎么样,十分精彩吧,这种分治递归,为数据找正确位置的思想简直爆炸精彩好嘛23333.。
说了这么多,先上个代码吧:
1 //快速排序 2 void FastSort(vector<int> & sort_a) 3 { 4 int a_size; 5 a_size = sort_a.size();//得到数组大小 6 if (a_size < 2) 7 return; 8 int MaxIndex = 0; 9 for (int count = 1; count < a_size;count++) 10 { 11 if (sort_a[count]>sort_a[MaxIndex]) 12 { 13 MaxIndex = count; 14 } 15 } 16 swap(sort_a[a_size - 1], sort_a[MaxIndex]);//这里的swap用的是vector中的函数 17 FastSort(sort_a, 0, a_size-2); 18 } 19 ///快速排序 20 void FastSort(vector<int> & sort_array, int first, int last) 21 { 22 int lower = first + 1; 23 int upper = last; 24 swap(sort_array[first], sort_array[(first + last) / 2]); 25 int bound = sort_array[first];//将中间位置元素作为bound,并将其放在最开始的位置 26 while (lower <= upper) 27 { 28 while (sort_array[lower] < bound) 29 lower++; 30 while (sort_array[upper] > bound) 31 upper--; 32 if (lower < upper) 33 swap(sort_array[lower++], sort_array[upper--]); 34 else 35 lower++; 36 } 37 swap(sort_array[first], sort_array[upper]); 38 if (first < upper-1) 39 FastSort(sort_array, first, upper - 1); 40 if (last > lower) 41 FastSort(sort_array, lower, last); 42 /*if (last > upper+1) 43 FastSort(sort_array, upper + 1, last);*/ 44 }
上述过程完美的呈现了快速排序的优雅。在此对这个程序不再作阐述,有兴趣参考《c++数据结构与算法》这本书。只是这个程序中,有一个让我不安的地方:将待排序数组的最大数据放到了数据最右边,最大数据不参与排序。。。原因在于:
如果我们不将最大数放在最后,恰巧最大数据作基准数据了,这个时候,会发生什么???
这个时候,28,29行的代码会一直执行,一直执行到lower =n+1,显然这个时候,28行的while循环sort_array[lower] 对数组的访问已经越界了!!!!!。造成程序非正常中断。
我们再来关注一下:上文中有一句:我们先选择这组数据 (1+n)/2位置的数据作为基准数据。可能,每个人都会问,为什么选这个地方的数据,这样做一定好吗,什么样才是好的呢?
先说结论:快速排序最复杂的地方在于边界位置的选取,我们选取的这个地方的边界不一定是最好的。那么什么样的叫好的边界呢?
我们知道:快速排序的过程,数组一生二,二生四,四生八......如果我们选的边界,使得每次划分的子数组彼此间数据个数大致相等,那么这种边界选取就是一种优秀的方案。遗憾的是,并不存在一个理论的划分方式,这和数据的序列有关。这就导致了快速排序并不是一种稳定的算法(尽管如此,仍然很优秀)
那为何我们通常会选择中间位置的数据作为边界呢?那是因为:在实际上,数组的排序基本排序好了(注意是“基本”),这样选择中间的数据,其左右数据交换后,个数大致也是相等的)。
最后,再给上一个我认为讲解快排比较好的网站:http://www.sohu.com/a/246785807_684445 (讲的十分清楚)
归并排序
归并排序仍然采用了分治递归的做法,其核心思想是: 递归的将 排好序 的子数组 组合成 排好序的 大数组。
也就是:我们将一个数组拆成若干组数组(如果是拆成2组就是2路归并),子数组再拆成2组,子数组再拆成两组,最后每个组只有一个元素为止。然后反过来2个排序、4个排序、8个排序....那个排序.这就是归并的过程。
更为精彩的讲解,可以见简书上这位大神的描述:https://www.jianshu.com/p/33cffa1ce613’
需要注意的是:归并排序存在一个最大的问题:需要产生一个与原数组同样大小的数组空间来缓存中间结果!!!,造成空间复杂度为O(n)。
当然,我们考虑采用 链表,部分插入排序,这些方式来提高归并排序的效率。
再说快速排序算法与归并排序:
快排和归并排序都体现了一种思想:分治,但是有所不同。
归并是严格的1分为2,2分为4......这样的对半分的思想;
而快速排序并没有采用这种对半分的思想,而是选择一个边界,小于等于这个边界的数放在其前面,大于这个数的数放到其后面。最后该数就放在了正确的位置。 那么对于快排而言:怎么选这个边界就成为一个严重影响排序效率的事情。
因此,对于归并排序,其划分的过程实质是一个平衡二叉树的过程,那么对于快速排序而言,是否是平衡树,则要打上大大的???
现在考虑一种情况:对于一个几乎排好序的数组,如果我们每次选择最左侧的数作为边界,会发生什么???对,树结构直接退化成链结构,logn层结构直接退化成 n层结构,这样算法的复杂度直接由nlogn退化成n^2。这也表明了排序算法的不稳定性。那么我们应该如何改变这一点呢?
那就是我们采用随机数的方式,每次随机从序列中选择一个数作为边界条件。
那么也许看似不平衡性问题被解决了,但是还没有,假如现在有10000个数,但是这些数据都是在[0,10]之前的整数。那么会面临什么样的问题?对,会存在大量等于边界的数,这样的话如果把等于归于小于等于,那么必然“左重右轻”,如果归于大于等于,则会“左轻右重”。即,此时仍然面临着不平衡的问题。则解决这种问题的思路有两种:将数据划分为小于等于和大于等于,也就是使得等于边界的数在两侧都有,另一种比较明显的思想是:分三路:小于 ,等于,大于。
最后,想表明:快速排序确实是一种不稳定的排序算法,而其不稳定性是归因为其分治策略,因此领悟一个算法的本质对于掌握衡量,甚至算法设计有着至关重要的作用。