目录
[1. 冒泡排序 (Bubble Sort)](#1. 冒泡排序 (Bubble Sort))
[2. 选择排序 (Selection Sort)](#2. 选择排序 (Selection Sort))
[3. 插入排序 (Insertion Sort)](#3. 插入排序 (Insertion Sort))
[4. 快速排序 (Quick Sort)](#4. 快速排序 (Quick Sort))
[5. 归并排序 (Merge Sort)](#5. 归并排序 (Merge Sort))
[6. 堆排序 (Heap Sort)](#6. 堆排序 (Heap Sort))
排序算法稳定性解释:
- 稳定排序:相等的元素在排序后保持原有的相对顺序
- 不稳定排序:相等的元素在排序后可能改变原有的相对顺序
比如:
原始顺序是 4₁, 2, 4₂, 1, 3,排序后变成了 1, 2, 3, 4₁, 4₂
- 原来:4₁ 在 4₂ 前面
- 现在:4₁ 仍然在 4₂ 前面
这就是稳定排序
1. 冒泡排序 (Bubble Sort)
算法思路分析
冒泡排序是最简单的排序算法之一,其核心思想是:
- 相邻比较:比较相邻的两个元素,如果第一个比第二个大,则交换它们
- 逐步冒泡:每一轮比较后,最大的元素会"冒泡"到数组的末尾
- 重复执行:重复n-1轮,直到所有元素都排好序
代码实现
java
/**
* 冒泡排序算法
*
* 算法思路:
* 1. 比较相邻的两个元素,如果第一个比第二个大,则交换它们
* 2. 对每一对相邻元素执行同样的操作,从开始第一对到结尾的最后一对
* 3. 重复以上步骤,每次都能将当前最大的元素"冒泡"到数组末尾
* 4. 重复n-1轮,直到没有任何一对数字需要比较
*/
public static void bubbleSort(int[] arr) {
int n = arr.length;
// 外层循环控制排序轮数,总共需要n-1轮
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) - 只需要一个临时变量
- 稳定性:稳定排序
2. 选择排序 (Selection Sort)
算法思路分析
选择排序的核心思想是:
- 找最小值:在未排序序列中找到最小元素
- 交换位置:将找到的最小元素与未排序序列的第一个元素交换位置
- 重复执行:重复上述过程,直到所有元素都排好序
代码实现
java
/**
* 选择排序算法
*
* 算法思路:
* 1. 在未排序序列中找到最小元素,存放到排序序列的起始位置
* 2. 然后,再从剩余未排序元素中继续寻找最小元素
* 3. 重复第二步,直到所有元素均排序完毕
*/
public static 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;
}
}
// 将找到的最小元素与当前位置的元素交换
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
复杂度分析
- 时间复杂度:O(n²) - 无论最好、最坏、平均情况都是O(n²)
- 空间复杂度:O(1) - 只需要一个临时变量
- 稳定性:不稳定排序
3. 插入排序 (Insertion Sort)
算法思路分析
插入排序的核心思想是:
- 构建有序序列:将第一个元素看作已排序序列
- 逐个插入:对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
- 重复执行:重复上述过程,直到所有元素都插入到有序序列中
代码实现
java
/**
* 插入排序算法
*
* 算法思路:
* 1. 从第一个元素开始,该元素可以认为已经被排序
* 2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
* 3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
* 4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
* 5. 将新元素插入到该位置后
* 6. 重复步骤2~5
*/
public static void insertionSort(int[] arr) {
int n = arr.length;
// 从第二个元素开始,逐个插入到已排序序列中
for (int i = 1; i < n; i++) {
// 保存当前要插入的元素
int key = arr[i];
// j指向已排序序列的最后一个元素
int j = i - 1;
// 从后向前扫描已排序序列,寻找插入位置
// 如果已排序序列中的元素大于key,则将其向后移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
// 找到插入位置,将key插入
arr[j + 1] = key;
}
}
复杂度分析
- 时间复杂度:O(n²) - 最坏和平均情况,最好情况O(n)(已排序数组)
- 空间复杂度:O(1) - 只需要一个临时变量
- 稳定性:稳定排序
4. 快速排序 (Quick Sort)
算法思路分析
快速排序是一种高效的排序算法,使用分治策略:
- 选择基准:从数组中选择一个基准元素(通常是最后一个元素)
- 分区操作:将数组分为两部分,左边都小于基准,右边都大于基准
- 递归排序:对左右两部分分别进行快速排序
- 合并结果:由于分区操作,合并时数组已经有序
代码实现
java
/**
* 快速排序算法
*
* 算法思路:
* 1. 选择一个基准元素(通常是最后一个元素)
* 2. 将数组分为两部分:左边都小于基准,右边都大于基准
* 3. 对左右两部分分别递归执行快速排序
* 4. 由于分区操作,合并时数组已经有序
*/
public static void quickSort(int[] arr) {
quickSort(arr, 0, arr.length - 1);
}
/**
* 快速排序的递归实现
* @param arr 待排序数组
* @param low 起始索引
* @param high 结束索引
*/
private static void quickSort(int[] arr, int low, int high) {
// 递归终止条件:当起始索引小于结束索引时
if (low < high) {
// 进行分区操作,返回基准元素的最终位置
int pi = partition(arr, low, high);
// 递归排序基准元素左边的子数组
quickSort(arr, low, pi - 1);
// 递归排序基准元素右边的子数组
quickSort(arr, pi + 1, high);
}
}
/**
* 分区操作:将数组分为两部分
* @param arr 待分区数组
* @param low 起始索引
* @param high 结束索引
* @return 基准元素的最终位置
*/
private static int partition(int[] arr, int low, int high) {
// 选择最后一个元素作为基准
int pivot = arr[high];
// i指向小于基准元素的最后一个位置
int i = low - 1;
// 遍历数组,将小于基准的元素移到左边
for (int j = low; j < high; j++) {
// 如果当前元素小于基准
if (arr[j] < pivot) {
i++;
// 交换元素
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;
}
复杂度分析
- 时间复杂度:O(n log n) - 平均情况,最坏情况O(n²)
- 空间复杂度:O(log n) - 递归调用栈的深度
- 稳定性:不稳定排序
5. 归并排序 (Merge Sort)
算法思路分析
归并排序是一种稳定的排序算法,使用分治策略:
- 分解:将数组分成两个子数组,递归地对子数组进行排序
- 合并:将两个已排序的子数组合并成一个有序数组
- 递归执行:重复上述过程,直到所有元素都排好序
代码实现
java
/**
* 归并排序算法
*
* 算法思路:
* 1. 将数组分成两个子数组,递归地对子数组进行排序
* 2. 将两个已排序的子数组合并成一个有序数组
* 3. 重复上述过程,直到所有元素都排好序
*/
public static void mergeSort(int[] arr) {
mergeSort(arr, 0, arr.length - 1);
}
/**
* 归并排序的递归实现
* @param arr 待排序数组
* @param left 左边界
* @param right 右边界
*/
private static 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);
}
}
/**
* 合并两个已排序的子数组
* @param arr 原数组
* @param left 左边界
* @param mid 中间位置
* @param right 右边界
*/
private static void merge(int[] arr, int left, int mid, int right) {
// 计算两个子数组的长度
int n1 = mid - left + 1;
int n2 = right - mid;
// 创建临时数组来存储两个子数组
int[] leftArray = new int[n1];
int[] rightArray = new int[n2];
// 复制数据到临时数组
for (int i = 0; i < n1; i++) {
leftArray[i] = arr[left + i];
}
for (int j = 0; j < n2; j++) {
rightArray[j] = arr[mid + 1 + j];
}
// 合并两个子数组
int i = 0, j = 0, k = left;
// 比较两个子数组的元素,将较小的元素放入原数组
while (i < n1 && j < n2) {
if (leftArray[i] <= rightArray[j]) {
arr[k] = leftArray[i];
i++;
} else {
arr[k] = rightArray[j];
j++;
}
k++;
}
// 将剩余的元素复制到原数组
while (i < n1) {
arr[k] = leftArray[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArray[j];
j++;
k++;
}
}
复杂度分析
- 时间复杂度:O(n log n) - 无论最好、最坏、平均情况都是O(n log n)
- 空间复杂度:O(n) - 需要额外的数组来存储合并结果
- 稳定性:稳定排序
6. 堆排序 (Heap Sort)
算法思路分析
堆排序是一种基于堆数据结构的排序算法:
- 构建堆:将数组构建成最大堆(或最小堆)
- 提取根节点:将堆的根节点(最大值)与最后一个元素交换
- 调整堆:对剩余的n-1个元素重新构建堆
- 重复执行:重复上述过程,直到所有元素都排好序
代码实现
java
/**
* 堆排序算法
*
* 算法思路:
* 1. 将数组构建成最大堆
* 2. 将堆的根节点(最大值)与最后一个元素交换
* 3. 对剩余的n-1个元素重新构建堆
* 4. 重复上述过程,直到所有元素都排好序
*/
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;
// 对剩余的i个元素重新构建最大堆
heapify(arr, i, 0);
}
}
/**
* 将以root为根的子树调整为最大堆
* @param arr 数组
* @param n 堆的大小
* @param root 根节点的索引
*/
private static void heapify(int[] arr, int n, int root) {
// 假设根节点是最大的
int largest = root;
// 计算左子节点的索引
int left = 2 * root + 1;
// 计算右子节点的索引
int right = 2 * root + 2;
// 如果左子节点存在且大于根节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点存在且大于当前最大值
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是根节点
if (largest != root) {
// 交换根节点和最大值
int temp = arr[root];
arr[root] = arr[largest];
arr[largest] = temp;
// 递归调整被交换的子树
heapify(arr, n, largest);
}
}
复杂度分析
- 时间复杂度:O(n log n) - 无论最好、最坏、平均情况都是O(n log n)
- 空间复杂度:O(1) - 原地排序
- 稳定性:不稳定排序
堆排序原理参考:堆排序步骤推演-CSDN博客
算法性能对比总结
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
---|---|---|---|---|
冒泡排序 | O(n²) | O(1) | 稳定 | 小数据量 |
选择排序 | O(n²) | O(1) | 不稳定 | 小数据量,交换次数少 |
插入排序 | O(n²) | O(1) | 稳定 | 小数据量,基本有序 |
快速排序 | O(n log n) | O(log n) | 不稳定 | 大数据量,实际应用 |
归并排序 | O(n log n) | O(n) | 稳定 | 大数据量,要求稳定 |
堆排序 | O(n log n) | O(1) | 不稳定 | 大数据量,原地排序 |
使用建议:小数据量(n < 50):使用插入排序,代码简单且效率较高;大数据量:优先使用快速排序,平均性能最好;要求稳定性:使用归并排序;内存受限:使用堆排序,原地排序;
推荐:Java的Arrays.sort(),此方法已经为我们提供了高效的排序实现