• 一起学数据结构(11)——快速排序及其优化


         上篇文章中,解释了插入排序、希尔排序、冒泡排序、堆排序及选择排序的原理及具体代码实现本片文章将针对快速排序,快速排序的几种优化方法、快速排序的非递归进行解释。

    目录

    1. 快速排序原理解析以及代码实现:

    2. 如何保证相遇位置的值一定小于所对应的值:

    3 优化最坏情况下快速排序的时间复杂度:

    4. 对于快速排序代码书写格式的优化(挖坑法):

    5. 对于快速排序代码的另一种书写格式优化(前后指针法): 


    1. 快速排序原理解析以及代码实现:

    给定下面一个数组:

           对于快速排序,其中心思想就是首先选取一个值,一般选择数组中最左边的数据,例如本数组中的6。创建一个变量,来保存这个值所对应的下标,为了方便表达,将这个变量命名为key

          在确定了key之后,分别从两头遍历数组,定义两个变量用于遍历数组,其中,left=0right=n-1

         对于遍历数组的过程,需要分成两种情况来查看。其中,left用于从左向右遍历数组,right用于从右向左遍历数组。当数组从左向右进行遍历时,一旦遇到比key所对应的值大的数据,则停left在这个数据的所对应的下标处。即:

    当数组从右向左进行遍历时,一旦遇到比key所对应的值小的数据,则right停在这个数据的所对应的下标处。即:

    在找到了符合上面规定的值后,交换两个值在数组中的位置:

    接着继续向下遍历,并且重复之前的动作。当leftright相遇时,停止循环,此时的数组可以表示为:

    最后,将key所对应的值,与此时left或者right所对应的值进行一次交换,便完成了整个流程,用图形表示为:

     在进行了上面一整套流程后,此时的数组虽然还是无序,但是可以观察到,key所对应的值的右半部分的数组的值都大于key所对应的值。key左半部分数组的值都小于key所对应的值。

    对于上述流程,可以用下面的代码进行表示:

    1. //部分快排
    2. int PortSort(int* a, int left, int right)
    3. {
    4. int key = left;
    5. while (left < right)
    6. {
    7. //先从右边开始找小的
    8. while( left < right && a[right] >= a[key])
    9. {
    10. right--;
    11. }
    12. //再从左边开始找大的
    13. while(left < right && a[left] <= a[key])
    14. {
    15. left++;
    16. }
    17. //交换找到的值
    18. Swap(&a[left], &a[right]);
    19. }
    20. Swap(&a[key], &a[left]);
    21. return left;
    22. }

    对于上述给出的代码中,需要注意两个点:

    1.在进行遍历数组寻找值的时候,必须先从右边开始遍历找小于key所对应的值的数据,再从左边找大于key所对应的值的数据,对于原因将会在文章的后面进行探讨

    2.在遍历寻找值的while循环中,需要注意循环的两个条件,即left < right并且a[right] >= a[key],前面的条件是为了防止下面的两种情况:若从左向右遍历时,不存在比key对应的值大的数据,从右向左遍历时,不存在比key对应的值小的数据。以上两种情况均会导致left,right的范围超出数组下标的范围,对于第二个条件,如果不加上=,一旦在遍历的过程中,遇到了与key对应的值相等的值,会造成死循环。   

           完成了上述步骤后,数组依然是无序的,为了处理数组中其他的数据,需要利用到类似二叉树中递归的思想来实现:

           对于上述数组,他的数组左半部分的数据都是小于key所对应的值的,对于数组的右半部分的数据都是大于key所对应的值的,因此在下面的递归中,需要以key为分界线,key的左半部分为一组,右半部分为一组,对这两组值再次进行一次上面给出代码所对应的操作。

         例如,对于左半部分:

    此时key所对应的值为3,按照上述代码进行操作后,数组可以表示为:

    随后,再以key为分界线,分出左右部分,对于左右部分进行上述代码的操作,对于左半部分,进行一次操作后,可以表示为:

           此时,再向下进行递归,key的左半区间不存在,key的右半区间只有一个值。因此,对于上面两种情况,视作递归结束的条件。

          上面只是展示了每一次的递归中,数组的左半部分的情况,对于右半部分原理相同,这里不再进行赘述。当作伴部分,右半部分的递归都结束后,整个区间会变成有序的区间,即:

    对于上述递归,可以用下列代码进行实现:

    1. void QuickSort(int* a, int begin, int end)
    2. {
    3. if (begin >= end)
    4. return;
    5. int keyi = PortSort(a, begin, end);
    6. QuickSort(a, begin, keyi - 1);
    7. QuickSort(a, keyi + 1, end);
    8. }

    测试函数如下:

    1. void TestQuickSort()
    2. {
    3. int e[] = { 6,1,2,7,9,3,4,5,10,8 };
    4. int size = sizeof(e) / sizeof(int);
    5. QuickSort(e, 0,size-1);
    6. printf("快速排序:");
    7. ArrayPrint(e, size);
    8. }

    结果如下:

    2. 如何保证相遇位置的值一定小于key所对应的值:

           上面说到,在进行遍历寻找值这一步骤时,一定要先从右边开始向左遍历来找到比key对应的值小的值,再从左边向右开始遍历,来寻找比key对应值大的值,原因如下:

    例如对于上面给出的数组,对于left,right相遇的前一步情况:

    假设左边先进行遍历去寻找值,再从右边向左遍历来寻找值,则二者相遇的位置为:

    此时,如果按照上面代码的内容对key对应的值和left位置对应的值进行交换,则:

    此时,比key对应的值大的值不只是只存在右边。 

    这里需要注意,先从右边往左边遍历的,对应的是key的位置在数组的左端。当key在数组的右端时,需要先从左边向右遍历。

    3 优化最坏情况下快速排序的时间复杂度:

    快速排序的时间复杂度一般都认为是O(nlogn),但是对于下面的一种情况:

    前面说到,在快速排序中,当key取左端的值时,应该优先从右边开始遍历,对于例子中这种完全升序,或者说大部分区间都是升序的情况,每次从右端进行遍历时,都必须遍历到数组的最左边。因此,对于一个有n个数据的这样的数组来说,从右向左遍历的次数为n-1+n-1+n-3+.......次,这种情况下的快速排序的时间复杂度为O(n^2)。为了优化这种情况,这里引入三数取中的方法。代码如下:因为原理就是简单的两数相比,所以不做过多解释:

    1. //三数取中
    2. int GetMidi(int* a, int left, int right)
    3. {
    4. int mid = (left + right) / 2;
    5. if (a[left] < a[mid])
    6. {
    7. if (a[right] > a[mid])
    8. {
    9. return mid;
    10. }
    11. else if (a[left] > a[right])
    12. {
    13. return left;
    14. }
    15. else
    16. {
    17. return right;
    18. }
    19. }
    20. else//(a[left] > a[mid];
    21. {
    22. if (a[mid] > a[right])
    23. {
    24. return mid;
    25. }
    26. else if (a[left] < a[right])
    27. {
    28. return left;
    29. }
    30. else
    31. {
    32. return right;
    33. }
    34. }
    35. }

     在构建了三数取中函数之后,需要对之前的快速排序代码进行修改。修改的部分为:首先在函数PortSort的开头创建一个变量mid用于接受三数取中函数GetMidi的返回值。接着,让mid对应的数值与后续key下标对应的数据(即最左端,或者最右端)用交换函数交换即可。

    4. 对于快速排序代码书写格式的优化(挖坑法):

    对于挖坑法,具体的实现原理如下:

    首先还是确认key以及key所对应的值,例如在下面的例子中

    (注:为了方便演示原理,下面的情况不包括三数取中,但是在书写代码时,使用三数取中的方法与上方的使用发放相同)

           首先确定一个key,这里按照左端处理,创建一个变量key,令key = 6,接着,确定key所在的下标就是第一个坑位。在确认了第一个坑位后,与上面的快速排序相同,都需要先从右端开始遍历来找到比key小的值,即:

    随后,直接让right所指向的值覆盖到hole的位置,再让right指向的位置变成新的坑,即:

     再从左边向右进行遍历,找到比key小的值,并且让这个值覆盖到坑位hole中,再令下图中的left成为新的坑位:

     继续从右向左进行遍历,再从左向右遍历,直到left,right相遇为止,即:

    最后,再令二者相遇的位置赋值key即可。然后再按照上面的思想递归即可。代码实现为:
     

    1. //快排优化(挖坑法)
    2. int QuickDigSort(int* a, int begin, int end)
    3. {
    4. int mid = GetMidi(a, begin, end);
    5. Swap(&a[begin], &a[mid]);
    6. int key = a[begin];
    7. int hole = begin;
    8. while (begin < end)
    9. {
    10. //先从右边开始找小的
    11. while (begin < end && a[end] >= key)
    12. {
    13. end--;
    14. }
    15. a[hole] = a[end];
    16. hole = end;
    17. //再从左边开始找大的
    18. while (begin < end && a[begin] <= key)
    19. {
    20. begin++;
    21. }
    22. a[hole] = a[begin];
    23. hole = begin;
    24. }
    25. a[hole] = key;
    26. return hole;
    27. }
    28. void QuickSort2(int* a, int begin, int end)
    29. {
    30. if (begin >= end)
    31. return;
    32. int keyi = QuickDigSort(a, begin, end);
    33. QuickSort2(a, begin, keyi - 1);
    34. QuickSort2(a, keyi + 1, end);
    35. }

    测试函数如下:

    1. void TestQuickDigSort()
    2. {
    3. int f[] = { 6,1,5,3,9,10,7,4,2,8 };
    4. int size = sizeof(f) / sizeof(int);
    5. QuickSort2(f, 0, size - 1);
    6. printf("快速排序(挖坑法):");
    7. ArrayPrint(f, size);
    8. }

    运行结果如下:

    5. 对于快速排序代码的另一种书写格式优化(前后指针法): 

            对于前后指针法,就是通过控制两个指针,这里分别命名为prev,cur。通过控制这两个指针的动作时序来完成对于数组的排序,中心思想如下:

           对于指针cur的作用,就是用来寻找比key小的值( key的意义与上面相同),对于指针prev的动作时序,分为下面两种情况: 当cur没有遇到比key大的值时,prev一直紧跟着cur,当cur遇到比key大的值后,令prev的位置处于这个值的前面。

           继续令cur向后遍历,如果又找到了比key小的值,则交换这个值,与prev后面的值。

    例如对于下面的数组:

    按照上面所说的思想,在cur没有遇到比key大的值时,prev一直紧跟着cur运动,即:

    此时,再向下运动,cur遇到了比key大的值,令cur继续向后遍历,直至找到比key小的值,令prev停在比key大的值的前面,即:

    再向下运动后,cur遇到了比key小的值,令prev后面的值与比key小的值交换,即:

    接着继续向下遍历,此时prev,cur指向的位置为:

    此时再对两个指针指向的值进行交换,即:

    在遍历结束时,数组如下:

     在结束遍历后,交换prev位置的值与key的值,即:

    交换完后,比key小的值都位于prev左边,比 key大的值都位于prev右边。

    总览上面的整个过程,当遇到了比key大的值后,两个指针便开始拉开差距。此时cur每次向后遍历,都会找到一个比key大的值,并且形成一个比key大的值的区间,在其期间,prev一直保持不动,直到cur遇到一个key小的值,prev在向后指向第一个比key大的值并且交换,此后cur每找到一个key小的值,都会把之前key大的值置换到后面,把key小的值置换到前面。

    代码如下:

    1. //快速排序(前后指针法)
    2. int QuickTailSort(int* a, int begin, int end)
    3. {
    4. int key = begin;
    5. int prev = begin, cur = begin+1;
    6. while (cur <= end)
    7. {
    8. if (a[cur] < a[key])
    9. {
    10. Swap(&a[++prev], &a[cur]);
    11. }
    12. cur++;;
    13. }
    14. Swap(&a[key], &a[prev]);
    15. return prev;
    16. }

    测试函数如下:

    1. void TestQuickTailSort()
    2. {
    3. int g[] = { 5,1,6,9,3,10,4,7,2,8 };
    4. int size = sizeof(g) / sizeof(int);
    5. QuickSort3(g, 0, size - 1);
    6. printf("快速排序(挖坑法):");
    7. ArrayPrint(g, size);
    8. }

    运行结果如下:

    6. 快速排序的非递归实现: 

           对于上述的递归方式是借助了二叉树的思想实现的,对于递归来说,一旦需要处理的数据过多,导致递归的深度过多,很有可能会导致栈溢出。所以,对于快速排序的非递归写法的掌握是十分必要的。具体思路如下:

            在前面的递归中,每次递归都是会选取整个数组中一部分区间进行排序。所以,对于区间的存储,是需要第一个解决的问题。文章本处选择使用栈来存储需要排序的区间,具体思路如下:

    给定一个数组,其数组的内容以及下标如图所示:

    栈如图所示:

     首先,将数组最左边的下标和最右边的下标压入栈,即:

    此时,将这两个元素取出,并且按照此范围进行一次快速排序(只是单次处理,不需要递归)的处理,定义keyi接受快速排序的返回值,并且按照(left,keyi-1),(keyi+1,right)将数组划分为两个区间,再次将这两个区间的左右端下标压入栈,随后重复上述过程,代码表示为:
     

    1. //快速排序(非递归)
    2. void QuickSortNonR(int* a, int begin, int end)
    3. {
    4. ST st;
    5. STInit(&st);
    6. //先将最左,最右两端的下标压入栈
    7. //注意顺序,后进先出
    8. STPush(&st, end);
    9. STPush(&st, begin);
    10. while (!STEmpty(&st))
    11. {
    12. //取出这两个压入栈的元素
    13. int left = STTop(&st);
    14. STPop(&st);
    15. int right = STTop(&st);
    16. STPop(&st);
    17. int keyi = QuickDigSort(a, left, right);
    18. if (keyi + 1 < right)
    19. {
    20. STPush(&st, right);
    21. STPush(&st, keyi + 1);
    22. }
    23. if (left < keyi - 1)
    24. {
    25. STPush(&st, keyi - 1);
    26. STPush(&st, left);
    27. }
    28. }
    29. STDestory(&st);
    30. }

    测试函数为:

    1. void TestQuickSortNonR()
    2. {
    3. int h[] = { 10,1,9,5,7,3,4,2,8,6 };
    4. int size = sizeof(h) / sizeof(int);
    5. QuickSortNonR(h, 0, size - 1);
    6. printf("快速排序(非递归):");
    7. ArrayPrint(h, size);
    8. }

    运行结果为:

  • 相关阅读:
    buuctf(探险2)
    <蓝桥杯软件赛>零基础备赛20周--第4周--杂题-1
    【数据结构】堆及堆排序详解
    python3 -- json档案处理
    Android PackageManager 基本使用
    我的 Kafka 旅程 - Consumer
    Greenplum数据库源码分析——Standby Master操作工具分析
    基于SpringBoot的家电销售电商管理平台
    JS 常用方法合集
    干货!用于多模态深度图超分辨率的离散余弦变换网络
  • 原文地址:https://blog.csdn.net/2301_76836325/article/details/133973291