目录
1. 冒泡排序(Bubble Sort)
2. 选择排序(Selection Sort)
3. 插入排序(Insertion Sort)
4. 希尔排序(Shell Sort)
5. 快速排序(Quick Sort)
6. 归并排序(Merge Sort)
7. 堆排序(Heap Sort)
在计算机科学领域,排序是一项基础但至关重要的操作。无论你是处理数据库查询结果还是优化搜索效率,了解不同的排序算法及其适用场景都至关重要。本文将介绍并简要分析数据结构中最常用的七种排序算法,以及它们的时间/空间复杂度和稳定性信息。
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就交换它们的位置。
特点:简单但效率低,适用于小规模数据集。
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length; // 获取数组的长度n
boolean swapped; // 定义一个布尔变量swapped用于检测在一次完整的遍历中是否发生了交换
// 外层循环:控制需要进行多少轮比较,最多n-1轮
for (int i = 0; i < n - 1; i++) {
swapped = false; // 每次进入外层循环时将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; // 发生了交换,将swapped设为true
}
}
// 如果在一轮比较中没有发生任何交换,说明数组已经有序,提前结束排序
if (!swapped)
break;
}
}
}
时间复杂度:最坏情况下为O(n²),平均情况也是O(n²)。
空间复杂度:O(1),因为它只需要常量级别的额外空间。
稳定性:稳定。相等的元素不会改变其相对位置。
选择排序首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
特点:实现简单,但性能较差,不推荐用于大型数据集。
public class SelectionSort {
public static void selectionSort(int[] arr) {
int n = arr.length; // 获取数组的长度n
// 外层循环:遍历数组中的每一个元素,除了最后一个
for (int i = 0; i < n - 1; i++) {
int minIdx = i; // 假设当前索引i处的元素是最小值所在的索引
// 内层循环:从i+1开始遍历至数组末尾,寻找比arr[minIdx]更小的元素
for (int j = i + 1; j < n; j++)
if (arr[j] < arr[minIdx]) // 如果找到一个更小的元素
minIdx = j; // 更新最小值的索引为j
// 将找到的最小值与第i个位置的元素交换
int temp = arr[minIdx];
arr[minIdx] = arr[i];
arr[i] = temp;
}
}
}
时间复杂度:无论哪种情况均为O(n²)。
空间复杂度:O(1)。
稳定性:不稳定。因为可能会改变相等元素的相对位置。
插入排序通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
特点:对于几乎已经排序的数据集非常有效,稳定且易于实现。
public class InsertionSort {
public static void insertionSort(int[] arr) {
int n = arr.length; // 获取数组的长度n
// 外层循环:从数组的第二个元素开始遍历到数组末尾
for (int i = 1; i < n; ++i) {
int key = arr[i]; // 将当前元素存储为key
int j = i - 1; // j初始化为当前元素的前一个位置
// 内层循环:将当前元素与之前已经排序好的元素进行比较
// 如果找到比key大的元素,则将该元素向右移动一位
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // 将大于key的元素向右移动
j--; // j向前移动一位,继续比较
}
// 找到比key小或等于key的元素的位置后,将key插入到正确的位置
arr[j + 1] = key;
}
}
}
时间复杂度:最坏情况下为O(n²),最佳情况下接近O(n)。
空间复杂度:O(1)。
稳定性:稳定。相等的元素不会改变其相对位置。
希尔排序是插入排序的一种更高效的改进版本。它通过比较相隔一定间隔的元素来工作,随着算法的进行,间隔逐渐减少。
特点:比直接插入排序更快,尤其适合中等规模的数据集。
public class ShellSort {
public static void shellSort(int[] arr) {
int n = arr.length; // 获取数组的长度n
// 初始间隔gap设置为数组长度的一半,并逐步减小间隔
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个间隔gap进行插入排序
for (int i = gap; i < n; i += 1) {
int temp = arr[i]; // 当前元素保存到temp变量
int j;
// 将当前元素与前面间隔gap的元素比较并适当移动
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap]; // 如果前一个间隔的元素更大,则向后移动
}
// 找到合适的位置,将temp插入
arr[j] = temp;
}
}
}
}
时间复杂度:取决于增量的选择,通常介于O(n log n)到O(n^(3/2))之间。
空间复杂度:O(1)。
稳定性:不稳定。由于使用了间隔比较,可能会改变相等元素的相对位置。
快速排序使用分治法策略来把一个序列分成较小和较大的两个子序列,然后递归地排序这两个子序列。
特点:平均情况下非常高效,广泛应用于实际应用中,但最坏情况下的性能不佳。
public class QuickSort {
// 快速排序主函数
public static void quickSort(int[] arr, int low, int high) {
if (low < high) { // 确保至少有两个元素需要排序
int pi = partition(arr, low, high); // 找到分区点pi
// 递归调用quickSort对分区点左边和右边的子数组进行排序
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
// 分区函数,返回分区点
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为枢轴(pivot)
int i = (low - 1); // i是较小元素的索引
// j遍历从low到high-1的所有元素,将小于等于pivot的元素放到i的右侧
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++; // 发现一个小于等于pivot的元素,增加i
// 交换arr[i]和arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将pivot放到正确的位置,并返回该位置的索引
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),最坏情况下为O(n)(递归调用栈的空间)。
稳定性:不稳定。由于分区过程中可能会改变相等元素的相对位置。
归并排序也是一种基于分治思想的排序算法。它将数组分为两半分别排序后再合并这两个有序数组。
特点:稳定且具有良好的时间复杂度O(n log n),适用于大规模数据集。
public class MergeSort {
// 归并排序主函数
public static void mergeSort(int[] arr, int l, int r) {
if (l < r) { // 确保至少有两个元素需要排序
int m = (l + r) / 2; // 找到中间点m
// 递归调用mergeSort对左半部分和右半部分分别进行排序
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
// 合并两个已排序的部分
merge(arr, l, m, r);
}
}
// 合并函数,将两个有序子数组合并成一个有序数组
private static void merge(int[] arr, int l, int m, int r) {
// 计算左半部分和右半部分的长度
int n1 = m - l + 1;
int n2 = r - m;
// 创建临时数组L和R,用于存储左右两部分的数据
int[] L = new int[n1];
int[] R = new int[n2];
// 将数据复制到临时数组L和R中
for (int i = 0; i < n1; ++i)
L[i] = arr[l + i];
for (int j = 0; j < n2; ++j)
R[j] = arr[m + 1 + j];
// 初始化索引i, j, k
int i = 0, j = 0;
int k = l;
// 合并临时数组L和R回到原数组arr中
while (i < n1 && j < n2) {
if (L[i] <= R[j]) { // 如果L[i]小于等于R[j]
arr[k] = L[i]; // 将L[i]放入arr[k]
i++; // 移动L的指针
} else { // 如果L[i]大于R[j]
arr[k] = R[j]; // 将R[j]放入arr[k]
j++; // 移动R的指针
}
k++; // 移动原数组的指针
}
// 将剩余的L[]中的元素复制回arr[]
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
// 将剩余的R[]中的元素复制回arr[]
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
}
时间复杂度:无论何种情况下的时间复杂度都是O(n log n)。
空间复杂度:O(n),需要额外的数组来存储中间结果。
稳定性:稳定。相等的元素不会改变其相对位置。
堆排序利用了堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质。
特点:在空间效率方面优于归并排序,但在实际应用中的表现可能不如快速排序。
public class HeapSort {
// 堆排序主函数
public static void heapSort(int[] arr) {
int n = arr.length;
// 构建最大堆(Build Max Heap)
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从根节点开始,此时堆大小为i
heapify(arr, i, 0);
}
}
// 维护堆的性质(Max Heapify)
private static void heapify(int[] arr, int n, int i) {
int largest = i; // 初始化largest为根节点
int l = 2 * i + 1; // 左子节点索引
int r = 2 * i + 2; // 右子节点索引
// 如果左子节点大于根节点,则更新largest
if (l < n && arr[l] > arr[largest])
largest = l;
// 如果右子节点大于目前的largest,则更新largest
if (r < n && arr[r] > arr[largest])
largest = r;
// 如果largest不是根节点,则交换并递归调用heapify
if (largest != i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
// 递归地在受影响的子树上调用heapify
heapify(arr, n, largest);
}
}
}
时间复杂度:稳定在O(n log n)。
空间复杂度:O(1)。
稳定性:不稳定。因为在调整堆的过程中可能会改变相等元素的相对位置。