vlambda博客
学习文章列表

加餐--快速排序 Java 版

很多面试题的解答都是一排序为基础的,如果我们写出一个 O( ) 的算法,大概率要被挂,今天写个快排的基础文章,后面看情况再把归并和堆排序写一写,至于选择排序、冒泡排序这种时间复杂度高的就不写了,有兴趣的可以找书自己看一下。

算法的实现是用 Java 写了一个比较简单的实现方法,方便大家理解。

快速排序的思想

快速排序算法首先会在序列中随机选择一个基准值(pivot),然后将除了基准值之外的数分为 "比基准值小的数" 和 "比基准值大的数" 这两个类别,再将其排列成以下形式。

【比基准值小的数】 基准值 【比基准值大的数】

接着,继续对两个序列 "【】"中的数据进行排序之后,整体的排序便完成了。对基准值左右两侧的序列排序时,同样也会使用快速排序。

快速排序是一种"分治法",将原本的问题分解成两个子问题—— 比基准值小的数和比基准值大的数,然后再分别解决这两个子问题。解决子问题的时候会再次使用快速排序,只有在子问题里只剩下一个数字的时候,排序才算完成。

快排的过程

下面我们用示意图更好地理解一下快速排序对一个序列进行排序的过程。

图例出自—《我的第一本算法书》

假定有如下待排序序列

加餐--快速排序 Java 版
待排序序列

首先在序列中随机选择一个基准值,这里选择了 4。

加餐--快速排序 Java 版
选择基准值 pivot

将其他数字和基准值进行比较,小于基准值的往左移,大于基准值的往右移。

首先比较第一个元素 3 和基准值4,因为 3 < 4, 所以将 3放在基准值的左边。

加餐--快速排序 Java 版
首先比较 3 和基准值4,因为 3 < 4, 所以将 3放在基准值的左边

接下来,比较 5 和基准值,因为 5 > 4,所以将 5 放在基准值的右边。

加餐--快速排序 Java 版
5 > 4, 将5放在基准值右边

对整个序列进行同样操作后,所有小于基准值的数字全都放到了基准值的左边,大于的则全都放在了右边。

加餐--快速排序 Java 版
一轮排序完成后的结果
加餐--快速排序 Java 版
把基准值放入序列

现在排序就分成了两个子问题,分别再对基准值左边和右边的数据进行排序。

加餐--快速排序 Java 版
分解成了两个子问题

两边的排序操作也和前面的一样,也是使用快排算法,选取基准值,把小于的数字放左边大于的放右边。

加餐--快速排序 Java 版
对子序列使用快速排序

子问题有可能会再分解成子问题,直到子问题里只剩下一个数字,再也无法分解出子问题的时候,整个序列的排序才算完成。

加餐--快速排序 Java 版
排序完成

因为快速排序算法在对序列进行排序的过程中会再次使用该算法,所以快速排序算法在实现时需要使用"递归”来实现。

快速排序的 Java 实现

下面上一个用 Go 版本的快速排序算法的简单实现

import java.util.Arrays;

class Solution {
    public static void main(String[] args) {
        int[]  randomIntsArray = new int[]{9341245563126984698245643219};
        quickSort(randomIntsArray, 0, randomIntsArray.length - 1);
        System.out.println(Arrays.toString(randomIntsArray));

    }

    public static void quickSort(int[] sequence, int low, int high) {
        if (high <= low) {
            return;
        }
        int j = partition(sequence, low, high);
        quickSort(sequence, low, j - 1);
        quickSort(sequence, j + 1, high);
    }

    public static int partition(int[] sequence, int low, int high) {
        int i = low + 1;
        int j = high;
        while (true) {
            while (true) {
                // 低坐标 i 从前往后访问序列,如果位置上的值大于基准值,停下来。
                // 准备和高坐标 j 访问到的小于基准值的值交换位置
                if (sequence[i] < sequence[low]) {
                    i++;
                } else {
                    break;
                }

                if (i >= high) {
                    break;
                }
            }

            while (true) {
                // 高坐标 j 从后往前访问序列,如果位置上的值小于基准值,停下来。
                // 和低坐标 i 指向的大于基准值的值交换位置
                if (sequence[j] > sequence[low]) {
                    j--;
                } else {
                    break;
                }

                if (j <= low) {
                    break;
                }
            }

            if (i >= j) {
                // 小于基准值的已经都到了左边,大于基准值的已经都到了右边,结束循环
                break;
            }

            swapItem(sequence, i, j);
        }
        swapItem(sequence, low, j);

        return j;
    }

    public static void swapItem(int[] sequence, int i, int j) {
        int temp = sequence[i];
        sequence[i] = sequence[j];
        sequence[j] = temp;
    }
}

每一轮快速排序都会经历下面这几个步骤:

  1. 设置两个变量i、j,排序开始的时候:i=0,j=待排序序列长度 - 1。
  2. 以第一个数组元素作为基准值 pivot(也可以是最后一个元素,或者是随机的一个元素)。
  3. i 坐标从开始向后访问序列里的元素,即 i++,找到第一个大于 pivot 的位置 ,和 j 坐标访问到的小于基准值的值交换位置。
  4. j 坐标从末尾向前搜索,即j--,找到第一个小于 pivot 的位置,将i,j坐标上的值进行互换。
  5. 重复第3、4步,直到i=j,然后将 pivot 和 j 坐标上的值互换,完成一轮排序,小于 pivot 的值都放在了它的左边,大于的则放到了右边。

重复进行上面的过程,直到排序完成。

快速排序的时间复杂度

分割子序列时需要选择基准值,如果每次选择的基准值都能使得两个子序列的长度为原本的一半,那么快速排序的运行时间和归并排序的一样,都为 O(nlogn)。将序列对半分割 log2n 次之后,子序列里便只剩下一个数据,这时子序列的排序也就完成了。

因此,如果像下图这样一行行地展现根据基准值分割序列的过程,那么总共会有 log2n 行。

加餐--快速排序 Java 版
快排分解序列的次数

每行中每个数字都需要和基准值比较大小,因此每行所需的运行时间为 O(n)。由此可知,整体的时间复杂度为 O(nlogn)。

如果运气不好,每次都选择最小值作为基准值,那么每次都需要把其他数据移到基准值的右边,递归执行 n 行,运行时间也就成了 O( )。

所以真正应用的时候基准值的选取也比较讲究,比如以中位数做基准值:本轮排序序列的第一个、最后一个、中间位置三个数的中位数作为基准值进行排序。

今天的内容分享到这里就结束了,觉得不错还请给个关注。

文章的图片来自书籍《我的第一本算法书》,觉得内容不错了建议下单买一本,支持原作者的创作。