zoukankan      html  css  js  c++  java
  • 排序算法之快速排序

    参考过以下博客,在此表示感谢:
    1. 白话经典算法系列之六 快速排序 快速搞定
    2. 坐在马桶上看算法:快速排序

    1. 基本思路(挖坑填数 + 分而治之)

    • 1.1 从数组A中取出一个数作为基准数,比如说取A[0],将A[0]保存到x中,这时可以看作已经在元素A[0]处挖了一个,可以将其他数据填充到这里来。初始化,i = 0(left),j = 9(right),x = A[0] = 9
    0 1 2 3 4 5 6 7 8 9
    9 4 6 3 20 1 8 15 17 11
    • 1.2 先从j开始从后向前开始查找,遇到小于x的数,就将其填充到已挖好的坑A[0]处。此处,j = 6时,A[j] = 8,填充到坑A[0]处,此时相当于在A[j] = A[6]处又挖了一个新坑,下个数据可以填充到这里。又因为A[0]的坑已经填充了数据,故左下标值i要加一。这样,i = 1,j = 6。
    0 1 2 3 4 5 6 7 8 9
    8 4 6 3 20 1 8 15 17 11
    • 1.3 接着,从i = 1开始,从前向后查找,当遇到大于等于x的数,就将其填充到坑A[6]处。当i = 4,此时A[i] = A[4] = 20 > x,故将其填充到坑A[j] = A[6]处,j减一,并在A[i] = A[4]处形成新坑。这样,i = 4,j = 5。
    0 1 2 3 4 5 6 7 8 9
    8 4 6 3 20 1 20 15 17 11
    • 1.4 再从j开始,向前查找,当j = 5时,A[j] = 1 < x,此时将A[5]填入坑A[4]中,i加一,而A[5]成为新坑。这时,i = 5,j = 5。
    0 1 2 3 4 5 6 7 8 9
    8 4 6 3 1 1 20 15 17 11
    • 1.5 接着,从i = 5开始向右开始查找,由于i = j = 5,查找结束。这时,将x填入坑A[5]中,即另A[5] = 9,第一次排序完成。这时,A[5]前面的数都小于它,A[5]后面的数都大于它。
    0 1 2 3 4 5 6 7 8 9
    8 4 6 3 1 9 20 15 17 11
    • 1.6 再对两个分支,A[0…4]和A[6…9]重复上述步骤就可以完成排序。

    2. 步骤总结

    1. i = L, j = R,将基准数挖出形成第一个坑A[i]
    2. j–由后向前找出比基准数小的数,找出后挖出此数填入前一个坑A[i]中,对应的i加一
    3. i++由前向后找出大于等于基准数的数,找到后挖出此数填到前一个坑A[j]中,对应的j加一
    4. 重复执行2,3步,直到i == j,将基准数填入A[i]中

    参照此步骤,很容易写出挖坑填数的代码:

     1 int i = L, j = R, x = A[L];     //A[L]即A[i]就是第一个坑
     2 while(i < j)
     3 {
     4     //从后向前找小于x的数来填坑A[i]
     5     while(i < j)
     6     {
     7         if(A[j] < x)
     8         {
     9             A[i] = A[j];    //将A[j]填入A[i]中,这样A[j]就成了新坑
    10             i++;            //原来的坑A[i]已经填好,i值加一
    11             break;
    12         }
    13         else
    14             j--;
    15     }
    16     //从前向后找大于或等于x的数来填A[j]
    17     while(i < j)
    18     {
    19         if(A[i] >= x)
    20         {
    21             A[j] = A[i];    //将A[i]填入A[j]中,这样A[i]就成了新坑
    22             j--;            //原来的坑A[j]已经填好,j值减一
    23             break;
    24         }
    25         else
    26             i++;
    27     }
    28 }
    29 //退出时,i = j。将x填入这个坑中
    30 A[i] = x;

    进一步改写可得:

     1 int i = L, j = R, x = A[L];
     2 while(i < j)
     3 {
     4     while(A[j] >= x && i < j)       //从后往前找小于x的数
     5         j--;
     6     if(i < j)
     7         A[i++] = A[j];
     8     while(A[i] < x && i < j)        //从前往后找大于或等于x的数
     9         i++;
    10     if(i < j)
    11         A[i++] = A[j];
    12 }
    13 //退出时,i = j。将x填入这个坑中
    14 A[i] = x;

    3. 完整代码实现

    完整的代码实现如下:

     1 //override
     2 void quickSortOverride(int A[], int n)
     3 {
     4     //利用重载,简化程序的入口
     5     quickSortOverride(A, 0, n-1);
     6 }
     7 
     8 void quickSortOverride(int A[], int lo, int hi)
     9 {
    10     if (lo < hi)        //如果lo = hi,即只有一个元素时,已经是有序的了,不需要处理
    11     {
    12         int i = lo, j = hi, x = A[lo];
    13         while (i < j)
    14         {
    15             while (i < j && A[j] >= x)
    16                 j--;
    17             if (i < j)
    18                 A[i++] = A[j];
    19 
    20             while (i < j && A[i] < x)
    21                 i++;
    22             if (i < j)
    23                 A[j--] = A[i];
    24         }
    25         //退出时,i = j。将x填入这个坑中
    26         A[i] = x;
    27         quickSortOverride(A, lo, i-1);      //左边继续递归
    28         quickSortOverride(A, j+1, hi);      //右边继续递归
    29     }
    30 }

    4. 正确性分析

    1. 不变形
      经过k次排序,整个数组里已有k个元素有序,对于这k个元素中的任一元素,左边的所有元素都比它小,右边的所有元素都比它大。
    2. 单调性
      经过k次排序,相对无序的元素个数缩减至n-k
    3. 正确性
      经过至多n次排序后,算法必然终止,数组的元素都是有序的

    4.1 当选择最左边的元素A[lo]作为基准值时,为什么必须第一次从后往前扫描?

    假如是从前往后扫描,当扫描到第一个大于或等于x的元素A[i],必须将这个元素填入先前的坑A[lo]中,这样就已经出现了错误,导致最终i = j时,A[lo…i-1]的元素不是都小于A[i]的,至少A[lo]就已经不满足了。

    5. 复杂度分析

    为求解qSort(A, lo, hi)规模为n的问题,需要递归求解两个规模为n/2的问题qSort(A, lo, (lo+hi)/2-1)和qSort(A, (lo+hi)/2+1, hi),以及最坏情况下至多n次的比较填充操作。
    而递归基为:qSort(A, lo ,hi),其中lo = hi,所需的时间为O(1)。
    故可得到如下递推方程:
    T(n) = 2*T(n/2) + n;
    T(1) = O(1);
    求解:
    T(n) = 2*T(n/2) + n;
    T(n/2) = 2*T(n/22) + n/2;
    T(n/22) = 2*T(n/23) + n/22;
    ……
    T(2) = 2*T(1) + 2 = 2*T(n/2log2n) + n/2log2(n-1);
    T(1) = O(1);
    继而可得:
    T(n) = 2*T(n/2) + n;
    2*T(n/2) = 22*T(n/22) + n;
    22*T(n/22) = 23*T(n/23) + n;
    ……
    2log2(n-1)T(2) = 2log2n*T(1) + n;
    2log2nT(1) = 2log2nO(1) = n;
    可得:
    T(n) = n*(log2n - 0 + 1) = n*(log2n + 1) = O(nlogn)
    即最坏情况下的时间复杂度为:
    T(n) = O(nlogn)

    6. 另一种代码实现思路

    仍然是先选取一基准值,初始情况,可另i = lo, j = hi, x = A[lo]
    也是先从后向前查找小于x的数,当找到时,记录下此时的j值。
    接着从前向后查找大于x的数(注意此处是大于,不是上面一种算法实现的大于或等于),当找到时,记录下此时的i值。
    然后,交换对应的A[i]和A[j],如果i仍然小于j,重复上述步骤,直至i = j。紧接着,交换A[lo]和A[i]的值,至此做完了一次排序。这是A[lo…i-1]的各元素都小于等于A[i],A[i+1…hi]的各元素都大于等于A[i]。

    1. 代码实现

     1 //override
     2 void quickSortSwap(int A[], int n)
     3 {
     4     //利用重载,简化程序的接口
     5     quickSortSwap(A, 0, n-1);
     6 }
     7 
     8 void mySwap(int &x1, int &x2)
     9 {
    10     int tmp = x1;
    11     x1 = x2;
    12     x2 = tmp;
    13 }
    14 
    15 void quickSortSwap(int A[], int lo, int hi)
    16 {
    17     if (lo < hi)
    18     {
    19         int i = lo, j = hi, x = A[lo];
    20         while (i < j)
    21         {
    22             while (i < j && A[j] >= x)
    23                 j--;
    24 
    25             while (i < j && A[i] <= x)      //注意此处必须是小于等于
    26                 i++;
    27             if (i < j)
    28                 mySwap(A[i], A[j]);     //注意此处不能让i或j自加或者自减
    29         }
    30         mySwap(A[lo], A[i]);
    31         quickSortSwap(A, lo, i-1);
    32         quickSortSwap(A, j+1, hi);
    33     }
    34 }

    2. 正确性分析

    2.1 当选择最左边的元素A[lo]作为基准值时,为什么必须第一次从后往前扫描?

    若第一次从后往前扫描。交换总是成对进行的。进行完一次交换后,必然再一次从后往前扫描,分两种情况:1. 若j一直自减,没有发现比x小的元素,直至i = j终止,此刻停在之前完成交换的元素A[i]处,由于已经是参与过交换的元素,故A[i]一定小于x,这时,要拿x与A[i]交换,不会影响整体的有序性。2. 若j一直自减,中途发现了比x小的元素,记录下此时j的值。然后从前往后扫描,没有发现大于x的值,直至i = j,此时停在之前保留的j值的地方。由于还未完成一次交换,A[j]小于x,这时,要拿x和A[j]进行交换,也不会影响整体的有序性。而要做到这些,程序里必须保证在进行一次交换后,不要让i或j的值自增或者自减,而是让程序再次扫描时处理。
    反之,如果第一次从前往后扫描,同样可以按照这个方法分析,就不能保证整体的有序性。

    2.2 为什么从前往后扫描的判断条件不能是小于x而必须是小于或等于x?

    如果是小于x,那么第一次从前往后扫描时,就会将左边缘的基准值A[lo]记录下来,参与交换,这样如果一次排序结束,是直接交换先前保留的x的值和A[i]的值,那么会出现A[i]的值被覆盖为x的值,而之前x的值已经参与了交换,这样x的值就会重复出现。如果一次排序结束,是交换A[lo]和A[i]的值,那么会出现最终停在的i = j的位置上并不是之前选定的基准值x,而是其他元素。这样就出现了A[lo…i-1]各元素的值都小于那个其他元素,A[i+1…hi]各元素的值都大于那个其他元素,而不是基准值。

  • 相关阅读:
    shell 表达式
    manjaro 换源到中国并按照速度排序
    ORA-01950:对表空间 'USERS' 无权限
    normal 普通身份 sysdba 系统管理员身份 sysoper 系统操作员身份 dba和sysdba
    学生选课数据库SQL语句练习题
    多线程编程
    补充知识点
    输入输出
    集合作业
    银行(1)0925
  • 原文地址:https://www.cnblogs.com/Wangzhike/p/4912269.html
Copyright © 2011-2022 走看看