高效排序之-堆排序、快速排序、归并排序

除了上一次介绍的希尔排序,堆排序,快速排序,也是经常用到的排序方式,其中快速排序可以说是一种性能十分优秀的排序。

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]之前的整数。那么会面临什么样的问题?对,会存在大量等于边界的数,这样的话如果把等于归于小于等于,那么必然“左重右轻”,如果归于大于等于,则会“左轻右重”。即,此时仍然面临着不平衡的问题。则解决这种问题的思路有两种:将数据划分为小于等于和大于等于,也就是使得等于边界的数在两侧都有,另一种比较明显的思想是:分三路:小于 ,等于,大于。

最后,想表明:快速排序确实是一种不稳定的排序算法,而其不稳定性是归因为其分治策略,因此领悟一个算法的本质对于掌握衡量,甚至算法设计有着至关重要的作用。

原文地址:https://www.cnblogs.com/shaonianpi/p/10988518.html