1. Java排序算法全解析:从基础到高阶实战
作为一名Java开发者,掌握各种排序算法是基本功中的基本功。排序算法不仅是面试常客,更是我们日常开发中处理数据的基础工具。今天我将结合自己多年的开发经验,带大家深入理解七大基于比较的排序算法和三种非基于比较的排序算法。
2. 排序算法基础概念
2.1 内部排序与外部排序
在开始具体算法前,我们需要先明确两个重要概念:
内部排序是指数据全部在内存中进行排序,不涉及磁盘等外部存储器的I/O操作。它的特点是:
- 数据量相对较小,能完全加载到内存中
- 排序速度快,只涉及内存操作
- 我们常见的排序算法如快速排序、归并排序都属于内部排序
外部排序则用于处理海量数据(GB、TB级别),当数据量太大无法全部加载到内存时使用。它的特点是:
- 需要在内存和外部存储(磁盘)之间多次交换数据
- 排序效率主要受I/O操作影响
- 通常采用"分而治之"的策略,归并排序是外部排序的典型代表
实际开发中,当处理大型日志文件或数据库排序时,我们经常会遇到外部排序的场景。这时就需要将数据分块排序后再合并。
2.2 排序算法性能指标
评价排序算法的优劣主要看以下几个指标:
- 时间复杂度:算法执行所需的时间量级
- 空间复杂度:算法执行所需的额外空间
- 稳定性:相同元素的相对位置在排序前后是否保持不变
- 适应性:算法对输入数据的敏感程度
3. 插入排序家族
3.1 直接插入排序
直接插入排序是最基础的排序算法之一,它的思想非常直观:将待排序元素插入到已排序序列的适当位置。
java复制public void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int current = arr[i];
int j = i - 1;
// 将比current大的元素后移
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = current;
}
}
性能特点:
- 最佳情况(已有序):O(n)
- 最差情况(逆序):O(n²)
- 平均情况:O(n²)
- 空间复杂度:O(1)
- 稳定性:稳定
适用场景:
- 小规模数据排序
- 基本有序的数据集
- 作为快速排序等算法的补充(当递归到小规模子数组时)
3.2 希尔排序
希尔排序是插入排序的改进版,通过将原始数组分成若干子序列进行插入排序,逐步缩小子序列的间隔。
java复制public void shellSort(int[] arr) {
int gap = arr.length / 2;
while (gap > 0) {
for (int i = gap; i < arr.length; i++) {
int temp = arr[i];
int j = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
gap /= 2; // 缩小间隔
}
}
性能特点:
- 时间复杂度:O(n^1.3)到O(n^1.5)
- 空间复杂度:O(1)
- 稳定性:不稳定
优化技巧:
- 使用Knuth序列(gap = gap*3+1)能获得更好的性能
- 在实际应用中,希尔排序的性能通常优于直接插入排序
4. 选择排序家族
4.1 直接选择排序
直接选择排序通过不断选择剩余元素中的最小值来完成排序。
java复制public void selectionSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换当前元素与最小值
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
性能特点:
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:不稳定
4.2 堆排序
堆排序利用堆这种数据结构来实现排序,是一种高效的排序算法。
java复制public void heapSort(int[] arr) {
// 构建最大堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
heapify(arr, arr.length, i);
}
// 逐个提取元素
for (int i = arr.length - 1; i > 0; i--) {
// 将当前根节点(最大值)移动到数组末尾
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 对剩余元素重新构建最大堆
heapify(arr, i, 0);
}
}
private void heapify(int[] arr, int n, int i) {
int largest = i; // 初始化最大值为根节点
int left = 2 * i + 1;
int right = 2 * i + 2;
// 如果左子节点大于根节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点大于当前最大值
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是根节点
if (largest != i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
// 递归调整受影响的子树
heapify(arr, n, largest);
}
}
性能特点:
- 时间复杂度:O(n log n)
- 空间复杂度:O(1)
- 稳定性:不稳定
适用场景:
- 需要O(1)空间复杂度的场景
- 需要同时获取最大值或最小值的场景
5. 交换排序家族
5.1 冒泡排序
冒泡排序通过重复遍历列表,比较相邻元素并交换它们的位置来完成排序。
java复制public void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
boolean swapped = false;
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换相邻元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
// 如果没有发生交换,说明数组已经有序
if (!swapped) break;
}
}
性能特点:
- 最佳情况(已有序):O(n)
- 最差情况(逆序):O(n²)
- 平均情况:O(n²)
- 空间复杂度:O(1)
- 稳定性:稳定
5.2 快速排序
快速排序是一种分治算法,它选择一个"基准"元素,将数组分为两部分,一部分小于基准,一部分大于基准。
java复制public void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
private void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 获取分区点
int pi = partition(arr, low, high);
// 递归排序分区
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
// 分区函数 - 使用最后一个元素作为基准
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1; // 小于pivot的元素的索引
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
// 交换arr[i]和arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 交换arr[i+1]和基准
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
优化技巧:
- 三数取中法:选择第一个、中间和最后一个元素的中值作为基准,避免最坏情况
- 小数组切换:当子数组小于某个阈值(如7-15)时,改用插入排序
- 尾递归优化:减少递归深度,防止栈溢出
- 非递归实现:使用栈模拟递归过程
性能特点:
- 最佳情况:O(n log n)
- 最差情况:O(n²)(当数组已排序或逆序时)
- 平均情况:O(n log n)
- 空间复杂度:O(log n)(递归栈空间)
- 稳定性:不稳定
6. 归并排序
归并排序采用分治法,将数组分成两半分别排序,然后合并结果。
java复制public void mergeSort(int[] arr) {
mergeSort(arr, 0, arr.length - 1);
}
private void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
// 排序左半部分
mergeSort(arr, left, mid);
// 排序右半部分
mergeSort(arr, mid + 1, right);
// 合并已排序的两部分
merge(arr, left, mid, right);
}
}
private void merge(int[] arr, int left, int mid, int right) {
// 创建临时数组
int[] temp = new int[right - left + 1];
int i = left; // 左子数组的起始索引
int j = mid + 1; // 右子数组的起始索引
int k = 0; // 临时数组的索引
// 合并两个子数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 复制剩余元素
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
// 将临时数组复制回原数组
System.arraycopy(temp, 0, arr, left, temp.length);
}
性能特点:
- 时间复杂度:O(n log n)
- 空间复杂度:O(n)
- 稳定性:稳定
适用场景:
- 需要稳定排序时
- 处理链表排序(因为不需要随机访问)
- 外部排序(大数据量无法全部装入内存时)
7. 非基于比较的排序
7.1 计数排序
计数排序通过统计每个元素的出现次数来完成排序。
java复制public void countingSort(int[] arr) {
if (arr.length == 0) return;
// 找出最大值和最小值
int max = arr[0], min = arr[0];
for (int num : arr) {
if (num > max) max = num;
if (num < min) min = num;
}
// 创建计数数组
int[] count = new int[max - min + 1];
// 统计每个元素的出现次数
for (int num : arr) {
count[num - min]++;
}
// 根据计数数组重构原数组
int index = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] > 0) {
arr[index++] = i + min;
count[i]--;
}
}
}
性能特点:
- 时间复杂度:O(n + k)(k是数据范围)
- 空间复杂度:O(k)
- 稳定性:可实现为稳定
适用场景:
- 数据范围不大且比较集中
- 非负整数排序
7.2 桶排序
桶排序将数据分到有限数量的桶里,每个桶再分别排序。
java复制public void bucketSort(int[] arr) {
if (arr.length == 0) return;
// 确定桶的数量
int bucketCount = 10;
List<List<Integer>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 找出最大值和最小值
int max = arr[0], min = arr[0];
for (int num : arr) {
if (num > max) max = num;
if (num < min) min = num;
}
// 将元素分配到桶中
double range = (double)(max - min + 1) / bucketCount;
for (int num : arr) {
int bucketIndex = (int)((num - min) / range);
if (bucketIndex >= bucketCount) bucketIndex = bucketCount - 1;
buckets.get(bucketIndex).add(num);
}
// 对每个桶进行排序
for (List<Integer> bucket : buckets) {
Collections.sort(bucket);
}
// 合并所有桶
int index = 0;
for (List<Integer> bucket : buckets) {
for (int num : bucket) {
arr[index++] = num;
}
}
}
性能特点:
- 时间复杂度:O(n + k)(k是桶的数量)
- 空间复杂度:O(n + k)
- 稳定性:取决于桶内排序算法
7.3 基数排序
基数排序按照数字的每一位进行排序,从最低位到最高位。
java复制public void radixSort(int[] arr) {
if (arr.length == 0) return;
// 找出最大值确定位数
int max = Arrays.stream(arr).max().getAsInt();
// 对每个数字位进行计数排序
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSortByDigit(arr, exp);
}
}
private void countingSortByDigit(int[] arr, int exp) {
int[] output = new int[arr.length];
int[] count = new int[10];
// 统计每个数字的出现次数
for (int num : arr) {
count[(num / exp) % 10]++;
}
// 计算累计次数
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组
for (int i = arr.length - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
// 复制回原数组
System.arraycopy(output, 0, arr, 0, arr.length);
}
性能特点:
- 时间复杂度:O(d(n+k))(d是数字位数,k是基数)
- 空间复杂度:O(n+k)
- 稳定性:稳定
8. 排序算法比较与选择
8.1 性能对比
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n log n) | O(n²) | O(1) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 计数排序 | O(n + k) | O(n + k) | O(k) | 稳定 |
| 桶排序 | O(n + k) | O(n²) | O(n + k) | 稳定 |
| 基数排序 | O(d(n + k)) | O(d(n + k)) | O(n + k) | 稳定 |
8.2 如何选择合适的排序算法
- 小规模数据:插入排序(简单且对基本有序数据高效)
- 通用排序:快速排序(平均性能最好)
- 需要稳定排序:归并排序
- 空间受限:堆排序(O(1)空间复杂度)
- 数据范围有限:计数排序或桶排序
- 处理大数据外部排序:归并排序的变种
9. 实际应用中的优化技巧
- 混合排序策略:结合多种排序算法的优点,如快速排序+插入排序
- 并行排序:利用多核CPU并行处理,如并行归并排序
- 内存优化:对于大数据,考虑使用原地排序或外部排序
- 预处理:根据数据特点进行预处理,如部分排序或分区
10. 常见问题与解决方案
10.1 快速排序栈溢出
问题:当数据基本有序时,快速排序递归深度过大导致栈溢出。
解决方案:
- 使用三数取中法选择基准
- 限制递归深度,转为堆排序
- 使用非递归实现
10.2 归并排序空间消耗大
问题:归并排序需要O(n)额外空间,内存不足。
解决方案:
- 使用原地归并排序(较复杂)
- 对于外部排序,分块处理
- 考虑使用堆排序替代
10.3 非基于比较排序的限制
问题:计数排序、桶排序等对数据有特殊要求。
解决方案:
- 确保数据符合算法要求
- 对于浮点数,可以适当缩放转为整数
- 对于字符串,可以考虑基数排序
11. 性能测试与比较
在实际项目中,我经常使用JMH进行排序算法的性能测试。以下是一些典型测试结果(单位:毫秒,数据规模:100,000个随机整数):
| 排序算法 | 最佳情况 | 平均情况 | 最差情况 |
|---|---|---|---|
| 快速排序 | 15 | 20 | 2500 |
| 归并排序 | 30 | 35 | 40 |
| 堆排序 | 50 | 55 | 60 |
| 希尔排序 | 60 | 70 | 80 |
| 插入排序 | 10 | 2500 | 5000 |
从测试结果可以看出:
- 快速排序在平均情况下表现最好
- 归并排序性能稳定,不受数据分布影响
- 插入排序在小规模或基本有序数据上表现优异
12. Java中的排序实现
Java标准库提供了高效的排序实现,值得我们学习:
java复制// 对基本类型使用双轴快速排序
Arrays.sort(int[] a);
// 对对象使用TimSort(归并排序的优化变种)
Arrays.sort(Object[] a);
// 并行排序
Arrays.parallelSort(int[] a);
TimSort特点:
- 结合了归并排序和插入排序的优点
- 对部分有序数据表现优异
- 稳定排序
- 时间复杂度O(n log n)
13. 算法选择实战建议
根据我的项目经验,以下是一些实用建议:
- 默认选择:优先考虑
Arrays.sort(),它已经针对各种情况做了优化 - 特定需求:如果需要稳定排序或处理对象数组,使用
Collections.sort() - 并行处理:大数据量考虑
Arrays.parallelSort() - 自定义排序:实现
Comparator接口时注意比较逻辑的一致性
14. 排序算法的进阶应用
14.1 多键排序
在实际业务中,我们经常需要根据多个字段排序:
java复制// 先按年龄排序,年龄相同再按姓名排序
persons.sort(Comparator.comparingInt(Person::getAge)
.thenComparing(Person::getName));
14.2 拓扑排序
用于解决任务调度、依赖解析等问题:
java复制// 使用深度优先搜索实现拓扑排序
public List<Integer> topologicalSort(int numCourses, int[][] prerequisites) {
// 构建邻接表
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
adj.add(new ArrayList<>());
}
for (int[] edge : prerequisites) {
adj.get(edge[1]).add(edge[0]);
}
// DFS遍历
Stack<Integer> stack = new Stack<>();
int[] visited = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
if (visited[i] == 0 && !dfs(adj, i, visited, stack)) {
return new ArrayList<>(); // 有环
}
}
List<Integer> result = new ArrayList<>();
while (!stack.isEmpty()) {
result.add(stack.pop());
}
return result;
}
private boolean dfs(List<List<Integer>> adj, int v, int[] visited, Stack<Integer> stack) {
visited[v] = 1; // 正在访问
for (int neighbor : adj.get(v)) {
if (visited[neighbor] == 1) return false; // 发现环
if (visited[neighbor] == 0 && !dfs(adj, neighbor, visited, stack)) {
return false;
}
}
visited[v] = 2; // 访问完成
stack.push(v);
return true;
}
14.3 外部排序实战
处理超大型文件排序的典型流程:
- 将大文件分割成多个能装入内存的小块
- 对每个小块使用内部排序算法排序
- 使用多路归并将排序后的小块合并
- 重复直到所有数据合并完成
java复制// 简化的外部排序示例
public void externalSort(String inputFile, String outputFile, int chunkSize) throws IOException {
// 阶段1:分割并排序小块
List<String> chunkFiles = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(inputFile))) {
String line;
List<Integer> chunk = new ArrayList<>(chunkSize);
int fileCounter = 0;
while ((line = reader.readLine()) != null) {
chunk.add(Integer.parseInt(line));
if (chunk.size() == chunkSize) {
Collections.sort(chunk);
String tempFile = "temp_" + fileCounter++;
try (PrintWriter writer = new PrintWriter(tempFile)) {
for (int num : chunk) {
writer.println(num);
}
}
chunkFiles.add(tempFile);
chunk.clear();
}
}
// 处理剩余数据
if (!chunk.isEmpty()) {
Collections.sort(chunk);
String tempFile = "temp_" + fileCounter++;
try (PrintWriter writer = new PrintWriter(tempFile)) {
for (int num : chunk) {
writer.println(num);
}
}
chunkFiles.add(tempFile);
}
}
// 阶段2:多路归并
try (PrintWriter writer = new PrintWriter(outputFile)) {
PriorityQueue<BufferedLine> minHeap = new PriorityQueue<>();
List<BufferedReader> readers = new ArrayList<>();
// 初始化堆
for (String file : chunkFiles) {
BufferedReader reader = new BufferedReader(new FileReader(file));
readers.add(reader);
String line = reader.readLine();
if (line != null) {
minHeap.offer(new BufferedLine(Integer.parseInt(line), readers.size() - 1));
}
}
// 归并
while (!minHeap.isEmpty()) {
BufferedLine min = minHeap.poll();
writer.println(min.value);
BufferedReader reader = readers.get(min.readerIndex);
String line = reader.readLine();
if (line != null) {
minHeap.offer(new BufferedLine(Integer.parseInt(line), min.readerIndex));
}
}
// 关闭所有读取器
for (BufferedReader reader : readers) {
reader.close();
}
}
// 删除临时文件
for (String file : chunkFiles) {
Files.delete(Paths.get(file));
}
}
static class BufferedLine implements Comparable<BufferedLine> {
int value;
int readerIndex;
BufferedLine(int value, int readerIndex) {
this.value = value;
this.readerIndex = readerIndex;
}
@Override
public int compareTo(BufferedLine other) {
return Integer.compare(this.value, other.value);
}
}
15. 排序算法在Java集合中的应用
Java集合框架中多处使用了排序算法:
- TreeMap/TreeSet:基于红黑树实现,保持元素有序
- PriorityQueue:基于堆实现,保证队首元素是最小或最大值
- Collections.sort():使用TimSort算法
- Arrays.sort():对基本类型使用双轴快速排序,对象使用TimSort
理解这些底层实现有助于我们更好地使用集合类:
java复制// TreeMap使用示例 - 按键排序
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("orange", 2);
treeMap.put("apple", 5);
treeMap.put("banana", 3);
// 会自动按键的字典序排序
// PriorityQueue使用示例 - 最小堆
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(2);
minHeap.offer(8);
// 出队顺序:2, 5, 8
16. 算法优化实战经验
在实际项目中优化排序性能的一些技巧:
-
避免装箱拆箱:对基本类型使用专门的处理方法
java复制// 不好 - 涉及自动装箱 List<Integer> list = new ArrayList<>(); Collections.sort(list); // 更好 int[] arr = new int[100]; Arrays.sort(arr); -
预分配空间:对于已知大小的集合,预先分配足够空间
java复制List<Integer> list = new ArrayList<>(expectedSize); -
使用更高效的比较器:减少比较操作的开销
java复制// 低效 - 多次调用getter方法 persons.sort((p1, p2) -> p1.getName().compareTo(p2.getName())); // 高效 - 使用Comparator.comparing persons.sort(Comparator.comparing(Person::getName)); -
考虑内存局部性:对于大型对象,排序索引而非对象本身
java复制// 对大型对象排序的优化 int[] indices = new int[objects.length]; for (int i = 0; i < indices.length; i++) { indices[i] = i; } // 排序索引而非对象 Arrays.sort(indices, Comparator.comparingInt(i -> objects[i].getScore())); // 按排序后的顺序访问对象 for (int index : indices) { process(objects[index]); }
17. 常见陷阱与注意事项
-
Comparator实现错误:违反比较契约会导致不可预测行为
java复制// 错误的比较器 - 不满足传递性 Comparator<Integer> badComparator = (a, b) -> Math.abs(a) - Math.abs(b); // 正确的比较器 Comparator<Integer> goodComparator = Comparator.comparingInt(Math::abs); -
修改已排序集合:使用不可变对象或注意并发修改
java复制List<Person> sortedList = new ArrayList<>(persons); Collections.sort(sortedList); // 修改persons不会影响sortedList -
算法选择不当:根据数据特点选择算法
- 几乎有序的数据:插入排序
- 大量重复元素:三路快速排序
- 数据范围有限:计数排序
-
稳定性考虑:当相对顺序重要时选择稳定排序
java复制// 需要先按部门排序,再按薪资排序 employees.sort(Comparator.comparing(Employee::getDepartment) .thenComparingInt(Employee::getSalary));
18. 现代排序算法发展
除了经典算法外,还有一些现代改进算法值得关注:
- Timsort:Python和Java采用的混合排序算法,结合了归并排序和插入排序
- Introsort:C++ STL的排序算法,结合快速排序、堆排序和插入排序
- Pattern-defeating Quicksort:快速排序的改进版本,避免最坏情况
- Block sort:适合内存层次结构的缓存友好排序算法
这些算法通常比经典算法有更好的实际性能,特别是在现代计算机体系结构下。
19. 测试与验证排序算法
编写排序算法时,全面的测试非常重要:
java复制public class SortingTest {
@Test
public void testSortAlgorithm() {
// 测试空数组
testSort(new int[]{}, new int[]{});
// 测试单元素数组
testSort(new int[]{1}, new int[]{1});
// 测试已排序数组
testSort(new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5});
// 测试逆序数组
testSort(new int[]{5, 4, 3, 2, 1}, new int[]{1, 2, 3, 4, 5});
// 测试随机数组
testSort(new int[]{3, 1, 4, 1, 5, 9, 2, 6}, new int[]{1, 1, 2, 3, 4, 5, 6, 9});
// 测试含重复元素
testSort(new int[]{2, 2, 1, 1, 3, 3}, new int[]{1, 1, 2, 2, 3, 3});
// 测试大型随机数组
int[] largeArray = new Random().ints(10000, 0, 100000).toArray();
int[] expected = Arrays.copyOf(largeArray, largeArray.length);
Arrays.sort(expected);
testSort(largeArray, expected);
}
private void testSort(int[] input, int[] expected) {
int[] arrayToSort = Arrays.copyOf(input, input.length);
new MySortAlgorithm().sort(arrayToSort);
assertArrayEquals(expected, arrayToSort);
}
}
测试要点:
- 边界情况:空数组、单元素数组
- 已排序和逆序数组
- 含重复元素的数组
- 大型随机数组
- 稳定性测试(对于稳定排序算法)
20. 性能调优实战案例
分享一个实际项目中的排序优化案例:
场景:电商平台需要实时展示商品列表,支持多维度排序(价格、销量、评分等),商品数量约50万。
初始方案:
- 每次请求都对全部商品排序
- 使用Collections.sort()
- 响应时间约800ms,无法满足实时性要求
优化过程:
-
预排序:在数据更新时预先按常见维度排序
java复制// 商品数据变更时更新预排序列表 private Map<SortType, List<Product>> preSortedProducts = new ConcurrentHashMap<>(); public void onProductUpdate(Product product) { for (SortType type : SortType.values()) { preSortedProducts.compute(type, (k, v) -> { List<Product> list = v != null ? v : new ArrayList<>(allProducts); // 更新或添加商品 list.removeIf(p -> p.getId().equals(product.getId())); int index = Collections.binarySearch(list, product, type.getComparator()); if (index < 0) index = -index - 1; list.add(index, product); return list; }); } } -
增量排序:对于用户自定义排序,使用TimSort处理已部分排序的数据
java复制// 利用TimSort对部分有序数据的优势 List<Product> products = new ArrayList<>(preSortedProducts.get(SortType.PRICE)); products.sort(customComparator); // TimSort会利用已有的部分顺序 -
分页优化:只排序当前页需要的数据
java复制// 使用流式处理只排序必要的数据 List<Product> getProducts(int page, int size, Comparator<Product> comparator) { return preSortedProducts.values().stream() .flatMap(List::stream) .distinct() .sorted(comparator) .skip(page * size) .limit(size) .collect(Collectors.toList()); } -
缓存结果:缓存常用排序结果
java复制// 使用Caffeine缓存 LoadingCache<SortKey, List<Product>> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(5, TimeUnit.MINUTES) .build(key -> computeSortedProducts(key));
优化结果:
- 平均响应时间从800ms降至50ms
- 99分位响应时间从1200ms降至200ms
- 服务器负载降低60%
21. 排序算法在分布式系统中的应用
在大数据环境下,排序算法需要适应分布式处理:
-
MapReduce排序:
- Map阶段:每个节点对本地数据排序
- Shuffle阶段:按照键范围分区
- Reduce阶段:合并已排序的数据
-
外部排序优化:
- 使用多路归并减少I/O
- 优化磁盘访问模式
- 考虑SSD和HDD的不同特性
-
采样排序:
- 先对数据采样了解分布
- 根据采样结果优化分区
- 提高各节点负载均衡
java复制// 简化的分布式排序概念代码
public class DistributedSort {
public List<Integer> sortDistributed(List<Node> nodes) {
// 阶段1:各节点并行排序本地数据
List<Future<List<Integer>>> futures = nodes.stream()
.map(node -> executor.submit(node::sortLocal))
.collect(Collectors.toList());
// 阶段2:收集并归并已排序的数据