算法导论-第7章-快速排序
7.1 快速排序的描述
对一个典型的子数组\(A[p..r]\)进行快速排序的三步分治过程:
- 分解:数组\(A[p..r]\)被划分为两个(可能为空)的子数组\(A[p..q-1]\)和\(A[q+1..r]\),使得\(A[p..q-1]\)中的每一个元素都小于等于\(A[q]\),而\(A[q+1..r]\)中的每个元素都大于等于\(A[q]\)。返回下标\(q\)。
- 解决:通过递归,对子数组\(A[p..q-1]\)和\(A[q+1..r]\)调用快速排序。
- 合并:数组\(A[p..r]\)已经有序。
下面的程序实现快速排序:
下图显示了PARTITION(A, p, r)的操作过程:选择\(x=A[r]\)作为枢轴(pivot)(不一定非要选择数组最后一个元素作为枢轴,也可以选择其他元素),并围绕它来划分子数组\(A[p..r]\)。
PARTITION(A, p, r)的核心思想:取数组的最后一个元素为枢轴,使用指针\(j\)从左向右遍历,遇到比枢轴小的元素,移动指针\(i\)。这就形成了在从数组开头到指针\(j\)的范围内,从数组开头到指针\(i\)为比枢轴小的元素,从指针\(i+1\)到指针\(j-1\)为比枢轴大的元素,直到遍历\(A.length-1\)个元素。最后,交换指针\(i+1\)指向的元素和数组最后一个元素即可。
PARTITION(A, p, r)在操作过程中将待排序的子数组划分为以下几个部分:
- \(A[p..i]\):已经遍历的比枢轴元素小的元素
- \(A[i+1..j-1]\):已经遍历的比枢轴元素大的元素
- \(A[j..r-1]\):将要遍历的元素
- \(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\)个元素和\(1\)个元素,其时间复杂度会达到\(O(n^2)\)。在最好的情况下,每次划分所取的基准都恰好是中值,即每次划分都产生两个大小为\(n/2\)的区域。此时,快排的时间复杂度为\(O(n \log n)\)。
所以基准的选择对快排而言至关重要。快排中基准的选择方式主要有以下三种:
- 固定基准
- 随机基准
- 三数取中
-
当输入数据已经“几乎有序”时,使用插入排序速度很快。我们可以利用这一特点来提高快速排序的速度。当对一个长度小于 \(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 常见排序算法的运行时间
算法 | 最坏运行时间 | 平均情况/期望运行时间 |
---|---|---|
插入排序 | \(\Theta(n^2)\) | \(\Theta(n^2)\) |
归并排序 | \(\Theta(n \log n)\) | \(\Theta(n \log n)\) |
堆排序 | \(\Omicron(n \log n)\) | -------- |
快速排序 | \(\Theta(n^2)\) | \(\Theta(n \log n)\)(期望) |
计数排序 | \(\Theta(k+n)\) | \(\Theta(k+n)\) |
基数排序 | \(\Theta(d(n+k))\) | \(\Theta(d(n+k))\) |
桶排序 | \(\Theta(n^2)\) | \(\Theta(n)\)(平均情况) |