目录
- 思路
- 代码示例
- 时间复杂度
- 优化枢轴的选取
- 优化不必要的交换
- 优化递归
- 完整代码
思路
快排的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
代码示例
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();
}
}