Java 快速排序

前言

快速排序是在面试中最常见的问题之一,如果有幸问到快排,面试官通常都要求应聘者在纸上手写出快排的代码。本人在最近的一次面试中就被要求手写快排,本来觉得快排的思想早已烂熟于心了,随便写一个出来没什么问题。但是当面前坐着一个面试官,并且要在15分钟内推导出一个无误的快排出来时,对我个人来说,但是还是非常紧张的,最终提交的答案还是存在瑕疵。

由此萌生了通过博客记录面试中基本问题的想法,一是给自己归纳总结,二是希望能够给阅读博客的童鞋也提供一些参考。

本文的目的是想要记录快速排序简洁、优雅的实现方式,并且探讨快速排序的一些问题。并且参考了各类书籍与博客,寻找觉得最简洁、最好理解的实现方式。

思路

快速排序运用了分治的思想,所以是以递归的方式实现。每一次排序都会选出一个基数,排序过后整个数组以基数所在位置为分界线将数组切分为2个部分:左边的元素不大于基数,右边的元素不小于基数。随后再对除了基数以外的左右两部分再次调用排序,直至整个数组有序。

![快排思想](https://algs4.cs.princeton.edu/23quicksort/images/partitioning-overview.png)
```c++ void quickSort(int arr[], int lo, int hi){ if(!arr || lo < 0 || lo >= hi) return; int mid = partition(arr, lo, hi); quickSort(arr, lo, mid - 1); quickSort(arr, mid + 1, hi); } ```

切分

快速排序的核心就是怎么完成数组切分了,思路也很简洁:

  1. 选择基数(可以直接选择第一个元素),设置前后两个下标指针。
  2. 前指针从前往后扫描数组,遇到大于等于基数的元素停下
  3. 后指针从后往前扫描数组,遇到小于等于基数的元素停下
  4. 交换两个指针元素内容,重复步奏2,当前后两个指针重叠或者后指针处于前指针之前时停止迭代
  5. 最后交换基数与后指针元素,返回后指针位置

实现代码如下

void partition(int arr[], int lo, int hi){
    int start = lo;
    int end = hi + 1;
    int value = arr[lo];/*arr[lo]为基数*/
    while(true){
        while(arr[++start] < value && start < hi);/*边界:start不会超过hi*/
        while(arr[--end] > value);/*由于value的取值为arr[lo],end不会小于lo*/
        if(start >= end) break;/*循环终止条件*/
        swap(&arr[start], &arr[end]);
    }/*循环终止时arr[end]为数组中最后一个不大于value的元素*/
    swap(&arr[lo], &arr[end]);/*基数arr[lo]与arr[end]交换后处于切分位置*/
    return end;
}

要特别注意边界问题,切分代码非常容易在边界问题上出错,下图展示了一次具体的切分过程

![快排思想](https://algs4.cs.princeton.edu/23quicksort/images/partitioning.png)

深入思考

尽管快速排序有很多优点,但是上述实现有一个潜在缺陷:如果数组切分不均匀,排序过程将会极为低效,极端的情况就是每次切分导致左右两边有一边元素个数始终为0,快排将退化为选择排序。一种解决方法就是在执行快速排序之前对数组进行一次乱序。

小数组排序:由于快速排序使用递归,对于比较小的数组不如直接使用插入排序快,有一种优化方式是在快速排序中添加判断,当数组长度小于预设值时使用插入排序。

数组中存在大量重复元素:由于数组中有大量重复元素,基本快速排序算法会将这些重复元素并入字数组中继续排序而降低效率,如果在切分时就把与基数相同的元素找到,不参与下一次的快排,性能能够得到很大的提升,这就引入了三取样切分的快速排序,有兴趣的同学可以自行搜索一下

主要参考链接https://algs4.cs.princeton.edu/home/

原文地址:https://www.cnblogs.com/migoo/p/8548618.html