目录

  • 思路
  • 代码示例
  • 时间复杂度
  • 优化枢轴的选取
  • 优化不必要的交换
  • 优化递归
  • 完整代码


思路

快排的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

代码示例

package org.example.demo;

public class QuickSortTest {

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

    private static void sort(int[] arr, int low, int high) {
        // 枢轴的索引
        int pivotIndex;
        if (low < high) {
            // 将数组一分为二,并返回枢轴的索引
            pivotIndex = partition(arr, low, high);
            // 对低子表递归排序
            sort(arr, low, pivotIndex - 1);
            // 对高子表递归排序
            sort(arr, pivotIndex + 1, high);
        }
    }

    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[low];
        while (low < high) {
            while (low < high && arr[high] >= pivot)
                high--;
            swap(arr, low, high);
            while (low < high && arr[low] <= pivot)
                low++;
            swap(arr, low, high);
        }
        return low;
    }

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

    private static void print(int[] arr) {
        for (int i : arr) {
            System.out.print(i + " ");
        }
        System.out.println();
    }

    public static void testCase() {
        int[] arr = { 5, 1, 9, 3, 7, 4, 8, 6, 2 };
        sort(arr);
        print(arr);
    }

    public static void main(String[] args) {
        testCase();
    }

}

上述代码中最关键和最难理解的就是partition方法,它要做的就是选取一个关键字,比如第一个关键字50,然后想办法将它放到一个位置,使得它左边的值都比它小,右边的值都比它大,我们将这样的关键字称为枢轴(pivot)

时间复杂度

最优的情况下,时间复杂度为O(nlogn)。
最坏的情况下,时间复杂度为O(n²)。

优化枢轴的选取

如果我们选取的pivot是整个序列的中间值,那么此时递归的深度最小,性能也最好。上述代码中我们固定选取第一个关键字作为枢轴,显然不是很好,那么如何选取中间值呢?这是个难题。一般采用多数取中的方式选取枢轴,比如三数取中或九数取中,我们均匀的从序列中采样,然后选取样本中的中位数作为枢轴。下面是用三数取中选取枢轴改良过的partition方法:

private static int partition(int[] arr, int low, int high) {
    // 优化pivot的选取,最左、中间、最右三数取中
    // 因为下面的代码是让low位作为枢轴,所以这里让low位的值是三数的中间值
    int mid = low + (high - low >>> 1);
    if (arr[low] > arr[high])
        swap(arr, low, high);
    if (arr[mid] > arr[high])
        swap(arr, mid, high);
    if (arr[mid] > arr[low])
        swap(arr, low, mid);

    int pivot = arr[low];
    while (low < high) {
        while (low < high && arr[high] >= pivot)
            high--;
        swap(arr, low, high);
        while (low < high && arr[low] <= pivot)
            low++;
        swap(arr, low, high);
    }
    return low;
}

优化不必要的交换

我们发现5这个关键字,其位置变化是0、8、1、8、2、7、2、6、2、5、3、5、4,可其实它的最终目标就是4,当中的交换其实是不必要的。我们对partition方法的代码再进行优化,减少不必要的位置交换:

private static int partition(int[] arr, int low, int high) {
    // 优化pivot的选取,最左、中间、最右三数取中
    // 因为下面的代码是让low位作为枢轴,所以这里让low位的值是三数的中间值
    int mid = low + (high - low >>> 1);
    if (arr[low] > arr[high])
        swap(arr, low, high);
    if (arr[mid] > arr[high])
        swap(arr, mid, high);
    if (arr[mid] > arr[low])
        swap(arr, low, mid);

    int pivot = arr[low];
    while (low < high) {
        while (low < high && arr[high] >= pivot)
            high--;
        arr[low] = arr[high];
        while (low < high && arr[low] <= pivot)
            low++;
        arr[high] = arr[low];
    }
    arr[low] = pivot;

    return low;
}

优化递归

栈的大小是有限的,每次递归都会耗费一定的栈空间,方法的参数越多,每次递归耗费的空间也越多,所以递归对性能是有一定影响的。sort方法在其尾部有两次递归操作,我们对其进行尾递归优化:

private static void sort(int[] arr, int low, int high) {
    // 枢轴的索引
    int pivotIndex;
    while (low < high) {
        // 将数组一分为二,并返回枢轴的索引
        pivotIndex = partition(arr, low, high);
        // 对低子表递归排序
        sort(arr, low, pivotIndex - 1);
        low = pivotIndex + 1;
    }
}

我们将if改为while,因为第一次递归之后变量low就没用了,我们可以将pivotIndex + 1赋值给low,再循环后,来一次partition(arr, low, high),其效果等同于sort(arr, pivotIndex + 1, high)。结果相同,但因采用迭代而不是递归的方式,减小了栈深度,从而提高了整体性能。

完整代码

package org.example.demo;

public class QuickSortTest {

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

    private static void sort(int[] arr, int low, int high) {
        // 枢轴的索引
        int pivotIndex;
        while (low < high) {
            // 将数组一分为二,并返回枢轴的索引
            pivotIndex = partition(arr, low, high);
            // 对低子表递归排序
            sort(arr, low, pivotIndex - 1);
            low = pivotIndex + 1;
        }
    }

    private static int partition(int[] arr, int low, int high) {
        // 优化pivot的选取,最左、中间、最右三数取中
        // 因为下面的代码是让low位作为枢轴,所以这里让low位的值是三数的中间值
        int mid = low + (high - low >>> 1);
        if (arr[low] > arr[high])
            swap(arr, low, high);
        if (arr[mid] > arr[high])
            swap(arr, mid, high);
        if (arr[mid] > arr[low])
            swap(arr, low, mid);

        int pivot = arr[low];
        while (low < high) {
            while (low < high && arr[high] >= pivot)
                high--;
            arr[low] = arr[high];
            while (low < high && arr[low] <= pivot)
                low++;
            arr[high] = arr[low];
        }
        arr[low] = pivot;

        return low;
    }

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

    private static void print(int[] arr) {
        for (int i : arr) {
            System.out.print(i + " ");
        }
        System.out.println();
    }

    public static void testCase() {
        int[] arr = { 5, 1, 9, 3, 7, 4, 8, 6, 2 };
        sort(arr);
        print(arr);
    }

    public static void testCase1() {
        int[] arr = null;
        sort(arr);
        //print(arr);
    }

    public static void testCase2() {
        int[] arr = {};
        sort(arr);
        print(arr);
    }

    public static void testCase3() {
        int[] arr = { 2, 1 };
        sort(arr);
        print(arr);
    }

    public static void main(String[] args) {
        testCase();
        testCase1();
        testCase2();
        testCase3();
    }
}