介绍几种排序算法,包括快速排序、归并排序、堆排序、希尔排序、插入排序、选择排序、冒泡排序、计数排序、随机排序、桶排序和基数排序。
1. 快速排序 (Quick Sort) - 时间复杂度:O(n²) / O(n log n) / O(n log n)
逻辑解释
快速排序是一种分治法的排序算法。选择一个“基准”元素,将数组分为两部分,左边部分的元素都小于基准,右边部分的元素都大于基准,然后递归地对这两部分进行排序。
Java代码
public static void quickSort(int[] array, int low, int high) {
if (low < high) {
int pivotIndex = partition(array, low, high);
quickSort(array, low, pivotIndex - 1); // 排序左半部分
quickSort(array, pivotIndex + 1, high); // 排序右半部分
}
}
private static int partition(int[] array, int low, int high) {
int pivot = array[high]; // 选择最后一个元素为基准
int i = low - 1; // 小于基准的元素索引
for (int j = low; j < high; j++) {
if (array[j] < pivot) {
i++;
// 交换元素
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// 交换基准元素到正确位置
int temp = array[i + 1];
array[i + 1] = array[high];
array[high] = temp;
return i + 1; // 返回基准元素索引
}
时间复杂度分析
- 最坏情况 O(n²):当数组已排序或全部相同元素时,快速排序每次选择的基准元素都是最小或最大,导致时间复杂度为O(n²)。
- 最好情况 O(n log n):当每次选择的基准元素能将数组均匀分割时,时间复杂度为O(n log n)。
- 平均情况 O(n log n):随机情况下,基准元素通常能较好地分割数组,时间复杂度为O(n log n)。
空间复杂度
- O(log n),递归调用栈的空间。
稳定性分析
快速排序是不稳定的,因为在排序过程中可能会改变相等元素的相对位置。
2. 归并排序 (Merge Sort) - 时间复杂度:O(n log n) / O(n log n) / O(n log n)
逻辑解释
归并排序是一种分治法的排序算法。将数组分成两部分,分别排序后再合并。具体步骤:
- 将数组分成两半,直到每个部分只有一个元素。
- 将两个部分合并成一个有序数组。
- 递归进行这个过程,直到所有部分都合并成一个完整的有序数组。
Java代码
public static void mergeSort(int[] array) {
if (array.length < 2) return; // 基础情况
int mid = array.length / 2; // 找到中间索引
int[] left = new int[mid]; // 创建左半部分数组
int[] right = new int[array.length - mid]; // 创建右半部分数组
// 拷贝数据到左右数组
System.arraycopy(array, 0, left, 0, mid);
System.arraycopy(array, mid, right, 0, array.length - mid);
mergeSort(left); // 递归排序左半部分
mergeSort(right); // 递归排序右半部分
merge(array, left, right); // 合并已排序的部分
}
private static void merge(int[] array, int[] left, int[] right) {
int i = 0, j = 0, k = 0;
// 合并两个已排序的数组
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
array[k++] = left[i++]; // 取较小的元素
} else {
array[k++] = right[j++];
}
}
// 将剩余元素拷贝到原数组
while (i < left.length) {
array[k++] = left[i++];
}
while (j < right.length) {
array[k++] = right[j++];
}
}
时间复杂度分析
- 最坏情况 O(n log n):归并排序的时间复杂度始终为O(n log n)。
- 最好情况 O(n log n):即使数组已经有序,归并排序仍然会执行相同数量的比较和合并操作,因此时间复杂度为O(n log n)。
- 平均情况 O(n log n):在随机情况下,合并操作的复杂度仍然为O(n log n)。
空间复杂度
- O(n),需要额外的数组空间用于合并。
稳定性分析
归并排序是稳定的,因为在合并过程中相等元素的相对位置不会改变。
3. 堆排序 (Heap Sort) - 时间复杂度:O(n log n) / O(n log n) / O(n log n)
逻辑解释
堆排序利用堆这种数据结构的特性,首先构建最大堆或最小堆,然后依次取出堆顶元素,放到已排序部分。具体步骤:
- 将数组构建成最大堆。
- 取出堆顶元素(最大元素),将其与数组最后一个元素交换。
- 除去最后一个元素(已排序),重新调整堆。
- 重复上述步骤,直到所有元素都排序完成。
Java代码
public static void heapSort(int[] array) {
int n = array.length;
// 构建最大堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(array, n, i);
}
// 排序
for (int i = n - 1; i > 0; i--) {
// 交换堆顶和当前元素
int temp = array[0];
array[0] = array[i];
array[i] = temp;
// 重新调整堆
heapify(array, i, 0);
}
}
private static void heapify(int[] array, int n, int i) {
int largest = i; // 初始化最大的元素为根
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 如果左子节点比根节点大
if (left < n && array[left] > array[largest]) {
largest = left;
}
// 如果右子节点比目前最大的节点大
if (right < n && array[right] > array[largest]) {
largest = right;
}
// 如果最大的节点不是根节点
if (largest != i) {
int swap = array[i];
array[i] = array[largest];
array[largest] = swap;
// 递归调整
heapify(array, n, largest);
}
}
时间复杂度分析
- 最坏情况 O(n log n):堆排序的时间复杂度始终为O(n log n),因为构建堆的时间复杂度为O(n),而每次调整堆的时间复杂度为O(log n),总共需要n次调整。
- 最好情况 O(n log n):在任何情况下,堆排序的时间复杂度均为O(n log n)。
- 平均情况 O(n log n):在随机情况下,堆排序的时间复杂度也为O(n log n)。
空间复杂度
- O(1),只使用了常数级别的额外空间。
稳定性分析
堆排序是不稳定的,因为在调整堆的过程中可能会改变相等元素的相对位置。
4. 希尔排序 (Shell Sort) - 时间复杂度:O(n²) / O(n log n) / O(n log n)
逻辑解释
希尔排序是插入排序的一种改进版本,通过将数组分为多个子序列(根据一个增量),对每个子序列进行插入排序。随着增量逐渐减小,最终实现整体排序。
Java代码
public static void shellSort(int[] array) {
int n = array.length;
for (int gap = n / 2; gap > 0; gap /= 2) { // 设置增量
for (int i = gap; i < n; i++) {
int temp = array[i];
int j;
for (j = i; j >= gap && array[j - gap] > temp; j -= gap) {
array[j] = array[j - gap]; // 移动元素
}
array[j] = temp; // 插入元素
}
}
}
时间复杂度分析
- 最坏情况 O(n²):在最坏情况下(增量选择不当),希尔排序可能退化为O(n²)。
- 最好情况 O(n log n):在合适的增量选择下,希尔排序能达到O(n log n)的性能。
- 平均情况 O(n log n):在随机情况下,希尔排序的性能通常在O(n log n)左右。
空间复杂度
- O(1),只使用了常数级别的额外空间。
稳定性分析
希尔排序是不稳定的,因为在插入过程中可能会改变相等元素的相对位置。
5. 插入排序 (Insertion Sort) - 时间复杂度:O(n²) / O(n) / O(n²)
逻辑解释
插入排序的思想是将数组分为已排序和未排序两部分,每次从未排序部分取出一个元素,插入到已排序部分的适当位置。具体步骤:
- 从第二个元素开始,假设第一个元素已经排序。
- 将当前元素与已排序部分的元素进行比较,找到合适的位置插入。
- 将大于当前元素的元素向后移动一位,为当前元素腾出位置。
- 重复这个过程,直到所有元素都排序完成。
Java代码
public static void insertionSort(int[] array) {
int n = array.length; // 获取数组长度
for (int i = 1; i < n; i++) { // 从第二个元素开始
int key = array[i]; // 当前要插入的元素
int j = i - 1;
// 找到插入位置
while (j >= 0 && array[j] > key) {
array[j + 1] = array[j]; // 向后移动元素
j--;
}
array[j + 1] = key; // 插入元素
}
}
时间复杂度分析
- 最坏情况 O(n²):当数组完全逆序时,插入排序需要进行n-1次外循环,每次外循环做最多n次比较,总时间复杂度为O(n²)。
- 最好情况 O(n):当数组已经有序时,只需一趟遍历,且没有交换,时间复杂度为O(n)。
- 平均情况 O(n²):在一般情况下,插入排序的时间复杂度为O(n²)。
空间复杂度
- O(1),只使用了常数级别的额外空间。
稳定性分析
插入排序是稳定的,因为在插入过程中相等元素的相对位置不会改变。
6. 选择排序 (Selection Sort) - 时间复杂度:O(n²) / O(n²) / O(n²)
逻辑解释
选择排序的基本思想是每次从未排序的部分中选择最小的元素,将其放到已排序部分的末尾。具体步骤如下:
- 从待排序的数组中找到最小的元素。
- 将其与数组的第一个元素交换位置。
- 再从剩下的元素中继续寻找最小的元素,与第二个元素交换。
- 重复这个过程,直到所有元素都排序完成。
Java代码
public static void selectionSort(int[] array) {
int n = array.length; // 获取数组长度
for (int i = 0; i < n - 1; i++) { // 遍历数组
int minIndex = i; // 假设当前元素为最小值
for (int j = i + 1; j < n; j++) { // 查找最小值
if (array[j] < array[minIndex]) {
minIndex = j; // 更新最小值索引
}
}
// 交换最小值与当前元素
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
}
时间复杂度分析
- 最坏情况 O(n²):选择排序在每次遍历中都需要查找未排序部分的最小元素,导致时间复杂度为O(n²)。
- 最好情况 O(n²):即使数组已排序,选择排序仍需进行n-1次比较,时间复杂度仍为O(n²)。
- 平均情况 O(n²):在任意情况下,选择排序的时间复杂度始终为O(n²)。
空间复杂度
- O(1),只使用了常数级别的额外空间。
稳定性分析
选择排序是不稳定的,因为在交换过程中相等元素的相对位置可能会被改变。
7. 冒泡排序 (Bubble Sort) - 时间复杂度:O(n²) / O(n) / O(n²)
逻辑解释
冒泡排序的思想是通过多次遍历数组,比较相邻的元素并交换它们的位置,使得较大的元素逐渐“冒泡”到数组的顶端。具体步骤:
- 从数组的第一对元素开始比较。
- 如果第一个元素大于第二个元素,则交换它们的位置。
- 继续比较下一对元素,重复这个过程,直到到达数组的末尾。
- 每一轮遍历后,最大的元素都会被移动到数组的末尾。
- 重复上述过程,直到没有需要交换的元素为止。
Java代码
public static void bubbleSort(int[] array) {
int n = array.length; // 获取数组长度
for (int i = 0; i < n - 1; i++) { // 外层循环控制遍历次数
for (int j = 0; j < n - 1 - i; j++) { // 内层循环进行相邻元素比较
if (array[j] > array[j + 1]) {
// 交换相邻元素
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
时间复杂度分析
- 最坏情况 O(n²):当数组完全逆序时,冒泡排序需要进行n-1次外循环,每次外循环做n次比较,总时间复杂度为O(n²)。
- 最好情况 O(n):当数组已经有序时,只需一趟遍历,且没有交换,时间复杂度为O(n)。
- 平均情况 O(n²):在一般情况下,冒泡排序的时间复杂度为O(n²)。
空间复杂度
- O(1),只使用了常数级别的额外空间。
稳定性分析
冒泡排序是稳定的,因为在交换过程中相等元素的相对位置不会改变。
8. 计数排序 (Counting Sort) - 时间复杂度:O(n + k) / O(n + k) / O(n + k)
逻辑解释
计数排序通过统计每个元素出现的次数,然后根据计数信息直接构建已排序的数组。具体步骤:
- 找到数组中的最大值,确定计数数组的大小。
- 统计每个元素的出现次数,存入计数数组。
- 计算每个元素的累积计数。
- 根据计数数组构建输出数组。
Java代码
public static void countingSort(int[] array) {
int max = getMax(array); // 找到最大值
int[] count = new int[max + 1]; // 创建计数数组
int[] output = new int[array.length]; // 输出数组
// 统计每个元素的出现次数
for (int num : array) {
count[num]++;
}
// 累加计数
for (int i = 1; i <= max; i++) {
count[i] += count[i - 1];
}
// 构建输出数组
for (int i = array.length - 1; i >= 0; i--) {
output[count[array[i]] - 1] = array[i];
count[array[i]]--; // 减少计数
}
System.arraycopy(output, 0, array, 0, array.length); // 拷贝回原数组
}
private static int getMax(int[] array) {
int max = array[0];
for (int num : array) {
if (num > max) {
max = num; // 找到最大值
}
}
return max;
}
时间复杂度分析
- 最坏情况 O(n + k):计数排序的时间复杂度为O(n + k),其中n是元素数量,k是范围。即使在最坏情况下,统计和构建输出数组的复杂度也为O(n + k)。
- 最好情况 O(n + k):无论数据如何,计数排序对每个元素的统计和输出构建都需要进行,因此时间复杂度为O(n + k)。
- 平均情况 O(n + k):在随机情况下,计数排序的时间复杂度也为O(n + k)。
空间复杂度
- O(k),需要额外的空间存储计数数组。
稳定性分析
计数排序是稳定的,因为在构建输出数组时,相同元素的相对位置不会改变。
9. 随机排序 (Random Sort) - 时间复杂度:O(∞) / O(n)
逻辑解释
随机排序是一种非常低效的排序方法,它反复随机排列数组,直到数组有序为止。这个方法并不实用,只是用于理论研究。
Java代码
import java.util.Random;
public static void randomSort(int[] array) {
Random rand = new Random();
while (!isSorted(array)) {
for (int i = 0; i < array.length; i++) {
int randomIndex = rand.nextInt(array.length); // 生成随机索引
// 交换元素
int temp = array[i];
array[i] = array[randomIndex];
array[randomIndex] = temp;
}
}
}
private static boolean isSorted(int[] array) {
for (int i = 1; i < array.length; i++) {
if (array[i] < array[i - 1]) return false; // 检查是否有序
}
return true;
}
时间复杂度分析
- 最坏情况 O(∞):随机排序可能需要无限次尝试才能成功排序。
- 最好情况 O(n):在极少数情况下,数组可能在第一次随机排列时就已经有序。
- 平均情况 O(n):在随机情况下,成功排序的期望次数是无穷大,因此该算法在实际中几乎没有应用。
空间复杂度
- O(1),只使用了常数级别的额外空间。
稳定性分析
随机排序是稳定的,因为在交换过程中相等元素的相对位置不会改变。
10. 桶排序 (Bucket Sort) - 时间复杂度:O(n²) / O(n + k) / O(n + k)
逻辑解释
桶排序是一种分布式排序算法,将数据分到有限数量的桶中,每个桶内部再进行排序(通常使用其他排序算法),最后再将所有桶中的数据合并。适合处理均匀分布的数据。
Java代码
import java.util.ArrayList;
import java.util.Collections;
public static void bucketSort(int[] array, int bucketSize) {
if (array.length == 0) return;
// 1. 找到最大值和最小值
int minValue = array[0];
int maxValue = array[0];
for (int num : array) {
if (num < minValue) minValue = num;
if (num > maxValue) maxValue = num;
}
// 2. 创建桶
int bucketCount = (maxValue - minValue) / bucketSize + 1;
ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 3. 将元素放入桶中
for (int num : array) {
buckets.get((num - minValue) / bucketSize).add(num);
}
// 4. 对每个桶进行排序并合并
int index = 0;
for (ArrayList<Integer> bucket : buckets) {
Collections.sort(bucket); // 可以使用其他排序算法
for (int num : bucket) {
array[index++] = num;
}
}
}
时间复杂度分析
- 最坏情况 O(n²):当所有元素都落在同一个桶中时,桶内部的排序可能退化为O(n²)。
- 最好情况 O(n + k):当数据均匀分布且桶的数量k足够大时,每个桶内的元素数量较少,排序时间复杂度为O(n)。
- 平均情况 O(n + k):在一般情况下,桶的数量k与元素数量n的关系良好,时间复杂度为O(n + k)。
空间复杂度
- O(n + k),需要额外的空间存储桶。
稳定性分析
桶排序是稳定的,因为相同元素在同一个桶内的相对位置不会改变。
11. 基数排序 (Radix Sort) - 时间复杂度:O(nk) / O(nk) / O(nk)
逻辑解释
基数排序通过逐位比较数字进行排序,适用于整数和字符串的排序。其基本思路是:
- 从最低位开始,对每一位进行计数排序。
- 先对个位进行排序,再对十位进行排序,以此类推,直到最高位。
Java代码
public static void radixSort(int[] array) {
int max = getMax(array); // 找到数组中的最大值
// 对每一位进行计数排序
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSort(array, exp);
}
}
private static int getMax(int[] array) {
int max = array[0];
for (int num : array) {
if (num > max) {
max = num; // 找到最大值
}
}
return max;
}
private static void countingSort(int[] array, int exp) {
int n = array.length;
int[] output = new int[n]; // 输出数组
int[] count = new int[10]; // 基数范围0-9
// 统计每个元素的出现次数
for (int num : array) {
count[(num / exp) % 10]++;
}
// 累加计数
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组
for (int i = n - 1; i >= 0; i--) {
output[count[(array[i] / exp) % 10] - 1] = array[i];
count[(array[i] / exp) % 10]--; // 减少计数
}
System.arraycopy(output, 0, array, 0, n); // 拷贝回原数组
}
时间复杂度分析
- 最坏情况 O(nk):基数排序的时间复杂度为O(nk),其中n是元素数量,k是数字的位数。对于每一位的计数排序需要O(n)的时间。
- 最好情况 O(nk):无论数据如何,基数排序对每一位的排序都需要进行,因此时间复杂度为O(nk)。
- 平均情况 O(nk):在随机情况下,基数排序的时间复杂度仍为O(nk)。
空间复杂度
- O(n + k),需要额外的空间存储计数数组和输出数组。
稳定性分析
基数排序是稳定的,因为在对每一位进行计数排序时,相同元素的相对位置不会改变。