目录
[1.1 排序的定义与重要性](#1.1 排序的定义与重要性)
[1.2 关键概念解析](#1.2 关键概念解析)
[内部排序 vs 外部排序](#内部排序 vs 外部排序)
[1.3 排序算法的应用场景](#1.3 排序算法的应用场景)
[2.1 插入排序类](#2.1 插入排序类)
[2.1.1 直接插入排序(Insertion Sort)](#2.1.1 直接插入排序(Insertion Sort))
[2.1.2 希尔排序(Shell Sort)](#2.1.2 希尔排序(Shell Sort))
[2.2 选择排序类](#2.2 选择排序类)
[2.2.1 直接选择排序(Selection Sort)](#2.2.1 直接选择排序(Selection Sort))
[2.2.2 堆排序(Heap Sort)](#2.2.2 堆排序(Heap Sort))
[2.3 交换排序类](#2.3 交换排序类)
[2.3.1 冒泡排序(Bubble Sort)](#2.3.1 冒泡排序(Bubble Sort))
[2.3.2 快速排序(Quick Sort)](#2.3.2 快速排序(Quick Sort))
[2.4 归并排序(Merge Sort)](#2.4 归并排序(Merge Sort))
[3.1 时间复杂度对比](#3.1 时间复杂度对比)
[3.2 排序算法选择指南](#3.2 排序算法选择指南)
[4.1 计数排序(Counting Sort)](#4.1 计数排序(Counting Sort))
[4.2 基数排序(Radix Sort)](#4.2 基数排序(Radix Sort))
[4.3 桶排序(Bucket Sort)](#4.3 桶排序(Bucket Sort))
[5.1 Java中的排序工具](#5.1 Java中的排序工具)
[5.2 排序算法优化策略](#5.2 排序算法优化策略)
前言
排序是计算机科学中最基础、最核心的算法之一。从简单的电话簿排序到复杂的数据库查询优化,排序算法无处不在。想象一下,如果没有排序,我们在电商网站搜索商品、在音乐库中查找歌曲、甚至在查看联系人列表时都会变得极其困难。
排序不仅仅是让数据有序排列------高效的排序算法能够显著提升系统性能,影响用户体验。本文将带你深入理解七大经典排序算法,掌握它们的实现原理、性能特点以及应用场景。
一、排序基本概念与分类
1.1 排序的定义与重要性
排序(Sorting)是将一组记录按照某个或某些关键字的大小,以递增或递减的顺序重新排列的操作。排序算法的选择直接影响程序的效率和资源消耗。
1.2 关键概念解析
稳定性(Stability)
-
稳定排序:如果两个相等元素的相对位置在排序前后保持不变
-
不稳定排序:相等元素的相对位置可能发生变化
示例:学生成绩表中,先按姓名排序,再按成绩排序。如果排序算法稳定,同分的学生仍会保持姓名的字典序。
- 稳定排序示例:冒泡排序
- 不稳定排序示例:快速排序
内部排序 vs 外部排序
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 内部排序 | 所有数据都在内存中进行排序 | 数据量较小,内存充足 |
| 外部排序 | 数据量太大,无法全部加载到内存 | 大数据处理,如数据库排序 |
1.3 排序算法的应用场景
-
电商平台:商品按价格、销量、评分排序
-
社交网络:动态按时间、热度排序
-
操作系统:进程调度优先级排序
-
数据库系统:索引构建和查询优化
二、基于比较的七大排序算法详解
2.1 插入排序类
2.1.1 直接插入排序(Insertion Sort)
核心思想:将待排序元素插入到已排序序列的适当位置,类似于整理扑克牌。
算法步骤:
-
从第一个元素开始,该元素可视为已排序
-
取出下一个元素,在已排序序列中从后向前扫描
-
如果该元素(已排序)大于新元素,将该元素移到下一位置
-
重复步骤3,直到找到已排序元素小于或等于新元素的位置
-
将新元素插入到该位置后
-
重复步骤2~5
public class InsertionSort {
public static void sort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i];
int j = i - 1;// 将比key大的元素向右移动 while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } }}
性能分析:
时间复杂度:
最好情况(已有序):O(n)
最坏情况(逆序):O(n²)
平均情况:O(n²)
空间复杂度:O(1)
稳定性:稳定
适用场景:小规模数据或基本有序的数据
2.1.2 希尔排序(Shell Sort)
核心思想:将原始数组分割成若干子序列进行插入排序,逐步缩小增量,最终完成整体排序。
算法步骤:
-
选择一个增量序列(如:n/2, n/4, ..., 1)
-
按增量序列个数k,对数组进行k趟排序
-
每趟排序,根据对应的增量,将待排序序列分割成若干子序列
-
分别对各子序列进行直接插入排序
-
当增量减至1时,整个序列作为一个表来处理
public class ShellSort {
public static void sort(int[] arr) {
int n = arr.length;// 使用Knuth增量序列 int gap = 1; while (gap < n / 3) { gap = gap * 3 + 1; } while (gap > 0) { // 对每个子序列进行插入排序 for (int i = gap; i < n; 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 = (gap - 1) / 3; // 缩小增量 } }}
性能分析:
时间复杂度:O(n^(1.3~2)),具体取决于增量序列
空间复杂度:O(1)
稳定性:不稳定
特点:希尔排序是插入排序的高效改进版
2.2 选择排序类
2.2.1 直接选择排序(Selection Sort)
核心思想:每次从未排序部分选择最小(或最大)元素,放到已排序部分的末尾。
算法步骤:
-
初始状态:有序区为空,无序区为整个数组
-
第i趟排序:从无序区选出最小元素,与无序区第一个元素交换
-
重复n-1趟,直到排序完成
public class SelectionSort {
public static void sort(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; } } // 将最小元素交换到已排序部分末尾 if (minIndex != i) { int temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } } }}
性能分析:
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:不稳定
特点:简单直观,但效率较低
2.2.2 堆排序(Heap Sort)
核心思想:利用堆数据结构设计的排序算法,是一种改进的选择排序。
算法步骤:
-
将无序数组构建成一个大顶堆(升序排序)
-
将堆顶元素(最大值)与末尾元素交换
-
调整堆结构,使其满足堆定义
-
重复步骤2-3,直到堆的大小为1
java
public class HeapSort {
public static void sort(int[] arr) {
int n = arr.length;
// 1. 构建大顶堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 2. 逐个提取元素
for (int i = n - 1; i > 0; i--) {
// 交换堆顶和当前末尾元素
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 调整剩余堆
heapify(arr, i, 0);
}
}
private static 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)
稳定性:不稳定
特点:原地排序,适合大规模数据
2.3 交换排序类
2.3.1 冒泡排序(Bubble Sort)
核心思想:重复遍历数组,比较相邻元素,如果顺序错误就交换它们。
算法步骤:
-
比较相邻元素,如果第一个比第二个大,就交换它们
-
对每一对相邻元素做同样工作,从开始第一对到结尾最后一对
-
针对所有元素重复以上步骤,除了最后一个
-
重复步骤1-3,直到排序完成
java
public class BubbleSort {
public static void sort(int[] arr) {
int n = arr.length;
boolean swapped;
for (int i = 0; i < n - 1; i++) {
swapped = false;
// 每次遍历将最大元素"冒泡"到末尾
for (int j = 0; j < n - i - 1; 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(1)
稳定性:稳定
特点:简单易懂,但效率较低
2.3.2 快速排序(Quick Sort)
核心思想:采用分治策略,选取一个基准元素,将数组分成两部分,左边小于基准,右边大于基准,然后递归排序。
三种分区方法:
1. Hoare分区法
java
private static int partitionHoare(int[] arr, int low, int high) {
int pivot = arr[low];
int i = low - 1;
int j = high + 1;
while (true) {
do {
i++;
} while (arr[i] < pivot);
do {
j--;
} while (arr[j] > pivot);
if (i >= j) return j;
// 交换arr[i]和arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
2. 挖坑法
java
private static int partitionDigging(int[] arr, int low, int high) {
int pivot = arr[low]; // 挖坑,保存基准值
int i = low, j = high;
while (i < j) {
// 从右向左找第一个小于pivot的元素
while (i < j && arr[j] >= pivot) {
j--;
}
if (i < j) {
arr[i] = arr[j]; // 填坑
i++;
}
// 从左向右找第一个大于等于pivot的元素
while (i < j && arr[i] < pivot) {
i++;
}
if (i < j) {
arr[j] = arr[i]; // 填坑
j--;
}
}
arr[i] = pivot; // 基准值放入最终位置
return i;
}
3. 前后指针法
java
private static int partitionTwoPointers(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = low - 1; // 指向小于基准的区域边界
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;
}
}
// 将基准放到正确位置
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
完整快速排序实现:
java
public class QuickSort {
// 快速排序主函数
public static void sort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
// 递归实现快速排序
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 优化1:小区间使用插入排序
if (high - low + 1 <= 10) {
insertionSort(arr, low, high);
return;
}
// 优化2:三数取中法选择基准
int pivotIndex = medianOfThree(arr, low, high);
swap(arr, pivotIndex, low); // 将基准放到开头
// 分区操作
int pi = partitionHoare(arr, low, high);
// 递归排序左右两部分
quickSort(arr, low, pi);
quickSort(arr, pi + 1, high);
}
}
// 三数取中法
private static int medianOfThree(int[] arr, int low, int high) {
int mid = low + (high - low) / 2;
if (arr[low] > arr[mid]) swap(arr, low, mid);
if (arr[low] > arr[high]) swap(arr, low, high);
if (arr[mid] > arr[high]) swap(arr, mid, high);
return mid; // 返回中间值索引
}
// 插入排序(用于小区间优化)
private static void insertionSort(int[] arr, int low, int high) {
for (int i = low + 1; i <= high; i++) {
int key = arr[i];
int j = i - 1;
while (j >= low && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
// 交换辅助函数
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
快速排序的非递归实现:
java
public class QuickSortIterative {
public static void sort(int[] arr) {
// 使用栈模拟递归调用
Stack<Integer> stack = new Stack<>();
stack.push(0);
stack.push(arr.length - 1);
while (!stack.isEmpty()) {
int high = stack.pop();
int low = stack.pop();
if (low < high) {
int pi = partitionHoare(arr, low, high);
// 压入左子数组边界
stack.push(low);
stack.push(pi);
// 压入右子数组边界
stack.push(pi + 1);
stack.push(high);
}
}
}
}
性能分析:
时间复杂度:
最好情况:O(n log n)
最坏情况:O(n²)(当数组已排序或逆序时)
平均情况:O(n log n)
空间复杂度:
递归实现:O(log n)(调用栈深度)
非递归实现:O(log n)(显式栈)
稳定性:不稳定
特点:平均性能最好,是实际应用中最常用的排序算法
2.4 归并排序(Merge Sort)
核心思想:采用分治法,将数组分成两半,分别排序,然后合并。
算法步骤:
-
分割:将数组递归地分成两半,直到每个子数组只有一个元素
-
合并:将两个已排序的子数组合并成一个有序数组
java
public class MergeSort {
// 归并排序主函数
public static void sort(int[] arr) {
mergeSort(arr, 0, arr.length - 1);
}
// 递归分割和合并
private static 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 static 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++];
}
// 将临时数组复制回原数组
for (i = left, k = 0; i <= right; i++, k++) {
arr[i] = temp[k];
}
}
// 归并排序的非递归实现
public static void sortIterative(int[] arr) {
int n = arr.length;
int currSize; // 当前子数组大小
// 从1开始,每次合并的子数组大小翻倍
for (currSize = 1; currSize <= n - 1; currSize = 2 * currSize) {
// 选择起始位置
for (int leftStart = 0; leftStart < n - 1; leftStart += 2 * currSize) {
int mid = Math.min(leftStart + currSize - 1, n - 1);
int rightEnd = Math.min(leftStart + 2 * currSize - 1, n - 1);
// 合并arr[leftStart...mid]和arr[mid+1...rightEnd]
merge(arr, leftStart, mid, rightEnd);
}
}
}
}
性能分析:
时间复杂度:O(n log n)
空间复杂度:O(n)(需要临时数组)
稳定性:稳定
特点:稳定排序,适合外部排序
三、排序算法性能对比分析
3.1 时间复杂度对比
| 排序算法 | 最好情况 | 平均情况 | 最坏情况 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n log n) | O(n^(1.3~2)) | O(n²) | O(1) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
3.2 排序算法选择指南
-
小规模数据(n ≤ 10):插入排序
-
基本有序数据:插入排序或冒泡排序
-
大规模数据,要求稳定排序:归并排序
-
大规模数据,对稳定性无要求:快速排序或堆排序
-
数据范围已知且较小:计数排序
-
外部排序(数据无法全部加载到内存):归并排序
四、非基于比较的排序算法
4.1 计数排序(Counting Sort)
核心思想:统计每个元素出现的次数,然后按顺序输出。
适用条件:元素范围已知且不大(如0-100的整数)
java
public class CountingSort {
public static void sort(int[] arr) {
if (arr.length == 0) return;
// 1. 找出最大值和最小值
int max = arr[0], min = arr[0];
for (int num : arr) {
if (num > max) max = num;
if (num < min) min = num;
}
// 2. 创建计数数组
int range = max - min + 1;
int[] count = new int[range];
// 3. 统计每个元素出现的次数
for (int num : arr) {
count[num - min]++;
}
// 4. 累加计数(确定每个元素的位置)
for (int i = 1; i < range; i++) {
count[i] += count[i - 1];
}
// 5. 构建排序结果
int[] output = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
int num = arr[i];
int pos = count[num - min] - 1;
output[pos] = num;
count[num - min]--;
}
// 6. 复制回原数组
System.arraycopy(output, 0, arr, 0, arr.length);
}
}
性能分析:
时间复杂度:O(n + k),k为数据范围
空间复杂度:O(n + k)
稳定性:稳定
4.2 基数排序(Radix Sort)
核心思想:按位排序,从最低位到最高位依次进行稳定排序。
java
public class RadixSort {
public static void sort(int[] arr) {
if (arr.length == 0) return;
// 找出最大值,确定最大位数
int max = arr[0];
for (int num : arr) {
if (num > max) max = num;
}
// 从个位开始,对每一位进行计数排序
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSortByDigit(arr, exp);
}
}
private static void countingSortByDigit(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10]; // 0-9十个数字
// 统计当前位的数字出现次数
for (int num : arr) {
int digit = (num / exp) % 10;
count[digit]++;
}
// 累加计数
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组(从后往前保持稳定性)
for (int i = n - 1; i >= 0; i--) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
// 复制回原数组
System.arraycopy(output, 0, arr, 0, n);
}
}
4.3 桶排序(Bucket Sort)
核心思想:将数据分到有限数量的桶中,每个桶分别排序,然后合并。
java
public class BucketSort {
public static void sort(int[] arr) {
if (arr.length == 0) return;
// 1. 确定桶的数量和范围
int max = arr[0], min = arr[0];
for (int num : arr) {
if (num > max) max = num;
if (num < min) min = num;
}
int bucketCount = 5; // 假设分为5个桶
List<List<Integer>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 2. 将元素分配到桶中
int range = max - min + 1;
for (int num : arr) {
int bucketIndex = (num - min) * bucketCount / range;
if (bucketIndex >= bucketCount) bucketIndex = bucketCount - 1;
buckets.get(bucketIndex).add(num);
}
// 3. 对每个桶进行排序
int index = 0;
for (List<Integer> bucket : buckets) {
// 使用插入排序对每个桶排序
Collections.sort(bucket);
// 4. 将排序后的桶合并
for (int num : bucket) {
arr[index++] = num;
}
}
}
}
五、实战应用与优化技巧
5.1 Java中的排序工具
java
import java.util.Arrays;
import java.util.Collections;
public class JavaSortExample {
public static void main(String[] args) {
// 1. 基本类型数组排序
int[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr); // 快速排序实现
// 2. 对象数组排序(需要实现Comparable接口)
Integer[] arr2 = {5, 2, 8, 1, 9};
Arrays.sort(arr2);
// 3. 降序排序
Arrays.sort(arr2, Collections.reverseOrder());
// 4. 自定义比较器
Arrays.sort(arr2, (a, b) -> b - a);
// 5. 部分排序
Arrays.sort(arr, 0, 3); // 只排序前3个元素
}
}
5.2 排序算法优化策略
-
混合排序:结合多种排序算法的优点
-
多线程并行排序:利用多核CPU加速
-
内存优化:原地排序 vs 非原地排序
-
缓存友好:考虑CPU缓存局部性原理