算法概述
计数排序(Counting Sort)是一种非基于比较的排序算法,由Harold Seward于1954年提出。它的核心思想是:对于待排序数组中的每个元素x,确定小于x的元素个数,从而将x直接放到输出数组中的正确位置。
核心特点
- 时间复杂度O(n+k):n为元素个数,k为元素取值范围
- 空间复杂度O(n+k):需要额外的计数数组和输出数组
- 稳定性:稳定的排序算法,相等元素的相对顺序不变
- 适用范围:仅适用于整数或有限范围内的数据
算法步骤
- 统计频次:遍历数组,统计每个值出现的次数,存入计数数组count
- 累积计数:将count数组转换为累积计数,count[i]表示小于等于i的元素个数
- 放置元素:从后向前遍历原数组,根据累积计数将元素放到输出数组的正确位置
Java完整实现
import java.util.Arrays;
/**
* 计数排序完整Java实现
* 包含基础版本、稳定版本、原地版本及负数支持版本
*/
public class CountingSort {
/**
* 标准计数排序(支持非负整数)
* 时间复杂度:O(n + k),空间复杂度:O(n + k)
* @param arr 待排序数组(元素为非负整数)
* @return 排序后的新数组
*/
public static int[] sort(int[] arr) {
if (arr == null || arr.length <= 1) {
return arr == null ? null : arr.clone();
}
// 找到最大值以确定计数数组大小
int max = arr[0];
for (int num : arr) {
if (num > max) max = num;
if (num < 0) throw new IllegalArgumentException("标准计数排序不支持负数");
}
return sortWithRange(arr, 0, max);
}
/**
* 已知取值范围的计数排序
*/
public static int[] sortWithRange(int[] arr, int min, int max) {
int n = arr.length;
int range = max - min + 1;
// 计数数组:记录每个值出现的次数
int[] count = new int[range];
for (int num : arr) {
count[num - min]++;
}
// 累积计数:count[i]表示小于等于i的元素个数
for (int i = 1; i < range; i++) {
count[i] += count[i - 1];
}
// 从后向前遍历,保证稳定性
int[] output = new int[n];
for (int i = n - 1; i >= 0; i--) {
int val = arr[i];
int pos = count[val - min] - 1;
output[pos] = val;
count[val - min]--;
}
return output;
}
/**
* 支持负数的计数排序
*/
public static int[] sortWithNegative(int[] arr) {
if (arr == null || arr.length <= 1) {
return arr == null ? null : arr.clone();
}
int min = arr[0], max = arr[0];
for (int num : arr) {
if (num < min) min = num;
if (num > max) max = num;
}
return sortWithRange(arr, min, max);
}
/**
* 原地计数排序(不返回新数组,修改原数组)
* 适用于已知范围的小整数
*/
public static void sortInPlace(int[] arr, int min, int max) {
int range = max - min + 1;
int[] count = new int[range];
// 统计频次
for (int num : arr) {
count[num - min]++;
}
// 按顺序写回原数组
int index = 0;
for (int i = 0; i < range; i++) {
while (count[i]-- > 0) {
arr[index++] = i + min;
}
}
}
/**
* 对象数组计数排序(基于某个整数字段)
* 演示稳定排序特性
*/
static class Student {
int score;
String name;
Student(int score, String name) {
this.score = score;
this.name = name;
}
@Override
public String toString() {
return name + "(" + score + ")";
}
}
public static Student[] sortStudentsByScore(Student[] students, int minScore, int maxScore) {
int n = students.length;
int range = maxScore - minScore + 1;
int[] count = new int[range];
for (Student s : students) {
count[s.score - minScore]++;
}
for (int i = 1; i < range; i++) {
count[i] += count[i - 1];
}
Student[] output = new Student[n];
// 从后向前遍历保证稳定性:相同分数的学生保持原有顺序
for (int i = n - 1; i >= 0; i--) {
int score = students[i].score;
int pos = count[score - minScore] - 1;
output[pos] = students[i];
count[score - minScore]--;
}
return output;
}
/**
* 验证排序正确性
*/
public static boolean isSorted(int[] arr) {
for (int i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) return false;
}
return true;
}
/**
* 性能对比:计数排序 vs 快速排序
*/
public static void performanceComparison() {
int[] sizes = {1000, 10000, 100000};
int range = 1000; // 取值范围0-999
System.out.println("性能对比(范围0-" + (range - 1) + "):");
System.out.println("规模 计数排序(ms) Arrays.sort(ms)");
for (int size : sizes) {
int[] arr1 = new int[size];
int[] arr2 = new int[size];
for (int i = 0; i < size; i++) {
arr1[i] = arr2[i] = (int)(Math.random() * range);
}
long start1 = System.currentTimeMillis();
sortWithRange(arr1, 0, range - 1);
long time1 = System.currentTimeMillis() - start1;
long start2 = System.currentTimeMillis();
Arrays.sort(arr2);
long time2 = System.currentTimeMillis() - start2;
System.out.println(size + " " + time1 + " " + time2);
}
}
public static void main(String[] args) {
// 测试基本排序
int[] arr = {4, 2, 2, 8, 3, 3, 1};
System.out.println("原始数组:" + Arrays.toString(arr));
int[] sorted = sort(arr);
System.out.println("排序结果:" + Arrays.toString(sorted));
System.out.println("是否正确排序:" + isSorted(sorted));
System.out.println();
// 测试负数
int[] arrNeg = {-5, -1, -10, 0, 5, 3, -3};
System.out.println("含负数数组:" + Arrays.toString(arrNeg));
int[] sortedNeg = sortWithNegative(arrNeg);
System.out.println("排序结果:" + Arrays.toString(sortedNeg));
System.out.println();
// 测试稳定性
Student[] students = {
new Student(85, "张三"),
new Student(90, "李四"),
new Student(85, "王五"),
new Student(90, "赵六"),
new Student(85, "孙七")
};
System.out.println("原始学生顺序:" + Arrays.toString(students));
Student[] sortedStudents = sortStudentsByScore(students, 80, 95);
System.out.println("排序后顺序:" + Arrays.toString(sortedStudents));
System.out.println("稳定性验证(相同分数保持原顺序):张三在孙七前?" +
(findIndex(sortedStudents, "张三") < findIndex(sortedStudents, "孙七")));
System.out.println();
// 性能对比
performanceComparison();
}
private static int findIndex(Student[] arr, String name) {
for (int i = 0; i < arr.length; i++) {
if (arr[i].name.equals(name)) return i;
}
return -1;
}
}
稳定性分析
计数排序的稳定性来源于从后向前遍历原数组的策略。当遇到相同值的元素时,后出现的元素会被先放置到输出数组中靠后的位置,从而保持相对顺序不变。
以 [85-张三, 85-王五, 85-孙七] 为例:
1. 累积计数后,85对应的位置范围为1-3
2. 从后向前遍历,孙七先被放到位置3
3. 然后是王五放到位置2
4. 最后是张三放到位置1
5. 最终顺序为 [85-张三, 85-王五, 85-孙七],保持了原始顺序
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n + k) | n为元素个数,k为取值范围 |
| 空间复杂度 | O(n + k) | 计数数组+输出数组 |
| 最好情况 | O(n + k) | 始终如此 |
| 最坏情况 | O(n + k) | 始终如此 |
| 稳定性 | 稳定 | 相等元素顺序不变 |
适用场景与局限性
适用场景
- 小范围整数排序:如年龄(0-150)、考试分数(0-100)
- 作为基数排序的子程序:基数排序利用计数排序对每一位进行稳定排序
- 需要稳定排序的场景:如多关键字排序的次级排序
局限性
- 仅适用于整数或有限离散值
- 空间开销大:当k远大于n时,空间效率低
- 不适合大范围数据:如32位整数范围太大
与快速排序对比
| 特性 | 计数排序 | 快速排序 |
|---|---|---|
| 时间复杂度 | O(n+k) | O(n log n) |
| 空间复杂度 | O(n+k) | O(log n) |
| 稳定性 | 稳定 | 不稳定 |
| 数据类型 | 整数/离散值 | 任何可比较类型 |
| 最好情况 | 当k较小时极快 | O(n log n) |
总结
计数排序通过巧妙的计数和累积策略,突破了比较排序O(n log n)的下界限制,在小范围整数排序场景下具有显著的性能优势。理解计数排序有助于掌握更高级的基数排序和桶排序算法。