排序问题(三)

原文地址:http://www.javawxs.com/index.php/sort-3/

前两篇谈了插入排序、希尔排序、堆排序和归并排序,都还是比较容易掌握的,这篇将主要说说快速排序算法;因为快速排序比较复杂涉及的算法较多,精炼而且高度优化的内部循环;使得快速排序运行非常迅速,但也加大了理解难度。它的平均运行时间是O(N log N),最坏情形是O(N^2);不过这种情况很少出现。通过快速排序和堆排序的结合使用,我们可以基本上所有的输入都达到快速排序的快速运行时间。和归并排序一样,快速排序也是一种分治的递归算法。将数组S排序的基本算法由以下步骤组成:

1、 如果S中元素个数是0或1,则返回。

2、 取S中任一元素v,作为枢纽元。

3、 将S-{v}(S中其余元素)分为两个不相交集合:S1={x∈S-{v}|x<=v}和S2={x∈S-{v}|x>=v}.

4、 返回{quicksort(S1)后跟v,继而返回quicksort(S2)}。

由于对那些等于枢纽元的元素的处理上,第三步分割的描述不是唯一的,因此这就成为了一种设计决策。我们最好能有效的解决这个问题;我们希望把等于枢纽元的大约一半的关键字分到S1中,剩余的分到S2中,就像二叉查找树那样保持平衡。

关于选择枢纽元的选取问题:

·选择第一个元素作为枢纽元,如果输入的随机的,那么不会存在问题,但是如果输入是预排序的或者是反序,就会出现严重的问题,所有元素不是被分到S1就是都被分到S2中,更严重的是这种情况会出现在所有的递归调用中。例如,在一个预排序的输入中,把第一个元素作为枢纽元,那么快速排序花费的时间是二次的,可实际毫无意义。所以选取第一个 元素作为枢纽元是非常不可取的。

·随机选择枢纽元,这种方法比较安全,但是生成随机数会开销很大,会影响算法的运行时间。

·选择数组中的中值(第[N/2]个最大数);不过,这很难算出并且会明显减慢排序速度。一般情况下是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。使用三数中值分割法消除了预排序输入的最坏情形,并减少了比较次数。

分割策略:例如输入为9,2,5,10,7,4,6,3,8,1的数据,第一步通过将枢纽元与最后的元素交换使其离开被分割的数据段,i从第一个元素开始,j从倒数第二个元素开始。下图为当前状态:

在分割阶段就是要把所有小于枢纽元的元素放到数组左边,把大于枢纽元的元素放到数组右边。

当i在j左边的时候,我们将i右移过所有小于枢纽元的元素,并将j左移,移过大于枢纽元的元素。当i和j停止时,i指向一个大元素而j指向一个小元素。如果i在j的左边,那么两个元素互换,把一个大元素推到右边而把一个小元素推到左边在上面的例子中,i不移动而j划过一个位置,如下图:

然后交换i和j指向的元素,重复该过程直到i和j彼此交错为止。

现在i和j已经交错,不用再交换,最后将枢纽元与i指向的元素交换。

在最后一步当枢纽元与i所指向的元素交换时,在p<i的每一个元素必然都是小元素,在p>i上的元素必然都是大元素。

还有一个重要细节必须考虑-如何处理那些等于枢纽元的元素。当i或j遇到一个等于枢纽元的元素时是否应该停止移动。i和j应该做同时移动,否则分割将会向一个方向偏;例如可能出现所有的元素都被划分到S2中。

现在假设数组中所有元素都相等,如果i和j都停止,那么相等元素间会有多次无意义的交换,通过归并排序分析,这时运行时间为O(N log N)。

如果i和j都不停止,那必须防止i和j越界,不再进行交换操作。也许不错,但是要把枢纽元交换到i最后到过的位置,这个位置是倒数第二个位置(或者最后的位置)。这样将会产生两个非常不均衡的子数组,如果所有元素都相同,那么花费的时间为O(N^2)。对于预排序的输入来说,与使用第一个元素作为枢纽元没有区别。

所以,进行不必要的交换建立两个均衡的子数组比得到两个不均衡子数组好得多。但i和j遇到等于枢纽元的关键字时,i和j停止,从而避免了话费二次时间。

对于很小的数组(N<=20),快速排序的性能相对较低,倒不如使用插入排序。

以下是程序代码:

  1. package com.javawxs;
  2. public class QuickSort {
  3. public static final int CUTOFF = 3;// 当截取元素数量大于此值是使用快速排序
  4. public static <T extends Comparable<? super T>> void quickSort(T[] a) {
  5. quickSort(a, 0, a.length - 1);
  6. }
  7. private static <T extends Comparable<? super T>> T median3(T[] a, int left,
  8. int right) {
  9. int center = (left + right) / 2;
  10. if (a[center].compareTo(a[left]) < 0) {
  11. swapReferences(a, left, center);
  12. }
  13. if (a[right].compareTo(a[left]) < 0) {
  14. swapReferences(a, left, right);
  15. }
  16. if (a[right].compareTo(a[center]) < 0) {
  17. swapReferences(a, center, right);
  18. }
  19. swapReferences(a, center, right - 1);
  20. return a[right - 1];
  21. }
  22. private static <T extends Comparable<? super T>> void quickSort(T[] a,
  23. int left, int right) {
  24. if (left + CUTOFF <= right) {
  25. T pivot = median3(a, left, right);
  26. int i = left, j = right - 1;
  27. for (;;) {
  28. while (a[i].compareTo(pivot) < 0)
  29. i++;
  30. while (a[j].compareTo(pivot) > 0)
  31. j–;
  32. if (i < j) {
  33. swapReferences(a, i, j);
  34. } else {
  35. break;
  36. }
  37. }
  38. swapReferences(a, i, right - 1);
  39. quickSort(a, left, i - 1);
  40. quickSort(a, i + 1, right);
  41. } else {
  42. insertionSort(a, left, right);
  43. }
  44. }
  45. private static <T extends Comparable<? super T>> void swapReferences(T[] a,
  46. int i, int n) {
  47. T temp = a[i];
  48. a[i] = a[n];
  49. a[n] = temp;
  50. }
  51. }   

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值