算法|快速排序

排序辅助类

import java.util.Arrays;
import java.util.Random;

public class SortHelper {

    // 打印数组arr
    public static void printArray(int[] arr) {
        System.out.println(Arrays.toString(arr));
    }

    // 交换数组元素 --> 需要传入数组进行操作,才能改变原数组的值
    public static void exchange(int[] arr, int x, int y) {
        int temp = arr[x];
        arr[x] = arr[y];
        arr[y] = temp;
    }

    // 生成数字范围在0-bound,且长度为n的随机数组
    public static int[] getRandomArray(int n, int bound) {
        Random random = new Random();
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = random.nextInt(bound);
        }
        return arr;
    }
    
    // 判断数组是否有序
    public static boolean isOrdered(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            if (arr[i] > arr[i + 1]) return false;
        }
        return true;
    }

}
1. 思路

通过arr[q]将数组arr[p...r]划分成两个子数组arr[p...(q-1)]arr[(q+1)...r],前一个子数组所有元素<=arr[q],后一个子数组所有元素>=arr[q]。以同样的做法对子数组进行递归划分,最后数组arr就排序好了。在这过程中,计算下标q,即划分子数组(partition)是快速排序最关键的一部分。

① 划分

选择一个x=arr[r]作为主元(pivot element)来进行划分子数组,从p开始遍历数组,<=x的数组元素与下标为i数组元素交换,ip-1开始记录,jp开始记录。

2. 时间复杂度

O(n•lgn)

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

// 递归
public static void quickSort(int[] arr, int p, int r) {
    if (p >= r) return;
    int q = partition(arr, p, r);
    quickSort(arr, p, q - 1);
    quickSort(arr, q + 1, r);
}

// 划分子数组,获取下标q
public static int partition(int[] arr, int p, int r) {
    int x = arr[r];
    int i = p - 1;
    for (int j = p; j < r; j++) {
        if (arr[j] <= x) {
            SortHelper.exchange(arr, ++i, j);
        }
    }
    SortHelper.exchange(arr, i + 1, r);
    return i + 1;
}
4. 思考

① 以arr[p]作为主元,如何划分?

// 反向遍历
public static int partition(int[] arr, int p, int r) {
    int x = arr[p];
    int i = r + 1;
    for (int j = r; j > p; j--) {
        if (arr[j] > x) {
            SortHelper.exchange(arr, --i, j);
        }
    }
    SortHelper.exchange(arr, --i, p);
    return i;
}
// 正向遍历
public static int partition(int[] arr, int p, int r) {
    int x = arr[p];
    int i = p;
    // p为主元,必须遍历到r
    for (int j = p + 1; j <= r; j++) {
        if (arr[j] <= x) {
            SortHelper.exchange(arr, j, ++i);
        }
    }
    SortHelper.exchange(arr, i, p);
    return i;
}

② 说明partition在数组arr={2, 8, 7, 1, 3, 5, 6, 4}上的操作过程。

[初始]

ip, jr (主元x)
28713564

[划分]

p, ijr (主元x)
28713564
p, ijr (主元x)
28713564
p, ijr (主元x)
28713564
pijr (主元x)
21783564
pijr (主元x)
21387564
pijr (主元x)
21387564
pi + 1jr
21347568

5. 性能

O(n•lgn)只是快排在最好情况下的时间复杂度。假如数组原本有序,那么使用快排的时间复杂度将到达O(n²)。或者说在每次拆分的时候选择的主元都只能将数组拆分成一个子数组,那么每次拆分的复杂度都是O(n-1),最终其时间复杂度也是O(n²)级别。

我们可以通过在快排算法中引入随机性(随机拆分),从而使得算法对于所有的输入都能获得较好的期望性能。

import java.util.Random;

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

public static void randomQuickSort(int[] arr, int p, int r) {
    if (p >= r) return;
    int q = randomPartition(arr, p, r);
    randomQuickSort(arr, p, q - 1);
    randomQuickSort(arr, q + 1, r);
}

public static int randomPartition(int[] arr, int p, int r) {
    // arr[r]随机化
    int i = getRandom(p, r);
    SortHelper.exchange(arr, r, i);
    return partition(arr, p, r);
}

// 随机获取p到r中一个数
public static int getRandom(int p, int r) {
    Random random = new Random();
    int i = random.nextInt(r - p + 1);
    return p + i;
}

public static int partition(int[] arr, int p, int r) {
    int x = arr[p];
    int i = p;
    for (int j = p + 1; j <= r; j++) {
        if (arr[j] <= x) {
            SortHelper.exchange(arr, j, ++i);
        }
    }
    SortHelper.exchange(arr, i, p);
    return i;
}
点赞