排序(六):快速排序法

一. 基本思想
    快速排序可以说是20世纪最伟大的算法之一了。相信都有所耳闻,它的速度也正如它的名字那样,是一个非常快的算法了。当然它也后期经过了不断的改进和优化,才被公认为是一个值得信任的非常优秀的算法。

在这里插入图片描述
在归并排序中,不管数组的内容是什么,直接将数组一分为二,不断归并;而快速排序则每次从当前数组中选择一个元素作为标定点,然后想办法将这个标定点挪到合适的位置,使其满足上图中所示的排列,然后再从两端继续进行上述操作,直到整个数组有序。

快速排序中的核心操作就是对数组进行分区。
在这里插入图片描述
假设我们每次选择数组的首元素作为标定点,即上图中的 l 位置对应的元素 v,j 是 <v 部分的末尾位置,i表示当前正在访问的元素,这里要注意区间的开闭。

下面讨论 i 位置元素值和 标定点比较:
在这里插入图片描述
e代表 i 位置的元素,如果 e > v,则只需要将 i++,继续下一个的比较即可;
在这里插入图片描述
如果 e < v,则需要将 e 与 j+1 位置的元素交换,同时 j++;i++;
重复上面的递归,直到 l == r。

二. 代码实现

  • 版本一:普通快速排序
    快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。
    快速排序最好的情况下是每次都正好能将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,复杂度为 O(NlogN)。
    最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。
package com.hong.sort;

/**
 * <br>快速排序</br>
 *  * 快速排序是不稳定的算法
 * Quick Sort也是一个O(nlogn)复杂度的算法
 */
public class QuickSort {

    /**
     * 分区.对arr[l...r]部分进行partition操作
     * 返回p,使得arr[l...p-1] < arr[p] < arr[p+1...r]
     *
     * @param arr
     * @param l
     * @param r
     * @return
     */
    public static int partition(int[] arr, int l, int r) {
        int v = arr[l]; // 将第一个元素值作为临界点
        //arr[l+1...j] < v ; arr[j+1...i) > v
        int j = l;
        for (int i = l; i <= r; i++) {
            if (arr[i] < v){
                /**
                 * 从第一个元素开始依次与临界点值比较,若发现 arr[i]< 临界点,
                 * 则j向前一位,则arr[j]就是当前第一个>v的值,交换i,j.
                 */
                swap(arr,i,++j);
            }
        }
        swap(arr, l, j);
        return j;
    }

    private static void swap(int[] arr, int i, int j) {
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

    public static void sort(int[] arr) {
        int n = arr.length;
        sort(arr, 0, n-1);
    }

    /**
     * 递归使用快速排序,对arr[l...r]的范围进行排序
     * @param arr
     */
    public static void sort(int[] arr, int l, int r) {
        if( l >= r ){
            return;
        }
        int p = partition(arr, l, r);
        sort(arr, l, p-1 );
        sort(arr, p+1, r);
    }
}
  • 版本二
    使用Insertion Sort优化快速排序。
    对于近乎有序的数组,在我们上面的快排中,时间复杂度可能退化为O(n^2)。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    归并排序可以保证每次都是平均的一分为二,而在上面的快速排序中因为每次都是选择第一个元素作为标定点,当整个数组本身是有序的情况下,就会退化为 O(n ^ 2)的复杂度。

改进:使用随机化防止Quick Sort降至O(n^2)

package com.hong.sort;

/**
 * <br>随机化快速排序法</br>
 *  */
public class QuickSort2 {
    /**
     * 分区.对arr[l...r]部分进行partition操作
     * 返回p,使得arr[l...p-1] < arr[p] < arr[p+1...r]
     *
     * @param arr
     * @param l
     * @param r
     * @return
     */
    public static int partition(int[] arr, int l, int r) {
        /**
         * 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
         */
        swap( arr, l , (int)(Math.random()*(r-l+1))+l );
        int v = arr[l];
        int j = l;
        for (int i = l; i <= r; i++) {
            if (arr[i] < v){
                j++;
                swap(arr,i,j);
            }
        }
        swap(arr, l, j);
        return j;
    }

    private static void swap(int[] arr, int i, int j) {
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

    public static void sort(int[] arr) {
        int n = arr.length;
        sort(arr, 0, n-1);
    }

    /**
     * 递归使用快速排序,对arr[l...r]的范围进行排序
     * @param arr
     */
    public static void sort(int[] arr, int l, int r) {
        // 对于小规模数组, 使用插入排序
        if( r - l <= 15 ){
            InsertionSort.sort(arr, l, r);
            return;
        }

        int p = partition(arr, l, r);
        sort(arr, l, p-1 );
        sort(arr, p+1, r);
    }
  • 版本三:双路快速排序
    若果数组中含有大量重复的元素,则partition很可能把数组划分成两个及其不平衡的两部分,时间复杂度退化成O(n²)。这时候应该把小于v和大于v放在数组两端。
    在这里插入图片描述
    在上面的快排中,默认是把 =v 的元素放到了右边,当然也可以改下代码使之放到左边,但不管那种放法,当数组中存在大量重复的元素时,将使分区的两边极度不平衡。
    在这里插入图片描述
    在这里插入图片描述
    同时从数组的两端开始扫描。i 从左往右,< v, i++; j 从右往左, > v, j–。注意这里的 i ,j 表示待扫描元素。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
package com.hong.sort;

/**
 * <br>双路快速排序法</br>
 * 在之前的快排中,对于与给定的标定点相同的重复元素都是放到了一边,
 * 这样会导致某一边会有可能有大量重复元素;
 * 为了解决这个弊端,双路快排会尽可能平均的将重复元素放到标定点的两边
 */
public class QuickSort2Ways {

    public static int partition(int[] arr, int l, int r) {
        swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
        int v = arr[l];
        // arr[l+1...i) <= v ; arr(j...r] >= v
        int i = l + 1;
        int j = r;
        while (true) {
            // 注意这里的边界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0
            while (i<= r && arr[i] < v) {
                i++;
            }
            // 注意这里的边界, arr[j].compareTo(v) > 0, 不能是arr[j].compareTo(v) >= 0
            while (j >= l+1 && arr[j] > v) {
                j--;
            }

            if (i > j) {
                break;
            }

            swap(arr, i, j);
            i++;
            j--;
        }

        swap(arr, l, j);
        return j;
    }

    private static void swap(int[] arr, int i, int j) {
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

    public static void sort(int[] arr) {
        int n = arr.length;
        sort(arr, 0, n - 1);
    }

    /**
     * 递归使用快速排序,对arr[l...r]的范围进行排序
     *
     * @param arr
     */
    public static void sort(int[] arr, int l, int r) {
        // 对于小规模数组, 使用插入排序
        if (r - l <= 15) {
            InsertionSort.sort(arr, l, r);
            return;
        }

        int p = partition(arr, l, r);
        sort(arr, l, p - 1);
        sort(arr, p + 1, r);
    } 
}
  • 版本四:三路快速排序
    数组分成三个部分,大于v 等于v 小于v
    在具有大量重复键值对的情况下使用三路快排
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
package com.hong.sort;

/**
 * <br>三路快速排序法</br>
 * 之前的快排中,都是把数组分成了两部分,<=v v >=v
 * 三路快排中, <v  ==v  >v
 * 三路快速排序处理 arr[l,r]
 * 将arr[l,r]分为<v ;==v ; >v 三部分
 * 之后递归对<v;>v两部分继续进行三路快排
 */
public class QuickSort3Ways {

    private static void swap(int[] arr, int i, int j) {
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

    public static void sort(int[] arr) {
        int n = arr.length;
        sort(arr, 0, n - 1);
    }

    /**
     * 递归使用快速排序,对arr[l...r]的范围进行排序
     *
     * @param arr
     */
    public static void sort(int[] arr, int l, int r) {
        // 对于小规模数组, 使用插入排序
        if (r - l <= 15) {
            InsertionSort.sort(arr, l, r);
            return;
        }

        // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot
        swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
        int v = arr[l];
        int lt = l; // arr[l+1...lt] < v
        int gt = r + 1; // arr[gt...r] > v
        int i = l + 1;

        while (i < gt) {
            if (arr[i] < v) {
                swap(arr, i, lt + 1);
                i++;
                lt++;
            } else if (arr[i] > v) {
                swap(arr, i, gt - 1);
                gt--;
            } else { // arr[i] == v
                i++;
            }
        }

        swap(arr, l, lt);
        sort(arr, l, lt - 1);
        sort(arr, gt, r);
    }

三. Merge Sort 和 Quick Sort 的衍生问题
Merge Sort 和 Quick Sort 都使用了分治算法。
在这里插入图片描述
在这里插入图片描述
Merge Sort的思路求逆序对的个数,算法复杂度:O(nlogn)。

取数组中第n大的元素:排序,算法复杂度:O(nlogn)
取数组中的最大值,最小值,遍历。算法复杂度:O(n)。

Quick Sort的思路求数组中第n大元素,算法复杂度:O(n)。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值