7.1 快速排序的描述
对一个典型的子数组 A [ p . . r ] A[p..r] A[p..r]进行快速排序的三步分治过程:
- 分解:数组 A [ p . . r ] A[p..r] A[p..r]被划分为两个(可能为空)的子数组 A [ p . . q − 1 ] A[p..q-1] A[p..q−1]和 A [ q + 1.. r ] A[q+1..r] A[q+1..r],使得 A [ p . . q − 1 ] A[p..q-1] A[p..q−1]中的每一个元素都小于等于 A [ q ] A[q] A[q],而 A [ q + 1.. r ] A[q+1..r] A[q+1..r]中的每个元素都大于等于 A [ q ] A[q] A[q]。返回下标 q q q。
- 解决:通过递归,对子数组 A [ p . . q − 1 ] A[p..q-1] A[p..q−1]和 A [ q + 1.. r ] A[q+1..r] A[q+1..r]调用快速排序。
- 合并:数组 A [ p . . r ] A[p..r] A[p..r]已经有序。
下面的程序实现快速排序:
下图显示了PARTITION(A, p, r)的操作过程:选择 x = A [ r ] x=A[r] x=A[r]作为枢轴(pivot)(不一定非要选择数组最后一个元素作为枢轴,也可以选择其他元素),并围绕它来划分子数组 A [ p . . r ] A[p..r] A[p..r]。
PARTITION(A, p, r)的核心思想:取数组的最后一个元素为枢轴,使用指针 j j j从左向右遍历,遇到比枢轴小的元素,移动指针 i i i。这就形成了在从数组开头到指针 j j j的范围内,从数组开头到指针 i i i为比枢轴小的元素,从指针 i + 1 i+1 i+1到指针 j − 1 j-1 j−1为比枢轴大的元素,直到遍历 A . l e n g t h − 1 A.length-1 A.length−1个元素。最后,交换指针 i + 1 i+1 i+1指向的元素和数组最后一个元素即可。
PARTITION(A, p, r)在操作过程中将待排序的子数组划分为以下几个部分:
- A [ p . . i ] A[p..i] A[p..i]:已经遍历的比枢轴元素小的元素
- A [ i + 1.. j − 1 ] A[i+1..j-1] A[i+1..j−1]:已经遍历的比枢轴元素大的元素
- A [ j . . r − 1 ] A[j..r-1] A[j..r−1]:将要遍历的元素
- A [ r ] A[r] A[r]:枢轴元素
快速排序实验:读取data.txt文件,文件格式如下:第一行为数组长度,第二行为数组(int类型)的内容,将结果数组的数据写在一行内,每个数组中间以空格隔开,输出为sorted.txt。
代码实现:
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class QuickSort {
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void quickSort(int[] arr, int left, int right) {
if (left < right) {
int pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
}
public static int partition(int[] arr, int left, int right) {
int pivot = arr[right];
int i = left - 1;
for (int j = left; j < right; j++) {
if (arr[j] < pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, right);
return i + 1;
}
public static void main(String[] args) throws IOException {
/*
1、读取data.txt文件中的数据(第一行为数组长度, 第二行为数组(int类型)的内容)
*/
String path = "D:/Projects/IDEAProjects/algorithms/src/main/java/ch07/data.txt";
List<String> readAllLines = Files.readAllLines(Paths.get(path));
int length = Integer.parseInt(readAllLines.get(0));
String[] split = readAllLines.get(1).split("\\s+");
int[] array = new int[length];
for (int i = 0; i < length; i++) {
array[i] = Integer.parseInt(split[i]);
}
/*
2、计算算法的耗时
*/
long start = System.currentTimeMillis();
quickSort(array, 0, length - 1);
long end = System.currentTimeMillis();
System.out.println("算法耗时: " + (end - start) + " ms");
/*
3、将排序后的数据写入结果文件result.txt
*/
String pathResult = "D:/Projects/IDEAProjects/algorithms/src/main/java/ch07/sorted.txt";
File file = new File(pathResult);
if (!file.exists()) {
file.createNewFile();
}
FileWriter fileWriter = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < length; i++) {
stringBuilder.append(array[i]).append(" ");
}
bufferedWriter.write(stringBuilder.toString());
bufferedWriter.close();
//System.out.println(Arrays.toString(array));
}
}
7.2 快速排序的优化
优化思路:
-
基准的选择:快速排序的运行时间与划分是否对称有关。最坏情况下,每次划分过程产生两个区域分别包含 n − 1 n-1 n−1个元素和 1 1 1个元素,其时间复杂度会达到 O ( n 2 ) O(n^2) O(n2)。在最好的情况下,每次划分所取的基准都恰好是中值,即每次划分都产生两个大小为 n / 2 n/2 n/2的区域。此时,快排的时间复杂度为 O ( n log n ) O(n \log n) O(nlogn)。
所以基准的选择对快排而言至关重要。快排中基准的选择方式主要有以下三种:
- 固定基准
- 随机基准
- 三数取中
-
当输入数据已经“几乎有序”时,使用插入排序速度很快。我们可以利用这一特点来提高快速排序的速度。当对一个长度小于k的子数组调用快速排序时,让她不做任何排序就返回。上层的快速排序调用返回后,对整个数组运行插入排序来完成排序过程。
-
(可选)聚集元素
思想:在一次分割结束后,将与本次基准相等的元素聚集在一起,再分割时,不再对聚集过的元素进行分割。
- 在划分过程中将与基准值相等的元素放入数组两端,
- 划分结束后,再将两端的元素移到基准值周围。
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class QuickSortPro {
private final static int THRESHOLD = 8; // 插入排序阈值
/**
* 交换数组中两个索引位置的元素
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* 插入排序
* @param A
* @param left
* @param right
*/
public static void insertionSort(int[] A, int left, int right) {
for (int j = left + 1; j <= right; j++) {
int key = A[j];
int i = j - 1;
while (i >= left && A[i] > key) {
A[i + 1] = A[i];
i--;
}
A[i + 1] = key;
}
}
/**
* 快速排序优化
* @param arr
* @param left
* @param right
*/
public static void quickSortPro(int[] arr, int left, int right) {
if (right - left + 1 <= THRESHOLD) {
insertionSort(arr, left, right);
}
if (left < right) {
int pivotIndex = randomPartition(arr, left, right);
//int pivotIndex = partition(arr, left, right);
quickSortPro(arr, left, pivotIndex - 1);
quickSortPro(arr, pivotIndex + 1, right);
}
}
/**
* 三数取中,将中间大的元素交换到枢轴的位置
* @param arr
* @param left
* @param right
* @return
*/
public static int randomPartition(int[] arr, int left, int right) {
int mid = left + (right - left) / 2;
if (arr[left] > arr[mid]) {
swap(arr, left, mid);
}
if (arr[mid] > arr[right]) {
swap(arr, mid, right);
}
if (arr[mid] < arr[left]) {
swap(arr, left, mid);
}
swap(arr, mid, right);
return partition(arr, left, right);
}
/**
* 选取的枢轴为数组中最后一个元素
* @param arr
* @param left
* @param right
* @return
*/
public static int partition(int[] arr, int left, int right) {
int pivot = arr[right];
int i = left - 1;
for (int j = left; j < right; j++) {
if (arr[j] < pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, right);
return i + 1;
}
public static void main(String[] args) throws IOException {
/*
1、读取data.txt文件中的数据(第一行为数组长度, 第二行为数组(int类型)的内容)
*/
String path = "D:/Projects/IDEAProjects/algorithms/src/main/java/ch07/data.txt";
List<String> readAllLines = Files.readAllLines(Paths.get(path));
int length = Integer.parseInt(readAllLines.get(0));
String[] split = readAllLines.get(1).split("\\s+");
int[] array = new int[length];
for (int i = 0; i < length; i++) {
array[i] = Integer.parseInt(split[i]);
}
/*
2、计算算法的耗时
*/
long start = System.currentTimeMillis();
quickSortPro(array, 0, length - 1);
long end = System.currentTimeMillis();
System.out.println("算法耗时: " + (end - start) + " ms");
/*
3、将排序后的数据写入结果文件result.txt
*/
String pathResult = "D:/Projects/IDEAProjects/algorithms/src/main/java/ch07/sorted-pro.txt";
File file = new File(pathResult);
if (!file.exists()) {
file.createNewFile();
}
FileWriter fileWriter = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < length; i++) {
stringBuilder.append(array[i]).append(" ");
}
bufferedWriter.write(stringBuilder.toString());
bufferedWriter.close();
//System.out.println(Arrays.toString(array));
}
}
7.3 常见排序算法的运行时间
算法 | 最坏运行时间 | 平均情况/期望运行时间 |
---|---|---|
插入排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n 2 ) \Theta(n^2) Θ(n2) |
归并排序 | Θ ( n log n ) \Theta(n \log n) Θ(nlogn) | Θ ( n log n ) \Theta(n \log n) Θ(nlogn) |
堆排序 | O ( n log n ) \Omicron(n \log n) O(nlogn) | -------- |
快速排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n log n ) \Theta(n \log n) Θ(nlogn)(期望) |
计数排序 | Θ ( k + n ) \Theta(k+n) Θ(k+n) | Θ ( k + n ) \Theta(k+n) Θ(k+n) |
基数排序 | Θ ( d ( n + k ) ) \Theta(d(n+k)) Θ(d(n+k)) | Θ ( d ( n + k ) ) \Theta(d(n+k)) Θ(d(n+k)) |
桶排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n ) \Theta(n) Θ(n)(平均情况) |