"排序就像整理书架,方法不同,效率大不同!" 📚
📊 排序算法全景图
排序算法家族:
比较排序(O(n log n)及以上):
├─ 冒泡排序 🐌 O(n²)
├─ 选择排序 🐌 O(n²)
├─ 插入排序 🐌 O(n²)
├─ 希尔排序 🚗 O(n^1.3)
├─ 归并排序 ⚡ O(n log n)
├─ 快速排序 ⚡ O(n log n)
└─ 堆排序 ⚡ O(n log n)
非比较排序(线性时间):
├─ 计数排序 ⚡ O(n+k)
├─ 桶排序 ⚡ O(n+k)
└─ 基数排序 ⚡ O(n×k)
🐌 冒泡排序(Bubble Sort)
原理
像气泡一样,大的往上冒!
第1轮:比较相邻元素,大的往后移
[5, 2, 8, 1, 9]
↓ ↓
[2, 5, 8, 1, 9] (2和5比较,交换)
↓ ↓
[2, 5, 8, 1, 9] (5和8比较,不交换)
↓ ↓
[2, 5, 1, 8, 9] (8和1比较,交换)
↓ ↓
[2, 5, 1, 8, 9] (8和9比较,不交换)
第1轮结果:最大的9"冒"到了最后!✅
代码实现
public void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
boolean swapped = false;
// 每轮把最大的"冒泡"到后面
for (int j = 0; j < n - 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(1)
稳定性:✅ 稳定
🎯 选择排序(Selection Sort)
原理
每次选出最小的,放到前面!
[5, 2, 8, 1, 9]
↑找最小的→ 1
[1, 2, 8, 5, 9]
↑找最小的→ 2
[1, 2, 8, 5, 9]
↑找最小的→ 5
[1, 2, 5, 8, 9]
↑找最小的→ 8
[1, 2, 5, 8, 9] ✅
代码实现
public void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
// 找到最小元素的索引
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:❌ 不稳定
📖 插入排序(Insertion Sort)
原理
像打扑克牌,新牌插入到合适位置!
初始:[5, 2, 8, 1, 9]
↑
已排序
步骤1:插入2
[5, 2, 8, 1, 9]
↓ ↑
[2, 5, 8, 1, 9]
步骤2:插入8
[2, 5, 8, 1, 9] (8已经在正确位置)
步骤3:插入1
[2, 5, 8, 1, 9]
←←← ↑
[1, 2, 5, 8, 9]
步骤4:插入9
[1, 2, 5, 8, 9] ✅
代码实现
public void insertionSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; 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(1)
稳定性:✅ 稳定
适用场景:小规模数据或基本有序的数据
⚡ 快速排序(Quick Sort)⭐⭐⭐⭐⭐
原理
分而治之!选一个基准,左边比它小,右边比它大!
[5, 2, 8, 1, 9, 3]
↑基准(pivot)
分区:
[2, 1, 3] < 5 < [8, 9]
递归排序:
[1, 2, 3] 5 [8, 9]
结果:
[1, 2, 3, 5, 8, 9] ✅
代码实现
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1); // 排序左半部分
quickSort(arr, pivotIndex + 1, high); // 排序右半部分
}
}
private 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++;
// 交换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;
}
// 调用
quickSort(arr, 0, arr.length - 1);
优化:三数取中法
// 选择low、mid、high的中位数作为基准
private 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);
// 现在arr[mid]是三个数的中位数
return arr[mid];
}
时间复杂度:
- 最好/平均:O(n log n) ⚡
- 最坏:O(n²) - 每次选到最小/最大值(已排序数据)
空间复杂度:O(log n) - 递归栈
稳定性:❌ 不稳定
应用:Java的Arrays.sort()对基本类型使用双轴快排
🔀 归并排序(Merge Sort)⭐⭐⭐⭐⭐
原理
分而治之!先拆分,再合并!
[5, 2, 8, 1, 9, 3]
拆分:
[5, 2, 8] [1, 9, 3]
↓ ↓
[5] [2, 8] [1] [9, 3]
↓ ↓
[2] [8] [9] [3]
合并:
[2, 8] [3, 9]
↓ ↓
[2, 5, 8] [1, 3, 9]
↓_______________↓
[1, 2, 3, 5, 8, 9] ✅
代码实现
public 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++];
}
// 复制回原数组
for (int p = 0; p < temp.length; p++) {
arr[left + p] = temp[p];
}
}
// 调用
mergeSort(arr, 0, arr.length - 1);
时间复杂度:O(n log n) - 任何情况都是!✅
空间复杂度:O(n) - 需要临时数组
稳定性:✅ 稳定
应用:Java的Arrays.sort()对对象使用归并排序(实际是TimSort)
🏔️ 堆排序(Heap Sort)⭐⭐⭐
原理
利用堆的性质:最大堆的根节点是最大值!
步骤1:建立最大堆
[5, 2, 8, 1, 9, 3]
↓
9
/ \
5 8
/ \ /
1 2 3
步骤2:交换根和最后元素,重新堆化
9和3交换 → 堆化 → 8成为新的根
重复...
最终:[1, 2, 3, 5, 8, 9] ✅
代码实现
public void heapSort(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 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 temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest);
}
}
时间复杂度:O(n log n)
空间复杂度:O(1)
稳定性:❌ 不稳定
🪣 计数排序(Counting Sort)
原理
统计每个数字出现的次数,然后按顺序输出!
数组:[3, 1, 2, 1, 3, 2, 1]
范围:1-3
统计:
count[1] = 3 (1出现3次)
count[2] = 2 (2出现2次)
count[3] = 2 (3出现2次)
输出:[1, 1, 1, 2, 2, 3, 3] ✅
代码实现
public void countingSort(int[] arr) {
if (arr.length == 0) return;
// 找出最大值
int max = arr[0];
for (int num : arr) {
if (num > max) max = num;
}
// 统计每个数字的出现次数
int[] count = new int[max + 1];
for (int num : arr) {
count[num]++;
}
// 按顺序输出
int index = 0;
for (int i = 0; i <= max; i++) {
while (count[i] > 0) {
arr[index++] = i;
count[i]--;
}
}
}
时间复杂度:O(n + k) - k是数据范围
空间复杂度:O(k)
稳定性:✅ 稳定
适用场景:数据范围不大(k较小)
📊 排序算法总结
| 排序算法 | 平均时间 | 最坏时间 | 空间 | 稳定性 | 说明 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | ✅ | 基础,慢 |
| 选择排序 | O(n²) | O(n²) | O(1) | ❌ | 简单,慢 |
| 插入排序 | O(n²) | O(n²) | O(1) | ✅ | 小数据好 |
| 希尔排序 | O(n^1.3) | O(n²) | O(1) | ❌ | 插入排序的改进 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | ❌ | 🏆最常用 |
| 归并排序 | O(n log n) | O(n log n) | O(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(n×k) | O(n×k) | O(n+k) | ✅ | 整数排序 |
🎯 如何选择排序算法?
数据规模小(<50)?
├─ 是 → 插入排序
└─ 否 → 继续
需要稳定排序?
├─ 是 → 归并排序
└─ 否 → 快速排序
数据范围小?
├─ 是 → 计数排序
└─ 否 → 快速排序 / 归并排序
内存有限?
└─ 堆排序(O(1)空间)
🏆 Java中的排序
Arrays.sort()
// 基本类型:双轴快速排序(Dual-Pivot QuickSort)
int[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr);
// 对象类型:TimSort(归并排序+插入排序)
Integer[] arr = {5, 2, 8, 1, 9};
Arrays.sort(arr);
Collections.sort()
List<Integer> list = Arrays.asList(5, 2, 8, 1, 9);
Collections.sort(list); // 底层也是TimSort
📝 总结
🎓 记忆口诀
冒泡选择和插入,
时间都是O(n²)。
快速归并和堆排,
O(n log n)称霸。
计数桶排和基数,
线性时间也疯狂。
Java快排用双轴,
对象排序用TimSort。
稳定首选归并法,
空间受限用堆排!
核心要点
- 快速排序:最常用,平均最快,但不稳定
- 归并排序:稳定,时间稳定,但空间大
- 堆排序:空间O(1),但不稳定
- 插入排序:小数据或基本有序时很快
- 计数排序:数据范围小时超快
恭喜你!🎉 你已经掌握了所有重要的排序算法!
记住:没有最好的排序算法,只有最合适的! 💪
📌 面试重点:快速排序、归并排序、堆排序
🤔 思考题:为什么Java对基本类型用快排,对对象用归并排序?
(答案:基本类型不需要稳定性,快排更快;对象需要稳定性,所以用归并排序!)