思路
- 从数组中取一个数,称之为基数。
- 遍历数组,将比基数大的放在它右边,比基数小的放在它左边。
- 遍历之后,原数组被分为左右两个区域。
- 将左右两个区域视为两个数组,重复前面两个步骤,知道排序完成,
快速递归框架
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int start, int end) {
// 将数组分区,并获得中间值的下标
int middle = partition(arr, start, end);
// 对左边区域快速排序
quickSort(arr, start, middle - 1);
// 对右边区域快速排序
quickSort(arr, middle + 1, end);
}
public static int partition(int[] arr, int start, int end) {
// TODO: 将 arr 从 start 到 end 分区,左边区域比基数小,右边区域比基数大,然后返回中间值的下标
}
partition 意为 “划分” , 我们期望partition函数做的事情是:
- 将arr 从 start 到 end 这一区间分成两个区
- 左边的区域的每个数都比基数小,右边区域的数都比基数大。
- 返回基数的下标
思路:
- 调用partition函数获取中间值的下标 middle。
- 对左边进行快排
- 对右边进行快排
退出递归的边界条件:
- 当某区域只剩下一个数字时。
- 当某区域剩下0个数字时。
- 即:start >= end
则加上退出递归的条件时为:
public static void quickSort(int[] arr, int start, int end) {
// 如果区域内的数字少于 2 个,退出递归
if (start == end || start == end + 1) return;
// 将数组分区,并获得中间值的下标
int middle = partition(arr, start, end);
// 对左边区域快速排序
quickSort(arr, start, middle - 1);
// 对右边区域快速排序
quickSort(arr, middle + 1, end);
}
分区算法的实现
就是上文的partition函数,到底该怎么写?!。
基数选择:
- 选择第一个元素作为基数
- 选择最后一个元素作为基数
- 选择区间内一个随机元素作为基数
以第一个元素作为基数来实现。
// 将 arr 从 start 到 end 分区,左边区域比基数小,右边区域比基数大,然后返回中间值的下标
public static int partition(int[] arr, int start, int end) {
// 取第一个数为基数
int pivot = arr[start];
// 从第二个数开始分区
int left = start + 1;
// 右边界
int right = end;
// TODO
}
最简单的分区算法
思路:
- 从left开始, 遇到比基数大的数,就交换到数组最后,并将rught - 1 ,直到left 和right 相遇。
- 将基数和中间的数进行交换,返回中间值的下标。
代码实现:
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int start, int end) {
// 如果区域内的数字少于 2 个,退出递归
if (start >= end) return;
// 将数组分区,并获得中间值的下标
int middle = partition(arr, start, end);
// 对左边区域快速排序
quickSort(arr, start, middle - 1);
// 对右边区域快速排序
quickSort(arr, middle + 1, end);
}
// 将 arr 从 start 到 end 分区,左边区域比基数小,右边区域比基数大,然后返回中间值的下标
public static int partition(int[] arr, int start, int end) {
// 取第一个数为基数
int pivot = arr[start];
// 从第二个数开始分区
int left = start + 1;
// 右边界
int right = end;
// left、right 相遇时退出循环
while (left < right) {
// 找到第一个大于基数的位置
while (left < right && arr[left] <= pivot) left++;
// 交换这两个数,使得左边分区都小于或等于基数,右边分区大于或等于基数
if (left != right) {
exchange(arr, left, right);
right--;
}
}
// 如果 left 和 right 相等,单独比较 arr[right] 和 pivot
if (left == right && arr[right] > pivot) right--;
// 将基数和中间数交换
if (right != start) exchange(arr, start, right);
// 返回中间值的下标
return right;
}
private static void exchange(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
双指针分区算法
思路:
- 从left 开始,遇到比基数大的数,记录其下标;
- 再从right 开始,找到比基数小的,记录其下标;
- 然后交换两个数。
- 继续上述步骤,直到left 和 right 相遇。
- 最后交换基数和中间值。
代码如下:
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int start, int end) {
// 如果区域内的数字少于 2 个,退出递归
if (start >= end) return;
// 将数组分区,并获得中间值的下标
int middle = partition(arr, start, end);
// 对左边区域快速排序
quickSort(arr, start, middle - 1);
// 对右边区域快速排序
quickSort(arr, middle + 1, end);
}
// 将 arr 从 start 到 end 分区,左边区域比基数小,右边区域比基数大,然后返回中间值的下标
public static int partition(int[] arr, int start, int end) {
// 取第一个数为基数
int pivot = arr[start];
// 从第二个数开始分区
int left = start + 1;
// 右边界
int right = end;
while (left < right) {
// 找到第一个大于基数的位置
while (left < right && arr[left] <= pivot) left++;
// 找到第一个小于基数的位置
while (left < right && arr[right] >= pivot) right--;
// 交换这两个数,使得左边分区都小于或等于基数,右边分区大于或等于基数
if (left < right) {
exchange(arr, left, right);
left++;
right--;
}
}
// 如果 left 和 right 相等,单独比较 arr[right] 和 pivot
if (left == right && arr[right] > pivot) right--;
// 将基数和轴交换
exchange(arr, start, right);
return right;
}
private static void exchange(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
**时间复杂度:**平均时间复杂度为 O(nlogn),最坏的时间复杂度为 O(n^2)。
**空间复杂度:**空间复杂度为 O(logn) ~ O(n),平均空间复杂度为 O(logn)。