算法导论-第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)\)(平均情况) |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本