Java基础——排序算法
排序算法不管是考试、面试、还是日常开发中都是一个特别高频的点。下面对八种排序算法做简单的介绍。
1. 冒泡排序(Bubble Sort)
原理:相邻元素比较,每一轮将最大元素“冒泡”到末尾。
示例数组:[5, 3, 8, 1, 2]
public static void bubbleSort(int[] arr) {for (int i = 0; i < arr.length - 1; i++) {boolean swapped = false;for (int j = 0; j < arr.length - i - 1; j++) {if (arr[j] > arr[j + 1]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;swapped = true;}}System.out.println("第 " + (i + 1) + " 轮后: " + Arrays.toString(arr));if (!swapped) break;}
}
输出:
第 1 轮后: [3, 5, 1, 2, 8]
第 2 轮后: [3, 1, 2, 5, 8]
第 3 轮后: [1, 2, 3, 5, 8]
第 4 轮后: [1, 2, 3, 5, 8]
时间复杂度:
- 最好情况:O(n)(已有序时一轮扫描结束)
- 最坏/平均:O(n²)(双重循环嵌套)
空间复杂度:O(1)(原地排序,仅需常数空间)
计算原理:
- 外层循环最多执行 n-1 次,内层循环每次执行 n-i-1 次
- 总比较次数 ≈ (n-1) + (n-2) + … + 1 = n(n-1)/2 → O(n²)
2. 选择排序(Selection Sort)
原理:每轮选择最小的元素,放到已排序部分的末尾。
示例数组:[5, 3, 8, 1, 2]
public static 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[i];arr[i] = arr[minIndex];arr[minIndex] = temp;System.out.println("第 " + (i + 1) + " 轮后: " + Arrays.toString(arr));}
}
输出:
第 1 轮后: [1, 3, 8, 5, 2]
第 2 轮后: [1, 2, 8, 5, 3]
第 3 轮后: [1, 2, 3, 5, 8]
第 4 轮后: [1, 2, 3, 5, 8]
时间复杂度:
- 所有情况:O(n²)(必须完整执行双重循环)
空间复杂度:O(1)(原地交换)
计算原理:
- 外层循环 n-1 次,内层循环每次执行 n-i-1 次
- 总比较次数 = (n-1) + (n-2) + … + 1 = n(n-1)/2 → O(n²)
3. 插入排序(Insertion Sort)
原理:将未排序元素插入已排序部分的正确位置。
示例数组:[5, 3, 8, 1, 2]
public static void insertionSort(int[] arr) {for (int i = 1; i < arr.length; i++) {int key = arr[i];int j = i - 1;while (j >= 0 && arr[j] > key) {arr[j + 1] = arr[j];j--;}arr[j + 1] = key;System.out.println("插入 " + key + " 后: " + Arrays.toString(arr));}
}
输出:
插入 3 后: [3, 5, 8, 1, 2]
插入 8 后: [3, 5, 8, 1, 2]
插入 1 后: [1, 3, 5, 8, 2]
插入 2 后: [1, 2, 3, 5, 8]
时间复杂度:
- 最好:O(n)(已有序时仅需线性扫描)
- 最坏/平均:O(n²)(每次插入需移动大量元素)
空间复杂度:O(1)
计算原理:
- 外层循环 n-1 次,内层循环平均移动 i/2 次
- 总操作次数 ≈ 1 + 2 + … + (n-1) = n(n-1)/2 → O(n²)
4. 快速排序(Quick Sort)
原理:通过基准分区,递归排序左右子数组。
示例数组:[5, 3, 8, 1, 2]
public static void quickSort(int[] arr, int low, int high) {if (low < high) {int pi = partition(arr, low, high);System.out.println("基准 " + arr[pi] + " 分区后: " + Arrays.toString(arr));quickSort(arr, low, pi - 1);quickSort(arr, pi + 1, high);}
}private static int partition(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++;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;
}
输出:
基准 2 分区后: [1, 2, 8, 5, 3]
基准 1 分区后: [1, 2, 8, 5, 3]
基准 3 分区后: [1, 2, 3, 5, 8]
基准 8 分区后: [1, 2, 3, 5, 8]
时间复杂度:
- 最好/平均:O(n log n)(每次分区将数组分成两半)
- 最坏:O(n²)(每次分区极不平衡,如已有序数组)
空间复杂度:O(log n)(递归调用栈的深度)
计算原理:
- 理想情况:每次分区后左右子数组大小相等,递归深度 log n,每层处理 O(n) → O(n log n)
- 最坏情况:每次分区后一个子数组为空,递归深度 n → O(n²)
5. 归并排序(Merge Sort)
原理:分治法,将数组拆分为两半,合并有序子数组。
示例数组:[5, 3, 8, 1, 2]
public static void mergeSort(int[] arr, int left, int right) {if (left < right) {int mid = (left + right) / 2;mergeSort(arr, left, mid);mergeSort(arr, mid + 1, right);merge(arr, left, mid, right);System.out.println("合并后: " + Arrays.toString(arr));}
}private static void merge(int[] arr, int left, int mid, int right) {int[] temp = new int[right - left + 1];int i = left, j = mid + 1, 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);
}
输出:
合并后: [3, 5, 8, 1, 2]
合并后: [3, 5, 1, 8, 2]
合并后: [1, 2, 3, 5, 8]
时间复杂度:
- 所有情况:O(n log n)(稳定分治策略)
空间复杂度:O(n)(合并时需要临时数组)
计算原理:
- 递归树深度为 log n,每层合并总时间 O(n) → O(n log n)
- 合并操作需要额外空间存储临时数组
6. 堆排序(Heap Sort)
原理:构建最大堆,依次将堆顶元素与末尾交换。
示例数组:[5, 3, 8, 1, 2]
public static void heapSort(int[] arr) {int n = arr.length;for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i);for (int i = n - 1; i > 0; i--) {int temp = arr[0];arr[0] = arr[i];arr[i] = temp;System.out.println("交换堆顶后: " + Arrays.toString(arr));heapify(arr, i, 0);}
}private static void heapify(int[] arr, int n, int i) {int largest = i, left = 2 * i + 1, 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);}
}
输出:
交换堆顶后: [5, 3, 2, 1, 8]
交换堆顶后: [1, 3, 2, 5, 8]
交换堆顶后: [2, 1, 3, 5, 8]
交换堆顶后: [1, 2, 3, 5, 8]
时间复杂度:
- 所有情况:O(n log n)(建堆 O(n),每次调整堆 O(log n))
空间复杂度:O(1)(原地排序)
计算原理:
- 建堆时间复杂度为 O(n)(非叶子节点下沉操作)
- 每次交换堆顶后调整堆需要 O(log n) 时间,共 n-1 次 → O(n log n)
7. 希尔排序(Shell Sort)
原理:按间隔分组,逐步缩小间隔进行插入排序。
示例数组:[5, 3, 8, 1, 2]
public static void shellSort(int[] arr) {int n = arr.length;for (int gap = n / 2; gap > 0; gap /= 2) {for (int i = gap; i < n; i++) {int temp = arr[i], j;for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {arr[j] = arr[j - gap];}arr[j] = temp;}System.out.println("间隔 " + gap + " 排序后: " + Arrays.toString(arr));}
}
输出:
间隔 2 排序后: [5, 1, 2, 3, 8]
间隔 1 排序后: [1, 2, 3, 5, 8]
时间复杂度:
- 取决于间隔序列:
- 最坏 O(n²)(如使用原始希尔序列)
- 平均 O(n log n)(如使用Knuth序列)
空间复杂度:O(1)
计算原理:
- 通过分组减少数据移动距离
- 不同间隔序列的时间复杂度不同,例如:
- 希尔原始序列(n/2^k):O(n²)
- Knuth序列(3k-1):O(n1.5)
8. 基数排序(Radix Sort)
原理:按位数从低到高排序(需稳定排序辅助)。
示例数组:[170, 45, 75, 90, 802, 24, 2, 66]
public static void radixSort(int[] arr) {int max = Arrays.stream(arr).max().getAsInt();for (int exp = 1; max / exp > 0; exp *= 10) {countingSort(arr, exp);System.out.println("按第 " + exp + " 位排序后: " + Arrays.toString(arr));}
}private static void countingSort(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);
}
输出:
按第 1 位排序后: [170, 90, 802, 2, 24, 45, 75, 66]
按第 10 位排序后: [802, 2, 24, 45, 66, 170, 75, 90]
按第 100 位排序后: [2, 24, 45, 66, 75, 90, 170, 802]
时间复杂度:O(nk)(k为最大数字位数)
空间复杂度:O(n + k)(计数排序的额外空间)
计算原理:
- 对每个位数进行稳定排序(如计数排序)
- 若最大数为 d 位,则进行 d 轮排序,每轮 O(n) → 总时间 O(dn)
总结表格
排序算法 | 最好时间 | 平均时间 | 最坏时间 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | 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 log n) | O(n²) | O(log n) | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
希尔排序 | O(n log n) | 取决于间隔 | O(n²) | O(1) | 不稳定 |
基数排序 | O(nk) | O(nk) | O(nk) | O(n + k) | 稳定 |
复杂度计算核心思想
-
循环嵌套次数:
- 双重循环(如冒泡、选择、插入)→ O(n²)
- 分治法(快速、归并)→ 递归深度 log n × 每层 O(n) → O(n log n)
-
数据移动代价:
- 插入排序的移动次数与逆序对数量相关
- 堆排序的调整代价与树高度 log n 相关
-
额外空间使用:
- 递归算法的栈空间(快速排序 O(log n))
- 合并排序的临时数组(O(n))
- 基数排序的计数数组(O(n + k))