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]各元素的值都大于那个其他元素,而不是基准值。

  • 相关阅读:
    CSS之旅——第二站 如何更深入的理解各种选择器
    CSS之旅——第一站 为什么要用CSS
    记录一些在用wcf的过程中走过的泥巴路 【第一篇】
    asp.net mvc 之旅—— 第二站 窥探Controller下的各种Result
    asp.net mvc 之旅—— 第一站 从简单的razor入手
    Sql Server之旅——终点站 nolock引发的三级事件的一些思考
    Sql Server之旅——第十四站 深入的探讨锁机制
    Sql Server之旅——第十三站 对锁的初步认识
    Sql Server之旅——第十二站 sqltext的参数化处理
    Sql Server之旅——第十一站 简单说说sqlserver的执行计划
  • 原文地址:https://www.cnblogs.com/Wangzhike/p/4912269.html
Copyright © 2011-2022 走看看