• 算法6 排序算法 QuickSort 快速排序


         

    Quick sort 快速排序

       

            快算排序 Quick Sort ,可能是应用最为广泛的算法,被视为20世纪科学和工程领域的十大算法之一。其流行的原因是因为它实现简单,可适用于不同数据,并且在一般场景下比其他算法要更快。其优点是:

    可借用一个很小空间的辅助栈来进行原地排序;

    对于长度为N的数组,其时间复杂度为 NlogN;

    快速排序的内循环短小,这意味它无论在理论上还是在实际中,它都要更快。

    缺点:

            要小心的避免错误,防止性能退化为N² 。

    算法思想:

            快速排序 和 归并排序一样都是使用了分治思想的排序算法。他们都是将一个数组,一分为二,独自独立排序。 归并排序是将数组分为两个子数组分别排序,并将有序的子数组归并将整个数组进行排序;而快速排序是当子数组都有序了之后,整个数组自然就实现了有序。归并的切分位置就在数组中间位置;而快速排序的切分位置partition 取决于数组的内容。

    Partition过程大致如图所示:

     

           快速排序需要推举出一个元素来作为我们的基准元素;如图所示:我们推举出我们的首个元素作为基准元素(V)。帮助我们的基准元素找他应该所处的位置过程,我们将其命名为 Partition的过程,即找到 j 的过程。

            结合如图所示,我们来看我们应该如何去做这件事:

    在这个过程中,我们需要满足的V前面的元素是不大于V的,V后面的元素是不小于V的。即 arr[l+1...j] < v && arr[j+1...i-1] > v

             图中变量 , v :基准元素  ,l :元素开始位置索引,j基准位置索引,i 遍历比较过程中的当前时刻的位置索引;

    蓝色元素e本身比较前的位置就处于不小于V的位置,如果发现e 大于 V 不动,i++遍历下一个元素;如果发现当前遍历元素e小于V,则需要将e和紫色部分的首元素交换,并且j++。如此进行下去,直到i遍历完整个数组后,把l位置所在元素,和 j 位置所在元素 进行交换后,即可完成整个Partition的过程。

    算法实现:

    基于以上内容,我们来实现第一版的算法基本实现:

    1. package com.cosyit.offer.algorithms;
    2. import java.util.Arrays;
    3. public class QuickSort1 {
    4. private static void quickSort(Comparable[] a, int n) {
    5. __quickSort(a, 0, n - 1); // a[0] - a[length-1]
    6. }
    7. private static void __quickSort(Comparable[] a, int l, int r) {
    8. //设置临界条件。 obscure
    9. if (l >= r) return;
    10. //隔板分区
    11. int p = partition(a, l, r);
    12. // l 到 p-1 的分区部分 进行快速排序。
    13. __quickSort(a, l, p - 1);
    14. // p+1 到 r 的分区部分 进行快速排序。
    15. __quickSort(a, p + 1, r);
    16. }
    17. /**
    18. * 推举出一个元素作为隔板元素以进行分区,隔板分区后该元素将处于其顺序的位置,并返回该隔板元素的索引。
    19. * 元素a[p]的隔板效果:左边的比隔板元素小 a[l...p-1] < arr[p],右边都比隔板元素大 a[p] < a[p+1...r]
    20. *
    21. * @param a 传入的数组。
    22. * @param l 左边界元素索引。
    23. * @param r 有边界元素索引。
    24. * @return 隔板操作后的,隔板位置索引。
    25. */
    26. private static int partition(Comparable[] a, int l, int r) {
    27. // 1.推举出一个元素,这里我们推举我们的头部元素为隔板元素。
    28. Comparable v = a[l];
    29. // 2.遍历整个数组。a[l+1...j]
    30. int j = l; //j:不大于v左边分区的尾部元素位置索引。
    31. /**
    32. * 这里有一个定义的技巧,就是 j 和 i 的初始值设计:
    33. * * 为什么 把j的初始化值设置为l , 因为这样设计 a[l+1...j]即a[l+1...l],保证了小于v的分区,从开始是不存在的分区。
    34. * * 为什么 把i的初始化值设置为l+1, 因为这样设计 a[j+1...i]即a[l+1...l+1) ,保证了大于v这个分区,从开始是不存在的分区。
    35. */
    36. for (int i = l + 1; i <= r; i++) {
    37. if (less(a[i], v)) {
    38. swap(a, j + 1, i);
    39. j++;
    40. }
    41. }
    42. //放置隔板:i 遍历完的时候,元素v即a[l] 和 j进行交换。把j的标记位置索引 j 返回。
    43. swap(a, l, j);
    44. return j;
    45. }
    46. private static void swap(Comparable[] arr, int i, int j) {
    47. Comparable temp = arr[i];
    48. arr[i] = arr[j];
    49. arr[j] = temp;
    50. }
    51. private static boolean less(Comparable a, Comparable b) {
    52. return a.compareTo(b) < 0;
    53. }
    54. public static void main(String[] args) {
    55. Integer[] arr = {6, 7, 1, 4, 3, 5, 2, 0};
    56. quickSort(arr, arr.length);
    57. System.out.println(Arrays.toString(arr));
    58. }
    59. }

    基础算法的改进:

          改进1:  推举首元素为隔板元素的是有弊端的,因为往往人们处理的数据大概率会是一个近乎有序的数组,这样的数据在进行 递归时候,分区的递归树的高度会很高,递归树的平衡度会很差。基于这个问题,我们应使 隔板元素 保持随机性,来避免这个问题。固添加如下代码以做优化:

    因为大部分的代码都是冗余的,关键代码贴出来即可

     

    改进2:如果处理切分元素有大量重复元素,而导致切分不平衡的情况,栈深度会变大。

           

     

    优化一下思路,如图所示:

              之前,我们把大于V和小于V的放在了数组的一端,现在把大于V和小于V的放在数据的两端。

            如图所示:i,j 的位置为比较元素e,i++往右跑,j--往左跑。

    i在遍历过程中遇到e>=v 不满足小于v的情况,停止i++;同理,j--的过程中遇到 e<= v 不满足大于v的情况,停止j--;

    i和j交换位置,交换位置后,i++查看下一个元素,j++查看下一个元素。

    直到 i 和 j 索引重合,意味着遍历结束。

            实际上,按照这个逻辑走完,其实 左分区 是小于等于v的元素集合,右分区是大于等于v的元素集合。

            如果 i 和 j 都指向了 等于v的元素,两个仍然要交换一下位置,这样就遇到大量等于v的元素的情况,也可以尽量平分这些元素。

    算法思想实现:

    1. package com.cosyit.offer.algorithms;
    2. import java.util.Arrays;
    3. import java.util.Random;
    4. public class QuickSort3 {
    5. private static void quickSort(Comparable[] a, int n) {
    6. __quickSort(a, 0, n - 1); // a[0] - a[length-1]
    7. }
    8. private static void __quickSort(Comparable[] a, int l, int r) {
    9. //设置临界条件。 obscure
    10. if (l >= r) return;
    11. //隔板分区
    12. int p = partition(a, l, r);
    13. // l 到 p-1 的分区部分 进行快速排序。
    14. __quickSort(a, l, p - 1);
    15. // p+1 到 r 的分区部分 进行快速排序。
    16. __quickSort(a, p + 1, r);
    17. }
    18. /**
    19. * 推举出一个元素作为隔板元素以进行分区,隔板分区后该元素将处于其顺序的位置,并返回该隔板元素的索引。
    20. * 元素a[p]的隔板效果:左边的比隔板元素小 a[l...p-1] < arr[p],右边都比隔板元素大 a[p] < a[p+1...r]
    21. *
    22. * @param a 传入的数组。
    23. * @param l 左边界元素索引。
    24. * @param r 有边界元素索引。
    25. * @return 隔板操作后的,隔板位置索引。
    26. */
    27. private static int partition(Comparable[] a, int l, int r) {
    28. // core 保持随机性。
    29. //生成随机索引。 l + 随机[0 ... r - l + 1)的数
    30. int randomIndex = l + new Random().nextInt(r - l + 1);
    31. //和首元素交换。
    32. swap(a, l, randomIndex);
    33. // 1.推举出一个元素,这里我们推举我们的头部元素为隔板元素。
    34. Comparable v = a[l];
    35. int i = l + 1, j = r;
    36. while (true) {
    37. // i在运动的过程中,i <= r 防止索引越界。
    38. while (i <= r && less(a[i], v)) i++; // i往前走不停止。
    39. while (j >= l + 1 && less(v, a[j])) j--; // j往前走不停止。
    40. if (i > j) break;
    41. //都停下来了,交换元素。
    42. swap(a, i, j);
    43. //各自指向下一个元素,继续。
    44. i++;
    45. j--;
    46. }
    47. swap(a,l,j);
    48. return j;
    49. }
    50. private static void swap(Comparable[] arr, int i, int j) {
    51. Comparable temp = arr[i];
    52. arr[i] = arr[j];
    53. arr[j] = temp;
    54. }
    55. private static boolean less(Comparable a, Comparable b) {
    56. return a.compareTo(b) < 0;
    57. }
    58. public static void main(String[] args) {
    59. Integer[] arr = {6, 7, 1, 4, 3, 5, 2, 0};
    60. quickSort(arr, arr.length);
    61. System.out.println(Arrays.toString(arr));
    62. }
    63. }

    改进3:以上实现中,对于大量重复的元素,会不必要的将等值的元素进行交换。为了规避这个问题,有一种经典的实现方式 --- 三路切分的快速排序 Quick Sort 3 Way。

    算法思路: 

    之前在切分过程中都是切分为大于基准元素V和小于基准元素V的情况,三路排序则是切分为 等于基准元素V,大于V和小于V 三种情况。

     

              如图所示  l 为 首元素,此处放置了基准元素。 lt 指向小于V分区 的最后一个元素;gt指向大于V分区的 第一个元素。e 是活动元素a[i]  —— 当前要和基准元素V进行比较的元素。

        ① e小于V,e 和 ==v 的第一个元素交换 即 a [ lt + 1 ] ,然后 lt++,  i++;

        ② e大于V,e和 a[gt-1] 元素交换后,gt -- ,但是此时 i不用处理,因为i的元素是一个gt-1换过来的新元素 ;

        ③ 当元素e 即 a[i] 和基准元素V相等,直接 i++ 继续处理下一个元素即可。

        当整个数组 i 和 gt 索引重合的时候,就是所有元素遍历完成的时候。最后,l位置元素和 v 进行交换。lt 位置即为返回的位置。 

    三路排序算法思想实现:

    1. package com.cosyit.offer.algorithms;
    2. import java.util.Arrays;
    3. import java.util.Random;
    4. public class QuickSort3Ways {
    5. private static void quickSort3Ways(Comparable[] a, int l, int r) {
    6. if(r<=l) return;
    7. // core 保持随机性。
    8. //生成随机索引。 l + 随机[0 ... r - l + 1)的数
    9. int randomIndex = l + new Random().nextInt(r - l + 1);
    10. //和首元素交换。
    11. swap(a, l, randomIndex);
    12. // 1.推举出一个元素,这里我们推举我们的头部元素为隔板元素。
    13. Comparable v = a[l];
    14. int lt = l; // a[l+1 ... lt] < v , a[l+1 ... lt]初始范围为空。
    15. int gt = r + 1; // a[gt...r] > v ,a[gt...r] 初始范围为空。
    16. int i = l + 1;// l为首个基准元素V,遍历元素从l+1开始。 a[lt+1...i) == v
    17. while (i < gt) { //只要i在比较的过程中,i没有和gt碰上,就一直遍历。
    18. int cmp = a[i].compareTo(v);
    19. if (cmp < 0) {
    20. swap(a, i, lt + 1);
    21. lt++; i++;
    22. } else if ( cmp >0) {
    23. swap(a, i, gt - 1);
    24. gt--;
    25. } else {
    26. i++;
    27. }
    28. }
    29. //v和lt交换
    30. swap(a, l, lt); lt--;
    31. //此时, ==v 的部分已经在有序的部分,无须排序。
    32. //排序 l...lt 的左分区。
    33. quickSort3Ways(a, l, lt );
    34. //排序 gt...r 的右分区。
    35. quickSort3Ways(a, gt, r);
    36. }
    37. private static void swap(Comparable[] arr, int i, int j) {
    38. Comparable temp = arr[i];
    39. arr[i] = arr[j];
    40. arr[j] = temp;
    41. }
    42. public static void main(String[] args) {
    43. Integer[] arr = {6, 7, 1, 4, 3, 5, 2, 0};
    44. quickSort3Ways(arr,0, arr.length-1);
    45. System.out.println(Arrays.toString(arr));
    46. }
    47. }

    以上代码可以将和切分元素相等的元素归位,这样和v相等的元素在就不会在递归函数中处理了,在递归的函数中和v相等的元素,不会参与排序了,这样提高了效率。

     

  • 相关阅读:
    接口自动化测试实践指导(中):接口测试场景有哪些
    Springboot+vue的入校申报审批管理系统(有报告),Javaee项目,springboot vue前后端分离项目。
    CSS初阶语法
    Siamese Neural Network (SNN: 孪生神经网络)
    在 Google Cloud 上轻松部署开放大语言模型
    Tomcat 异步组件 —— Nio2Endpoint
    WCF异常System.ServiceModel.ProtocolException问题处理
    浏览器页面刷新,history增加,需要多次调用history.back()才能后退的解决方法
    dspe-peg-cy7.5;磷脂-聚乙二醇-CY7.5吲哚菁绿
    【博客494】K8s TLS Bootstrap机制
  • 原文地址:https://blog.csdn.net/wdw18668109050/article/details/127662064