[C描述算法入门]快速排序(上篇)

105 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情

前言

        快速排序是常见排序的一种,属于交换排序,本文就来简单分享一波笔者的学习经验与心得,这是上篇,主要介绍快排的hoare版本,更多的内容请期待下篇。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

快速排序

        快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

 // 假设按照升序对array数组中[left, right)区间中的元素进行排序
 void QuickSort(int array[], int left, int right)
 {
     if(right - left <= 1)
         return;
     // 按照基准值对array数组的 [left, right)区间中的元素进行划分
     int div = partion(array, left, right);
     // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
     // 递归排[left, div)
     QuickSort(array, left, div);
     // 递归排[div+1, right)
     QuickSort(array, div+1, right);
 }

        上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时想想二叉树前序遍历规则即可快速写出来,只需分析如何按照基准值来对区间中数据进行划分的方式即可。

        将区间按照基准值划分为左右两半部分的常见方式有三种(本篇讲的是第一种,剩下的在下篇)。

        (以下都是基于要排升序来构思的)

hoare版本

基础讲解

        选定一个key值,然后让L和R分别从数组头部和尾部出发,向中间移动,L找比key大的值,R找比key小的值。

hoare

如何保证相遇位置的值比key的值要小?

        先说一下L和R怎样才会停下来:要么遇到较大值/较小值,要么就是L和R相遇。

        只需要把左边第一个元素作为key,并且让右边的R先走(L和R可不是同时开始走的,必然要有一个先走)。

有两种情况:

  1. R遇到较小值停下来了,接着是L走,L一直找不到较大值而与R相遇,此时相遇位置的值要比key小。
  2. L遇到较大值停下来了,R和L位置的值交换,下一轮开始时注意是R先走,此时L还停着(停着的位置的值在先前的交换中已经变成了较小值了),R一直找不到较小值而与L相遇,此时相遇位置的值要比key小。

image-20220907191655034

image-20220907191709766

        同理,如果要把右边第一个元素作为key,则需要让左边的L先走才能保证相遇位置的值比key的值要大。

代码实现

        由于我们是要交换数组元素的,key是临时变量的话就不能让key和数组元素交换值,所以可以用下标keyi来搞。

        不仅外层while条件是left < right,里面两个while也要加上这个条件,为什么?为了防止找着找着L和R就错过了,也作为L和R相遇的判断条件。同时也要注意是>=和<=,要把相等的情况筛选掉,因为我们要找的是较大值和较小值,如果遇到相等值也停下来的话就会产生干扰。

 void Swap(int* px, int* py)
 {
     int tmp = *px;
     *px = *py;
     *py = tmp;
 }
 ​
 int PartSort_1(int* arr, int left, int right)
 {
     assert(arr);
     
     int keyi = left;
     while(left < right)
     {
         while(left < right && arr[right] >= arr[keyi])
             --right;
         while(left < right && arr[left] <= arr[keyi])
             ++left;
         if(left < right)//不是因为相遇而停下来才交换,若是相遇就要出循环
             Swap(&arr[left], &arr[right]);
     }
     
     int meeti =  left;
     Swap(&arr[meeti], &arr[keyi]);
     
     return meeti;//返回相遇位置的下标是为了后续分割子区间
 }

意义

        这只是单趟的排序,它的意义何在呢?

  1. key对应的值已经排好了(排好了一个值)。
  2. 同时分割出以key为基准的左右两个子区间。如果子区间有序了,整体就有序了。这就涉及到了子问题的递归。

递归实现

        实际上在分割出左右子区间后就要用到递归来解决问题了,而这么一个递归的过程用二叉树来描述大概就是这个样子:

image-20220907202017301

        meeti就是每次L和R相遇的的位置下标,左右子区间就为[left, meeti - 1]和[meeti + 1, right],要注意别漏掉了递归终止的条件:if(left >= right)return;如果左边界大于或等于右边界显然说明区间是空区间或者仅有一个元素,不应该再继续递归下去了,此时就要返回上一个函数。

代码实现

 void QuickSort(int* arr, int left, int right)
 {
     assert(arr);
     
     if(left >= right)
         return;
     
     int meeti = PartSort_1(arr, left, right);
     
     QuickSort(arr, left, meeti - 1);
     QuickSort(arr, meeti + 1, right);
 }

image-20220907211052767

        时间复杂度:O(nlogn)

        空间复杂度:O(logn)

        稳定性:不稳定

        快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序 。

        更多内容请期待下篇。


以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif