并归排序
🌟 归并排序的核心思想
想象你有一堆扑克牌需要整理。归并排序使用的是"分而治之"的思想,就像这样:
- 📚 分解: 把一堆牌分成两半,然后继续对每一半分,直到每份只剩一张牌
- 🔄 合并: 把两份有序的牌合成一份更大的有序牌组
🎯 具体步骤形象化
想象你有这样一组数字:[6, 2, 7, 3, 1, 5, 8, 4]
- 第一步:分解
[6, 2, 7, 3, 1, 5, 8, 4]
↙ ↘
[6, 2, 7, 3] [1, 5, 8, 4]
↙ ↘ ↙ ↘
[6, 2] [7, 3] [1, 5] [8, 4]
↙ ↘ ↙ ↘ ↙ ↘ ↙ ↘
[6] [2] [7] [3] [1] [5] [8] [4]
- 第二步:合并
[6] [2] [7] [3] [1] [5] [8] [4]
↘ ↙ ↘ ↙ ↘ ↙ ↘ ↙
[2,6] [3,7] [1,5] [4,8]
↘ ↙ ↘ ↙
[2,3,6,7] [1,4,5,8]
↘ ↙
[1,2,3,4,5,6,7,8]
💻 Java代码实现
public class MergeSort {
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int[] temp = new int[arr.length];
mergeSortHelper(arr, 0, arr.length - 1, temp);
}
private static void mergeSortHelper(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = left + (right - left) / 2;
// 分解左半部分
mergeSortHelper(arr, left, mid, temp);
// 分解右半部分
mergeSortHelper(arr, mid + 1, right, temp);
// 合并两部分
merge(arr, left, mid, right, temp);
}
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 左半部分起始索引
int j = mid + 1; // 右半部分起始索引
int t = 0; // 临时数组索引
// 比较左右两部分的元素,将较小的放入临时数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
// 将左边剩余元素填充进临时数组
while (i <= mid) {
temp[t++] = arr[i++];
}
// 将右边剩余元素填充进临时数组
while (j <= right) {
temp[t++] = arr[j++];
}
// 将临时数组的元素复制回原数组
t = 0;
while (left <= right) {
arr[left++] = temp[t++];
}
}
}
左右有剩余的情况分析
好的,让我用具体例子来解释这两步!
🌟 例子1:左边有剩余元素
假设要合并两个有序数组:
左边:[1, 3, 5, 9]
右边:[2, 4, 6]
合并过程:
1. 开始比较合并:
[1] -> 1 < 2
[1,2] -> 3 > 2
[1,2,3] -> 3 < 4
[1,2,3,4] -> 5 > 4
[1,2,3,4,5] -> 5 < 6
[1,2,3,4,5,6] -> 右边处理完了!
2. 左边还剩[9]
// 将左边剩余元素填充进临时数组
把9直接放入临时数组
最终结果:[1,2,3,4,5,6,9]
🌟 例子2:右边有剩余元素
假设要合并两个有序数组:
左边:[1, 3, 5]
右边:[2, 4, 6, 8, 10]
合并过程:
1. 开始比较合并:
[1] -> 1 < 2
[1,2] -> 3 > 2
[1,2,3] -> 3 < 4
[1,2,3,4] -> 5 > 4
[1,2,3,4,5] -> 5 < 6
[1,2,3,4,5,6] -> 左边处理完了!
2. 右边还剩[8, 10]
// 将右边剩余元素填充进临时数组
把8,10直接放入临时数组
最终结果:[1,2,3,4,5,6,8,10]
💻 代码解释
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 左边起始位置
int j = mid + 1; // 右边起始位置
int t = 0; // 临时数组索引
// 比较左右两边的元素,较小的放入临时数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
// 左边有剩余元素
while (i <= mid) {
// 例子1中的情况:把剩下的9放入临时数组
temp[t++] = arr[i++];
}
// 右边有剩余元素
while (j <= right) {
// 例子2中的情况:把剩下的8,10放入临时数组
temp[t++] = arr[j++];
}
}
📝 要点说明
-
为什么会有剩余?
- 因为两个数组长度可能不同
- 或者一边的元素都比另一边小/大
-
为什么直接放入?
- 剩余的元素已经是有序的
- 不需要再比较,直接放入即可
-
注意事项
- 这两个while循环最多只会执行一个
- 要么左边有剩余,要么右边有剩余,要么都没有剩余
🎯 图解示例
例子1的过程:
步骤1:[1,3,5,9] 和 [2,4,6] 比较合并
temp: [1]
temp: [1,2]
temp: [1,2,3]
temp: [1,2,3,4]
temp: [1,2,3,4,5]
temp: [1,2,3,4,5,6]
步骤2:处理剩余的9
temp: [1,2,3,4,5,6,9] ← 直接放入
```好的,让我用具体例子来解释这两步!
### 🌟 例子1:左边有剩余元素
假设要合并两个有序数组:
左边:[1, 3, 5, 9]
右边:[2, 4, 6]
合并过程:
-
开始比较合并: [1] -> 1 < 2 [1,2] -> 3 > 2 [1,2,3] -> 3 < 4 [1,2,3,4] -> 5 > 4 [1,2,3,4,5] -> 5 < 6 [1,2,3,4,5,6] -> 右边处理完了!
-
左边还剩[9] // 将左边剩余元素填充进临时数组 把9直接放入临时数组
最终结果:[1,2,3,4,5,6,9]
### 🌟 例子2:右边有剩余元素
假设要合并两个有序数组:
左边:[1, 3, 5] 右边:[2, 4, 6, 8, 10]
合并过程:
-
开始比较合并: [1] -> 1 < 2 [1,2] -> 3 > 2 [1,2,3] -> 3 < 4 [1,2,3,4] -> 5 > 4 [1,2,3,4,5] -> 5 < 6 [1,2,3,4,5,6] -> 左边处理完了!
-
右边还剩[8, 10] // 将右边剩余元素填充进临时数组 把8,10直接放入临时数组
最终结果:[1,2,3,4,5,6,8,10]
### 💻 代码解释
```java
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 左边起始位置
int j = mid + 1; // 右边起始位置
int t = 0; // 临时数组索引
// 比较左右两边的元素,较小的放入临时数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
// 左边有剩余元素
while (i <= mid) {
// 例子1中的情况:把剩下的9放入临时数组
temp[t++] = arr[i++];
}
// 右边有剩余元素
while (j <= right) {
// 例子2中的情况:把剩下的8,10放入临时数组
temp[t++] = arr[j++];
}
}
📝 要点说明
-
为什么会有剩余?
- 因为两个数组长度可能不同
- 或者一边的元素都比另一边小/大
-
为什么直接放入?
- 剩余的元素已经是有序的
- 不需要再比较,直接放入即可
-
注意事项
- 这两个while循环最多只会执行一个
- 要么左边有剩余,要么右边有剩余,要么都没有剩余
🎯 图解示例
例子1的过程:
步骤1:[1,3,5,9] 和 [2,4,6] 比较合并
temp: [1]
temp: [1,2]
temp: [1,2,3]
temp: [1,2,3,4]
temp: [1,2,3,4,5]
temp: [1,2,3,4,5,6]
步骤2:处理剩余的9
temp: [1,2,3,4,5,6,9] ← 直接放入
📊 归并排序的特点
- 时间复杂度: O(nlogn)
- 空间复杂度: O(n)
- 稳定性: 稳定排序
- 优点:
- 效率高且稳定
- 适合大数据量排序
- 易于并行实现
- 缺点:
- 需要额外的空间
- 对于小规模数据,可能不如插入排序等简单排序算法
🔄 流程图
graph TD
A[开始] --> B[判断数组长度是否>1]
B -- 是 --> C[计算中间位置mid]
B -- 否 --> J[结束]
C --> D[递归排序左半部分]
C --> E[递归排序右半部分]
D --> F[合并两个有序数组]
E --> F
F --> G[创建临时数组]
G --> H[比较并合并元素]
H --> I[复制回原数组]
I --> J
🎯 实际应用场景
- 外部排序: 处理大文件排序
- 数据库: 优化查询性能
- 文件系统: 文件归并
- 大数据处理: MapReduce框架
算法详细步骤打印
public class MergeSort {
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
System.out.println("开始归并排序...");
System.out.printf("原始数组:%s\n", arrayToString(arr));
System.out.println("=" .repeat(50) + "\n");
int[] temp = new int[arr.length];
mergeSortHelper(arr, 0, arr.length - 1, temp);
}
private static void mergeSortHelper(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = left + (right - left) / 2;
System.out.printf("\n划分区间 [%d, %d]:\n", left, right);
System.out.printf("左半部分:%s\n",
arrayToString(Arrays.copyOfRange(arr, left, mid + 1)));
System.out.printf("右半部分:%s\n",
arrayToString(Arrays.copyOfRange(arr, mid + 1, right + 1)));
// 分解左半部分
mergeSortHelper(arr, left, mid, temp);
// 分解右半部分
mergeSortHelper(arr, mid + 1, right, temp);
// 合并两部分
merge(arr, left, mid, right, temp);
}
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
System.out.printf("\n开始合并区间 [%d, %d] 和 [%d, %d]:\n",
left, mid, mid + 1, right);
System.out.printf("左半部分:%s\n",
arrayToString(Arrays.copyOfRange(arr, left, mid + 1)));
System.out.printf("右半部分:%s\n",
arrayToString(Arrays.copyOfRange(arr, mid + 1, right + 1)));
int i = left; // 左半部分起始索引
int j = mid + 1; // 右半部分起始索引
int t = 0; // 临时数组索引
// 比较左右两部分的元素,将较小的放入临时数组
while (i <= mid && j <= right) {
System.out.printf("\n比较:arr[%d]=%d 和 arr[%d]=%d\n", i, arr[i], j, arr[j]);
if (arr[i] <= arr[j]) {
System.out.printf("选择左边的 %d\n", arr[i]);
temp[t++] = arr[i++];
} else {
System.out.printf("选择右边的 %d\n", arr[j]);
temp[t++] = arr[j++];
}
System.out.printf("当前临时数组:%s\n",
arrayToString(Arrays.copyOfRange(temp, 0, t)));
}
// 将左边剩余元素填充进临时数组
while (i <= mid) {
System.out.printf("\n左边剩余元素:%d\n", arr[i]);
temp[t++] = arr[i++];
System.out.printf("当前临时数组:%s\n",
arrayToString(Arrays.copyOfRange(temp, 0, t)));
}
// 将右边剩余元素填充进临时数组
while (j <= right) {
System.out.printf("\n右边剩余元素:%d\n", arr[j]);
temp[t++] = arr[j++];
System.out.printf("当前临时数组:%s\n",
arrayToString(Arrays.copyOfRange(temp, 0, t)));
}
// 将临时数组的元素复制回原数组
t = 0;
int tempLeft = left;
System.out.println("\n将临时数组复制回原数组:");
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
System.out.printf("arr[%d] = %d\n", tempLeft, temp[t]);
tempLeft++;
t++;
}
System.out.printf("\n合并后数组:%s\n", arrayToString(arr));
System.out.println("-" .repeat(50));
}
private static String arrayToString(int[] arr) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
sb.append(arr[i]);
if (i < arr.length - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22};
mergeSort(arr);
System.out.println("\n最终排序结果:" + arrayToString(arr));
}
}
运行结果(部分): 好的,我来展示完整的归并排序运行结果:
开始归并排序...
原始数组:[64, 34, 25, 12, 22]
==================================================
划分区间 [0, 4]:
左半部分:[64, 34]
右半部分:[25, 12, 22]
划分区间 [0, 1]:
左半部分:[64]
右半部分:[34]
开始合并区间 [0, 0] 和 [1, 1]:
左半部分:[64]
右半部分:[34]
比较:arr[0]=64 和 arr[1]=34
选择右边的 34
当前临时数组:[34]
左边剩余元素:64
当前临时数组:[34, 64]
将临时数组复制回原数组:
arr[0] = 34
arr[1] = 64
合并后数组:[34, 64, 25, 12, 22]
--------------------------------------------------
划分区间 [2, 4]:
左半部分:[25]
右半部分:[12, 22]
划分区间 [3, 4]:
左半部分:[12]
右半部分:[22]
开始合并区间 [3, 3] 和 [4, 4]:
左半部分:[12]
右半部分:[22]
比较:arr[3]=12 和 arr[4]=22
选择左边的 12
当前临时数组:[12]
右边剩余元素:22
当前临时数组:[12, 22]
将临时数组复制回原数组:
arr[3] = 12
arr[4] = 22
合并后数组:[34, 64, 25, 12, 22]
--------------------------------------------------
开始合并区间 [2, 2] 和 [3, 4]:
左半部分:[25]
右半部分:[12, 22]
比较:arr[2]=25 和 arr[3]=12
选择右边的 12
当前临时数组:[12]
比较:arr[2]=25 和 arr[4]=22
选择右边的 22
当前临时数组:[12, 22]
左边剩余元素:25
当前临时数组:[12, 22, 25]
将临时数组复制回原数组:
arr[2] = 12
arr[3] = 22
arr[4] = 25
合并后数组:[34, 64, 12, 22, 25]
--------------------------------------------------
开始合并区间 [0, 1] 和 [2, 4]:
左半部分:[34, 64]
右半部分:[12, 22, 25]
比较:arr[0]=34 和 arr[2]=12
选择右边的 12
当前临时数组:[12]
比较:arr[0]=34 和 arr[3]=22
选择右边的 22
当前临时数组:[12, 22]
比较:arr[0]=34 和 arr[4]=25
选择右边的 25
当前临时数组:[12, 22, 25]
左边剩余元素:34
当前临时数组:[12, 22, 25, 34]
左边剩余元素:64
当前临时数组:[12, 22, 25, 34, 64]
将临时数组复制回原数组:
arr[0] = 12
arr[1] = 22
arr[2] = 25
arr[3] = 34
arr[4] = 64
合并后数组:[12, 22, 25, 34, 64]
--------------------------------------------------
最终排序结果:[12, 22, 25, 34, 64]
这个完整的输出展示了归并排序的整个过程:
- 首先将数组分成 [64, 34] 和 [25, 12, 22]
- 对左边 [64, 34] 进行排序,得到 [34, 64]
- 对右边 [25, 12, 22] 进行排序,得到 [12, 22, 25]
- 最后合并两个有序数组,得到最终结果 [12, 22, 25, 34, 64]
每一步都包含了分解、排序和合并的详细过程。
归并排序的关键点:
- 分解过程:
[64, 34, 25, 12, 22]
↓
[64, 34] [25, 12, 22]
↓ ↓
[64] [34] [25] [12, 22]
↓
[12] [22]
- 合并过程:
[64] [34] → [34, 64]
[25] [12, 22] → [12, 22, 25]
[34, 64] [12, 22, 25] → [12, 22, 25, 34, 64]
- 临时数组的作用:
- 用于存储合并过程中的有序序列
- 避免在原数组上直接操作
- 最后将结果复制回原数组
- 递归的终止条件:
left >= right:表示区间长度小于等于1,不需要再分解
这样的详细输出可以帮助我们更好地理解归并排序的工作原理和过程。
int mid = left + (right - left) / 2; 这里为什么要这么划分呢
这个写法有两个目的:
- 数学上等价于
(left + right) / 2,但更安全:
例如:left = 2147483645, right = 2147483647
方式一:(left + right) / 2
2147483645 + 2147483647 = 4294967292 // 超出int范围,可能溢出
方式二:left + (right - left) / 2
2147483645 + (2 / 2) = 2147483646 // 不会溢出
- 更容易理解划分的含义:
right - left // 计算区间长度
(right - left)/2 // 计算区间长度的一半
left + ... // 从左边界开始偏移这个长度
让我们看一些具体例子:
例1:[64, 34, 25, 12, 22]
left = 0, right = 4
mid = 0 + (4 - 0) / 2
= 0 + 4/2
= 0 + 2
= 2
划分结果:
[64, 34 | 25 | 12, 22]
左半部分 mid 右半部分
例2:[64, 34]
left = 0, right = 1
mid = 0 + (1 - 0) / 2
= 0 + 1/2
= 0 + 0
= 0
划分结果:
[64 | 34]
左 右
图解说明:
数组:[64, 34, 25, 12, 22]
索引: 0 1 2 3 4
计算mid:
1. right - left = 4 - 0 = 4
2. (right - left)/2 = 4/2 = 2
3. left + 2 = 0 + 2 = 2
结果:
[64, 34 | 25 | 12, 22]
0 1 2 3 4
左半部分 mid 右半部分
生活类比:
想象你在分配一叠书:
- left 是第一本书的编号
- right 是最后一本书的编号
- mid 就是从左边数一半位置的书的编号
例如:编号1到9的书
left = 1, right = 9
mid = 1 + (9 - 1) / 2
= 1 + 8/2
= 1 + 4
= 5
[1,2,3,4 | 5 | 6,7,8,9]
左半部分 中间 右半部分
为什么这种写法更好:
- 防止溢出:
当数字很大时:
left = 2147483645
right = 2147483647
(left + right) / 2 // 可能溢出
left + (right - left) / 2 // 安全
- 逻辑更清晰:
right - left // 区间长度
(right - left)/2 // 一半长度
left + ... // 从左边开始数
- 数学上等价:
left + (right - left) / 2
= left + (right/2 - left/2)
= (2*left + right - left) / 2
= (left + right) / 2
所以这种写法:
- 更安全(防止整数溢出)
- 更直观(体现了从左边偏移半个区间的思想)
- 更常用(这是业界推荐的写法)
桶排序
我来用生动形象的方式讲解桶排序(Bucket Sort)!
🌟 生活中的类比
想象你是一个整理扑克牌的人:
- 你有13个桶(盒子),分别代表A-K
- 拿到一堆扑克牌时,你会按照点数把牌分别放入对应的桶中
- 最后按顺序从A到K的桶依次取出牌
这就是桶排序的基本思想!
📝 原理解释
桶排序的核心思想是:
- 将数据分散到有限数量的桶中
- 对每个桶中的数据进行排序
- 最后按顺序将桶中的数据合并
🔄 流程图
graph TD
A[输入数组] --> B[创建桶]
B --> C[将元素分配到桶中]
C --> D[对每个桶内部排序]
D --> E[合并所有桶]
E --> F[排序完成]
💻 Java代码示例
public class BucketSort {
public static void bucketSort(float[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
// 1. 创建桶
ArrayList<Float>[] buckets = new ArrayList[arr.length];
for (int i = 0; i < arr.length; i++) {
buckets[i] = new ArrayList<Float>();
}
// 2. 将元素分配到桶中
for (float num : arr) {
int bucketIndex = (int) (num * arr.length);
buckets[bucketIndex].add(num);
}
// 3. 对每个桶内部排序
for (ArrayList<Float> bucket : buckets) {
Collections.sort(bucket);
}
// 4. 合并所有桶
int index = 0;
for (ArrayList<Float> bucket : buckets) {
for (float num : bucket) {
arr[index++] = num;
}
}
}
}
⭐ 特点总结
优点:
- 适合均匀分布的数据
- 可以并行处理
- 时间复杂度可以达到O(n)
缺点:
- 需要额外的空间
- 对数据分布有要求
- 浮点数据效果更好
🎯 适用场景
- 数据分布相对均匀
- 数据范围已知
- 有足够的额外空间
- 适合外部排序
📊 时间复杂度
- 平均情况:O(n + k)
- 最坏情况:O(n²)
- 空间复杂度:O(n + k)
其中,n是元素个数,k是桶的数量
🌰 具体例子
假设要排序:[0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
- 创建10个桶
- 0.78会放入第7个桶
- 0.17会放入第1个桶
- 以此类推...
- 最后合并所有桶的内容
这样就完成了排序!
让我详细解释一下桶的计算过程!
🔢 桶索引的计算公式
在示例代码中,桶索引的计算公式是:
int bucketIndex = (int) (num * arr.length);
假设数组长度是10,让我们一步步计算:
📝 详细计算过程
-
对于 0.78:
- 0.78 × 10 = 7.8
- (int)7.8 = 7
- 所以 0.78 进入第7个桶
-
对于 0.17:
- 0.17 × 10 = 1.7
- (int)1.7 = 1
- 所以 0.17 进入第1个桶
🎨 图解示例
数值范围: 0.0 ~ 1.0
桶的分布:
桶0: [0.0 - 0.1)
桶1: [0.1 - 0.2)
桶2: [0.2 - 0.3)
桶3: [0.3 - 0.4)
桶4: [0.4 - 0.5)
桶5: [0.5 - 0.6)
桶6: [0.6 - 0.7)
桶7: [0.7 - 0.8)
桶8: [0.8 - 0.9)
桶9: [0.9 - 1.0]
🌟 完整示例
让我们看看整个数组的分配过程:
原始数组: [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
分配过程:
0.78 -> 7号桶 (0.78 × 10 = 7.8 → 7)
0.17 -> 1号桶 (0.17 × 10 = 1.7 → 1)
0.39 -> 3号桶 (0.39 × 10 = 3.9 → 3)
0.26 -> 2号桶 (0.26 × 10 = 2.6 → 2)
0.72 -> 7号桶 (0.72 × 10 = 7.2 → 7)
0.94 -> 9号桶 (0.94 × 10 = 9.4 → 9)
0.21 -> 2号桶 (0.21 × 10 = 2.1 → 2)
0.12 -> 1号桶 (0.12 × 10 = 1.2 → 1)
0.23 -> 2号桶 (0.23 × 10 = 2.3 → 2)
0.68 -> 6号桶 (0.68 × 10 = 6.8 → 6)
📌 重要说明
- 这种计算方式假设数据在[0,1)范围内
- 如果数据范围不在[0,1)之间,需要先进行归一化处理
- 桶的数量会影响排序效率,通常取决于数据分布情况
🔄 对于其他范围的数据
如果数据范围是[min, max],计算桶索引的公式需要调整为:
int bucketIndex = (int) ((num - min) * (bucketCount - 1) / (max - min));
这样就能处理任意范围的数据了!
为什么是ArrayList来实现
让我解释为什么使用ArrayList来实现桶排序,以及其他可能的实现方式。
🤔 为什么使用ArrayList?
-
动态扩容
- ArrayList可以自动扩容
- 我们无法预知每个桶中会有多少元素
- 不需要手动管理数组大小
-
便利性
- 提供了现成的
add()、sort()等方法 - 使用起来更简单直观
- 代码更加简洁
- 提供了现成的
🔄 其他实现方式
我们也可以使用链表或普通数组来实现,让我们看看不同实现方式的对比:
// 1. 使用普通数组实现
public class BucketSortWithArray {
public static void bucketSort(float[] arr) {
// 创建二维数组作为桶
float[][] buckets = new float[arr.length][arr.length];
int[] bucketSizes = new int[arr.length]; // 记录每个桶中元素数量
// 分配元素到桶中
for (float num : arr) {
int bucketIndex = (int) (num * arr.length);
buckets[bucketIndex][bucketSizes[bucketIndex]++] = num;
}
// 对每个桶排序并合并
int index = 0;
for (int i = 0; i < buckets.length; i++) {
if (bucketSizes[i] > 0) {
// 只对有效元素进行排序
float[] temp = Arrays.copyOf(buckets[i], bucketSizes[i]);
Arrays.sort(temp);
for (float num : temp) {
arr[index++] = num;
}
}
}
}
}
// 2. 使用链表实现
public class BucketSortWithLinkedList {
public static void bucketSort(float[] arr) {
LinkedList<Float>[] buckets = new LinkedList[arr.length];
// 初始化桶
for (int i = 0; i < arr.length; i++) {
buckets[i] = new LinkedList<>();
}
// 分配元素到桶中
for (float num : arr) {
int bucketIndex = (int) (num * arr.length);
buckets[bucketIndex].add(num);
}
// 对每个桶排序并合并
int index = 0;
for (LinkedList<Float> bucket : buckets) {
if (!bucket.isEmpty()) {
Collections.sort(bucket);
for (float num : bucket) {
arr[index++] = num;
}
}
}
}
}
📊 不同实现方式的对比
| 特性 | ArrayList | 数组 | LinkedList |
|---|---|---|---|
| 空间利用 | 较好 | 可能浪费 | 最优 |
| 动态扩容 | 自动 | 需手动处理 | 自动 |
| 插入性能 | O(1)均摊 | O(1) | O(1) |
| 内存连续性 | 是 | 是 | 否 |
| 代码复杂度 | 简单 | 较复杂 | 简单 |
💡 选择建议
-
使用ArrayList的情况:
- 数据量适中
- 需要简洁的代码
- 对空间要求不是特别严格
-
使用数组的情况:
- 对性能要求极高
- 能够预估每个桶的大小
- 内存要求严格
-
使用LinkedList的情况:
- 数据量大且分布不均
- 内存碎片不是问题
- 需要频繁的插入操作
🎯 实际应用建议
在实际应用中,除非有特殊需求,建议使用ArrayList实现,因为:
- 代码更加清晰易懂
- 维护成本低
- 性能表现足够好
- Java集合框架提供了完善的方法支持
Collections.sort()实现原理
让我来详细解释 Collections.sort() 的实现原理!
🔍 Collections.sort() 的内部实现
在Java中,Collections.sort() 实际上是调用 Arrays.sort() 方法,具体实现如下:
public static <T extends Comparable<? super T>> void sort(List<T> list) {
// 转换为数组
Object[] a = list.toArray();
// 调用Arrays.sort()
Arrays.sort(a);
// 将排序后的数组复制回列表
ListIterator<T> i = list.listIterator();
for (Object e : a) {
i.next();
i.set((T) e);
}
}
📚 Arrays.sort() 的算法选择
Arrays.sort() 会根据不同情况选择不同的排序算法:
-
对于基本类型数组:
- 使用双轴快速排序(Dual-Pivot QuickSort)
- JDK 7之后采用这种优化版快排
-
对于对象数组:
- 使用改进的归并排序(TimSort)
- JDK 7之后采用这种混合排序算法
🔄 TimSort 算法简介
TimSort 是一个混合排序算法,结合了归并排序和插入排序的优点:
// TimSort的核心思想伪代码
public void timSort(T[] arr) {
// 1. 首先将数组分成多个run(已排序的子数组)
// 最小run长度通常是32
int minRun = getMinRunLength(arr.length);
// 2. 对小的run使用插入排序
for (int i = 0; i < arr.length; i += minRun) {
insertionSort(arr, i, Math.min(i + minRun, arr.length));
}
// 3. 合并相邻的run
for (int size = minRun; size < arr.length; size = 2 * size) {
for (int left = 0; left < arr.length; left += 2 * size) {
int mid = left + size;
int right = Math.min(left + 2 * size, arr.length);
if (mid < right) {
merge(arr, left, mid, right);
}
}
}
}
🌟 TimSort 的主要特点
-
自适应性
- 能够利用数据中天然存在的顺序
- 对部分有序的数据特别高效
-
稳定性
- 保证相等元素的相对位置不变
-
混合策略
// 根据数组长度选择排序策略
if (length < INSERTIONSORT_THRESHOLD) {
// 小数组使用插入排序
insertionSort(arr, left, right);
} else {
// 大数组使用TimSort
timSort(arr);
}
⚡ 性能特征
-
时间复杂度:
- 最好情况:O(n)
- 平均情况:O(n log n)
- 最坏情况:O(n log n)
-
空间复杂度:
- O(n)
🎯 优化要点
- 二分查找优化
// 插入排序中使用二分查找找到插入位置
private static int binarySearch(T[] arr, T key, int low, int high) {
while (low < high) {
int mid = (low + high) >>> 1;
if (key.compareTo(arr[mid]) < 0)
high = mid;
else
low = mid + 1;
}
return low;
}
- 小数组优化
// 对于小数组,直接使用插入排序
if (length < 47) {
insertionSort(arr, left, right);
return;
}
- 归并优化
// 合并前检查是否已经有序
if (arr[mid - 1].compareTo(arr[mid]) <= 0) {
return; // 已经有序,无需合并
}
📝 总结
- Collections.sort() 实际上是复杂的混合排序算法
- 根据数据特点自动选择最优策略
- 结合了多种排序算法的优点
- 在实际应用中性能表现优异
这就是为什么在桶排序中使用 Collections.sort() 能够获得很好的性能!每个桶内的数据量通常不会太大,而且可能部分有序,非常适合 TimSort 算法处理。