【经典排序算法】6. 快速排序

快速排序是冒泡排序的一种改进版本,通过多次比较和交换基准值两边的数来实现排序。

时间复杂度分析:
快速排序和归并排序一样采用了分治法的设计思想。如果根据基准值,每次划分的两个子数组包含的元素数量是相同的,那么快排的时间复杂度为O(nlogn)。最坏情况下,如果根据基准值,每次划分的两个子数组其中一个总是只有一个元素,那快排的最坏情况的时间复杂度为O(n²)(给顺序数组排列,退化为冒泡排序)。因为在每一次划分的时候,都让一边只包含一个元素的情况是几乎不可能发生的,所以快排的平均时间复杂度是O(nlogn)。

空间复杂度:
快速排序在每次分割的过程中,需要 1 个空间存储基准值。而快排大概需要将数组分割logn次,所以占用空间也是 logn 个。

稳定性:
相等元素可能会因为分区而交换顺序,所以它是不稳定的算法。

应用:
快排的性能在顺序性越差的数据中表现越好,甚至可以比归并排序要好。这是因为虽然快速排序跟归并排序的平均时间复杂度都是O(nlogn),但是快速排序的O(nlogn) 记号中隐含的常数因子很小。

基础版

代码如下:

public class Main {

    public static void main(String[] args) {
        int[] arr = {3, 3, 5, 6, 2, 1};
        System.out.print("排序前:");
        arrPrint(arr);
        QuickSort(arr);
        System.out.print("排序后:");
        arrPrint(arr);
    }

    // 快速排序
    // 快速排序和归并排序一样采用了分治法的设计思想。
    // 把大问题分解成小问题,把大数组分解成小数组。
    //
    // 调用快速排序的递归函数,左索引记为left,初始化为0,
    // 右索引记为right,初始化为arr.length - 1。
    private static void QuickSort(int[] arr) {
        quickSort(arr, 0, arr.length - 1);
    }

    // 快速排序的递归函数
    // 递归终止条件为左索引>=右索引。不满足终止条件时:
    // 调用基准值分割函数partition,得到分割后的索引mid,
    // mid-1即为分割数组后的左子数组的终点,mid+1即为分割数组后右子数组的起点
    // 递归调用快速排序quickSort,对左子数组进行快速排序,
    // 递归调用快速排序quickSort,对右子数组进行快速排序。
    //
    // 想看中间输出的可以在partition函数后面使用arrPrint(arr)来打印
    private static void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            int mid = partition(arr, left, right);
            quickSort(arr, left, mid - 1);
            quickSort(arr, mid + 1, right);
        }
    }

    // 基准值分割函数partition
    // 选取基准值pivot后,循环使用双指针寻找左边比pivot大的数arr[l],
    // 右边比pivot小的数arr[r],并交换arr[l] arr[r]的位置。
    // 使得在r和l相遇位置的左半边的数不大于pivot,而在右半边的数则不小于pivot,
    // 最后把pivot交换到r和l的相遇位置,即可补全空间意义上真正的基准值分割。
    // 此时pivot的位置(r和l的相遇位置)将数组分割为了两边。左半边总是不大于右半边的数字。
    // 之后再利用分治法递归地调用partition,继续基准值分割左右子数组即可完成整个快排。
    //
    // 选取基准值pivot(默认arr是随机排序,所以直接取arr[left]),
    // 将左指针初始化为left + 1,右指针初始化为right,
    // 满足l小于r时(双指针没有超过遍历边界时)执行第一层while循环:
    // 第2层第1个while: 如果左指针l没有超过边界,且遍历元素arr[l]不大于pivot,
    //                  则左指针l循环右移,直到找到从左往右第一个比pivot大的遍历数arr[l]。
    // 第2层第1个while:同理,如果右指针r没有超边界,且遍历元素arr[r]不小于pivot,
    //                  则右指针r循环左移,直到找到从右往左第一个比pivot大的遍历数arr[r]。
    // 如果此时l依然满足小于r(双指针没有过界),则将arr[l] arr[r]交换位置,
    // l和r相遇后,所有while结束,此时将pivot交换到r与l的相遇位置。
    // pivot记录了arr[left]的值,所以先用arr[r]把arr[left]覆盖掉,
    // 再把pivot放到arr[r]上,完成最后交换,补全空间意义上真正的基准值分割,
    // 此时pivot的位置(r和l的相遇位置)将数组分割为了两边,左半边总是不大于右半边的数字。
    private static int partition(int[] arr, int left, int right) {
        int pivot = arr[left];
        int l = left;
        int r = right;
        while (l < r) {
            while (l <= r && arr[l] <= pivot)
                l++;
            while (l <= r && arr[r] >= pivot)
                r--;
            if (l < r)
                swap(arr, l, r);
        }
        arr[left] = arr[r];
        arr[r] = pivot;
        return r;
    }

    // partition中的交换元素位置函数
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 辅助函数:将int[] 打印出来
    private static void arrPrint(int[] arr) {
        StringBuilder str = new StringBuilder();
        str.append("[");
        for (int v : arr) {
            str.append(v + ", ");
        }
        str.delete(str.length() - 2, str.length());
        str.append("]");
        System.out.println(str.toString());
    }
}

该实例的快速排序动画演示如下:(省略了基准值分割函数partition)
【经典排序算法】6. 快速排序_第1张图片

基准值分割函数动画演示如下(动图有误,l应该从left开始):

【经典排序算法】6. 快速排序_第2张图片

优化1

加入随机位置基准值。

在50万个元素的数组中进行排序,优化前:78毫秒;优化后:114毫秒

可以看到耗时反而增加了,这种优化不是我们想要的。

public class Main {

    public static void main(String[] args) {
        // 生成500000个数的数组,数字分布在-3000到3000范围内
        int[] arr = randomArray(500000, 3000);

        arrPrint(arr);
        long begin = System.currentTimeMillis();
        QuickSort(arr);  // 优化前:78毫秒;优化后:114毫秒
        long end = System.currentTimeMillis() - begin;
        arrPrint(arr);
        System.out.println("耗时:" + end + "毫秒");
    }

    public static void QuickSort(int[] arr) {
        quickSort(arr, 0, arr.length - 1);
    }

    private static void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            int mid = partition(arr, left, right);
            quickSort(arr, left, mid - 1);
            quickSort(arr, mid + 1, right);
        }
    }

    private static int partition(int[] arr, int left, int right) {
        int l = left;
        int r = right;
        Random random = new Random();  // 优化
        swap(arr, left, left + random.nextInt(right - left));  // 优化:加入随机位置基准值
        int pivot = arr[left];
        while (l < r) {
            while (l <= r && arr[l] <= pivot)
                l++;
            while (l <= r && arr[r] >= pivot)
                r--;
            if (l < r)
                swap(arr, l, r);
        }
        arr[left] = arr[r];
        arr[r] = pivot;
        return r;
    }

    // 辅助: 交换函数
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 辅助:打印int[]
    private static void arrPrint(int[] arr) {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int nums: arr) {
            sb.append(nums + ", ");
        }
        sb.delete(sb.length() - 2, sb.length());
        sb.append("]");
        System.out.println(sb.toString());
    }

    // 辅助:随机数数组生成器
    private static int[] randomArray(int size,int value) {
        int[] arr = new int[size];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int)(Math.random()*(value + 1)) - (int)(Math.random()*value);
        }
        return arr;
    }
}

参考:

https://blog.csdn.net/qq_28063811/article/details/93199834

你可能感兴趣的:(#,图解十种排序算法,算法与数据结构,指针,快速排序,排序算法,数据结构)