目录
冒泡排序 - 像气泡一样往上冒
一句话理解:就像水里的气泡,小的往上冒,大的往下沉。
算法步骤:
-
从第一个元素开始,比较相邻的两个元素
-
如果前面的比后面的大,就交换它们的位置
-
对每一对相邻元素重复这个过程,这样最大的元素就会"冒"到最后面
-
重复上述步骤,每次忽略已经排好序的尾部元素
生动比喻:就像排队时,个子高的人不断往后换,直到最高的人站在最后面。
public static int[] bubbleSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
boolean isSorted = true; // 优化:如果一轮没有交换,说明已经排好序
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换位置
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
isSorted = false;
}
}
if (isSorted) break; // 提前结束
}
return arr;
}
特点:
-
稳定排序(相等元素相对位置不变)
-
简单易懂但效率低
-
最佳情况:O(n) - 已经排好序时
-
最差情况:O(n²) - 完全逆序时
选择排序 - 每次都选最小的放前面
一句话理解:就像打牌时,每次都从手里挑最小的牌放到前面。
算法步骤:
-
在未排序序列中找到最小元素
-
把它放到已排序序列的末尾(也就是与当前位置交换)
-
重复这个过程,直到所有元素都排好序
生动比喻:就像整理书架,每次都找最薄的书放到最左边。
public static int[] 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;
}
return arr;
}
特点:
-
不稳定排序
-
无论什么情况都是O(n²)
-
交换次数少(最多n-1次交换)
插入排序 - 像理牌一样插入
一句话理解:就像打扑克时,每摸一张新牌就把它插入到手牌中的正确位置。
算法步骤:
-
从第二个元素开始(第一个元素默认已排序)
-
把当前元素与前面已排序的元素比较
-
如果当前元素更小,就把前面的元素往后移
-
找到合适位置后插入当前元素
-
重复直到所有元素都插入正确位置
生动比喻:就像整理扑克牌,每次拿到新牌都插到合适位置。
public static int[] insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int current = arr[i]; // 当前要插入的牌
int j = i - 1; // 从已排序部分的最后一个开始比较
// 把比当前元素大的都往后移
while (j >= 0 && current < arr[j]) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = current; // 插入到正确位置
}
return arr;
}
特点:
-
稳定排序
-
对小规模数据或基本有序数据效率很高
-
最佳情况:O(n) - 已经排好序时
希尔排序 - 分组插入排序
一句话理解:先大步伐分组排序,再小步伐精细排序。
算法步骤:
-
选择一个间隔序列(比如长度的一半)
-
按间隔分组,对每组进行插入排序
-
缩小间隔,重复分组排序
-
最后间隔为1时,就是普通的插入排序
生动比喻:就像先粗略整理书架(按大类分),再精细整理每类中的书。
public static int[] shellSort(int[] arr) {
int n = arr.length;
// 从大间隔开始,逐渐缩小
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个间隔分组进行插入排序
for (int i = gap; i < n; i++) {
int current = arr[i];
int j = i;
// 组内插入排序
while (j >= gap && arr[j - gap] > current) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = current;
}
}
return arr;
}
特点:
-
不稳定排序
-
插入排序的改进版
-
时间复杂度取决于间隔序列的选择
归并排序 - 分而治之的典范
一句话理解:先把大问题拆成小问题,解决后再合并起来。
算法步骤:
-
把数组分成两半
-
分别对左右两半递归排序
-
合并两个已排序的子数组
生动比喻:就像合并两个已经排好序的名单,每次比较两个名单的第一个元素,取较小的。
public static int[] mergeSort(int[] arr) {
if (arr.length <= 1) return arr; // 基础情况
int mid = arr.length / 2;
// 分成两半
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
// 递归排序并合并
return merge(mergeSort(left), mergeSort(right));
}
// 合并两个有序数组
private static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
int i = 0, j = 0, k = 0;
// 比较两个数组的元素,取较小的
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result[k++] = left[i++];
} else {
result[k++] = right[j++];
}
}
// 把剩余的元素加进去
while (i < left.length) result[k++] = left[i++];
while (j < right.length) result[k++] = right[j++];
return result;
}
特点:
-
稳定排序
-
总是O(n log n)的时间复杂度
-
需要额外空间
快速排序 - 找个基准分两边
一句话理解:选个"裁判",比裁判小的站左边,比裁判大的站右边,然后递归处理两边。
算法步骤:
-
选择一个基准元素(pivot)
-
把所有比基准小的放左边,比基准大的放右边
-
递归地对左右两部分快速排序
生动比喻:就像体育老师让学生按身高排队,选个中间身高的同学当基准。
public static 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 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++;
// 交换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;
}
特点:
-
不稳定排序
-
平均情况很快,最坏情况较慢
-
原地排序,不需要额外空间
堆排序 - 用树结构排序
一句话理解:先把数据建成"堆"这种树结构,然后每次取树根(最大或最小值)。
算法步骤:
-
构建最大堆(父节点总比子节点大)
-
把堆顶元素(最大值)与末尾元素交换
-
重新调整堆,使其满足堆性质
-
重复步骤2-3,直到堆大小为1
生动比喻:就像从一堆苹果中每次挑出最大的。
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;
// 调整剩余元素的堆
heapify(arr, i, 0);
}
}
// 调整堆
private static 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 swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
// 递归调整受影响的子树
heapify(arr, n, largest);
}
}
特点:
-
不稳定排序
-
总是O(n log n)
-
原地排序
计数排序 - 数数排序法
一句话理解:统计每个数字出现的次数,然后按顺序输出。
算法步骤:
-
找出数组中的最大值和最小值
-
创建计数数组,统计每个数字出现的次数
-
累加计数数组,确定每个数字的最终位置
-
从后往前遍历原数组,根据计数数组放到正确位置
生动比喻:就像老师统计每个分数段有多少学生,然后按分数段排队。
public static int[] countingSort(int[] arr) {
if (arr.length == 0) return arr;
// 找到最大值和最小值
int max = arr[0], min = arr[0];
for (int num : arr) {
if (num > max) max = num;
if (num < min) min = num;
}
// 创建计数数组
int[] count = new int[max - min + 1];
for (int num : arr) {
count[num - min]++;
}
// 累加计数
for (int i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
// 构建结果数组
int[] result = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
result[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}
return result;
}
特点:
-
稳定排序
-
只能用于整数
-
当数据范围不大时很快
桶排序 - 分组再排序
一句话理解:把数据分到多个桶里,每个桶单独排序,然后合并。
算法步骤:
-
设置一定数量的空桶
-
把数据分布到各个桶中
-
对每个非空桶进行排序
-
按顺序把各个桶中的元素合并
生动比喻:就像把商品按价格范围分到不同货架上,每个货架单独整理。
public static List<Integer> bucketSort(List<Integer> arr, int bucketSize) {
if (arr.size() <= 1) return arr;
// 找到最大值和最小值
int max = Collections.max(arr);
int min = Collections.min(arr);
// 计算桶的数量
int bucketCount = (max - min) / bucketSize + 1;
List<List<Integer>> buckets = new ArrayList<>();
// 初始化桶
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 把元素分配到桶中
for (int num : arr) {
int bucketIndex = (num - min) / bucketSize;
buckets.get(bucketIndex).add(num);
}
// 对每个桶排序并合并结果
List<Integer> result = new ArrayList<>();
for (List<Integer> bucket : buckets) {
if (!bucket.isEmpty()) {
Collections.sort(bucket); // 可以用其他排序算法
result.addAll(bucket);
}
}
return result;
}
特点:
-
稳定排序
-
数据分布均匀时效率高
-
需要额外空间
基数排序 - 按位排序
一句话理解:先按个位排序,再按十位排序,再按百位排序...
算法步骤:
-
找到最大数字,确定需要排序的轮数(位数)
-
从最低位开始,对每一位进行稳定的排序(通常用计数排序)
-
重复直到最高位排序完成
生动比喻:就像整理学生档案,先按班级排,再按学号排。
public static int[] radixSort(int[] arr) {
if (arr.length <= 1) return arr;
// 找到最大值,确定位数
int max = Arrays.stream(arr).max().getAsInt();
int exp = 1; // 从个位开始
while (max / exp > 0) {
countingSortByDigit(arr, exp);
exp *= 10; // 处理下一位:十位、百位...
}
return arr;
}
// 按指定位数进行计数排序
private static void countingSortByDigit(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10]; // 0-9
// 统计每个数字出现的次数
for (int i = 0; i < n; i++) {
int digit = (arr[i] / exp) % 10;
count[digit]++;
}
// 累加计数
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组(从后往前保持稳定性)
for (int i = n - 1; i >= 0; i--) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
// 复制回原数组
System.arraycopy(output, 0, arr, 0, n);
}
特点:
-
稳定排序
-
只能用于整数
-
当位数不多时效率很高
总结对比
排序算法 | 最佳情况 | 平均情况 | 最差情况 | 空间复杂度 | 稳定性 | 适用场景 |
---|---|---|---|---|---|---|
冒泡排序 | 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(1) | 不稳定 | 中等规模数据 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 大数据、稳定排序 |
快速排序 | O(n log n) | O(n log n) | O(n²) | O(n log n) | 不稳定 | 通用、大数据 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 需要保证最差性能 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | 稳定 | 整数、范围小 |
桶排序 | O(n + k) | O(n + k) | O(n²) | O(n + k) | 稳定 | 数据分布均匀 |
基数排序 | O(n × k) | O(n × k) | O(n × k) | O(n + k) | 稳定 | 整数、位数少 |
选择建议:
-
小数据:插入排序
-
通用:快速排序
-
需要稳定:归并排序
-
整数且范围小:计数排序
-
保证最差性能:堆排序
-
大数据外部排序:归并排序
记住:没有最好的排序算法,只有最适合当前场景的算法!