排序算法是计算机科学中基础而又重要的部分,其中基数排序(Radix Sort)是一种高效的非比较排序算法。基数排序有两种主要的实现方式:LSD(Least Significant Digit)基数排序和MSD(Most Significant Digit)基数排序。本文将详细介绍这两种排序算法,并提供Java实现代码。
什么是基数排序?
基数排序是一种基于“位”的排序算法,通过将数据按位进行多次排序来实现最终的排序效果。它特别适用于整数和字符串的排序。基数排序有两种主要的变种:LSD和MSD,它们的主要区别在于排序的方向。
LSD基数排序
LSD基数排序从数据的最低有效位(最右边的位)开始排序,一直排序到最高有效位。每一轮排序都使用稳定的计数排序来确保排序的正确性。
LSD基数排序的步骤
- 确定最大数的位数:找出数组中最大数的位数 (d)。
- 从最低有效位开始排序:对每一位进行排序,使用计数排序(或桶排序)来保证排序的稳定性。
MSD基数排序
MSD基数排序从数据的最高有效位(最左边的位)开始排序,逐位向最低有效位排序。每一轮排序都将数据划分为多个子数组,并递归地对这些子数组进行排序。
MSD基数排序的步骤
- 确定最大数的位数:找出数组中最大数的位数 (d)。
- 从最高有效位开始排序:
- 对当前位进行排序,使用计数排序或桶排序来保证排序的稳定性。
- 对每个子数组递归地进行排序,处理下一位。
抽象计数排序
为了提高代码的复用性,我们将计数排序抽象出来,并在LSD和MSD基数排序中调用。
抽象计数排序的Java实现
import java.util.Arrays;
public class CountingSort {
public static void countingSort(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10];
Arrays.fill(count, 0);
// 统计每个桶中的计数
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
// 计算累加计数
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组
for (int i = n - 1; i >= 0; i--) {
int index = (arr[i] / exp) % 10;
output[count[index] - 1] = arr[i];
count[index]--;
}
// 将排序后的数组复制回原数组
System.arraycopy(output, 0, arr, 0, n);
}
}
LSD基数排序的实现
使用抽象出来的计数排序方法来实现LSD基数排序:
import java.util.Arrays;
public class LSDRadixSort {
private static int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
public static void radixSort(int[] arr) {
int max = getMax(arr);
// 从个位开始,对每个位进行排序
for (int exp = 1; max / exp > 0; exp *= 10) {
CountingSort.countingSort(arr, exp);
}
}
public static void main(String[] args) {
int[] arr = {170, 45, 75, 90, 802, 24, 2, 66};
System.out.println("Original array: " + Arrays.toString(arr));
radixSort(arr);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
MSD基数排序的实现
使用抽象出来的计数排序方法来实现MSD基数排序:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MSDRadixSort {
private static int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
private static void msdRadixSort(int[] arr, int exp) {
if (arr.length <= 1) {
return;
}
CountingSort.countingSort(arr, exp);
// 创建桶以保存每个数字的分割结果
List<List<Integer>> buckets = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
buckets.add(new ArrayList<>());
}
// 将元素分配到相应的桶中
for (int num : arr) {
buckets.get((num / exp) % 10).add(num);
}
// 对每个桶递归进行排序
int index = 0;
for (List<Integer> bucket : buckets) {
int[] bucketArray = bucket.stream().mapToInt(Integer::intValue).toArray();
if (exp > 1) {
msdRadixSort(bucketArray, exp / 10);
}
for (int num : bucketArray) {
arr[index++] = num;
}
}
}
public static void radixSort(int[] arr) {
int max = getMax(arr);
int maxDigit = (int) Math.pow(10, (int) Math.log10(max));
msdRadixSort(arr, maxDigit);
}
public static void main(String[] args) {
int[] arr = {170, 45, 75, 90, 802, 24, 2, 66};
System.out.println("Original array: " + Arrays.toString(arr));
radixSort(arr);
System.out.println("Sorted array: " + Arrays.toString(arr));
}
}
LSD和MSD基数排序的比较
相同点:
- 都是非比较排序算法。
- 时间复杂度都是 (O(d \times (n + k))),其中 (d) 是最大元素的位数,(n) 是数组长度,(k) 是计数排序的范围(通常为10)。
- 都使用计数排序来保证每轮排序的稳定性。
不同点:
- 排序方向:
- LSD基数排序从最低有效位开始排序。
- MSD基数排序从最高有效位开始排序。
- 实现复杂度:
- LSD基数排序实现较为简单。
- MSD基数排序实现相对复杂,需要递归处理每个子数组。
- 适用场景:
- LSD基数排序适用于整数和固定长度字符串。
- MSD基数排序适用于整数和长字符串,特别是当数据范围较大时。
总结
通过将计数排序抽象出来,我们简化了LSD和MSD基数排序的实现。这样不仅提高了代码的复用性和可读性,也使得修改和扩展算法变得更加容易。无论是从最低有效位开始排序的LSD基数排序,还是从最高有效位开始排序的MSD基数排序,都可以利用同一个计数排序模块来实现其核心功能。通过本文的介绍和代码实现,相信读者已经对LSD和MSD基数排序有了更深入的了解。