排序算法解析

 介绍几种排序算法,包括快速排序、归并排序、堆排序、希尔排序、插入排序、选择排序、冒泡排序、计数排序、随机排序、桶排序和基数排序。 

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)

逻辑解释

归并排序是一种分治法的排序算法。将数组分成两部分,分别排序后再合并。具体步骤:

  1. 将数组分成两半,直到每个部分只有一个元素。
  2. 将两个部分合并成一个有序数组。
  3. 递归进行这个过程,直到所有部分都合并成一个完整的有序数组。

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)

逻辑解释

堆排序利用堆这种数据结构的特性,首先构建最大堆或最小堆,然后依次取出堆顶元素,放到已排序部分。具体步骤:

  1. 将数组构建成最大堆。
  2. 取出堆顶元素(最大元素),将其与数组最后一个元素交换。
  3. 除去最后一个元素(已排序),重新调整堆。
  4. 重复上述步骤,直到所有元素都排序完成。

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²)

逻辑解释

插入排序的思想是将数组分为已排序和未排序两部分,每次从未排序部分取出一个元素,插入到已排序部分的适当位置。具体步骤:

  1. 从第二个元素开始,假设第一个元素已经排序。
  2. 将当前元素与已排序部分的元素进行比较,找到合适的位置插入。
  3. 将大于当前元素的元素向后移动一位,为当前元素腾出位置。
  4. 重复这个过程,直到所有元素都排序完成。

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²)

逻辑解释

选择排序的基本思想是每次从未排序的部分中选择最小的元素,将其放到已排序部分的末尾。具体步骤如下:

  1. 从待排序的数组中找到最小的元素。
  2. 将其与数组的第一个元素交换位置。
  3. 再从剩下的元素中继续寻找最小的元素,与第二个元素交换。
  4. 重复这个过程,直到所有元素都排序完成。

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²)

逻辑解释

冒泡排序的思想是通过多次遍历数组,比较相邻的元素并交换它们的位置,使得较大的元素逐渐“冒泡”到数组的顶端。具体步骤:

  1. 从数组的第一对元素开始比较。
  2. 如果第一个元素大于第二个元素,则交换它们的位置。
  3. 继续比较下一对元素,重复这个过程,直到到达数组的末尾。
  4. 每一轮遍历后,最大的元素都会被移动到数组的末尾。
  5. 重复上述过程,直到没有需要交换的元素为止。

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)

逻辑解释

计数排序通过统计每个元素出现的次数,然后根据计数信息直接构建已排序的数组。具体步骤:

  1. 找到数组中的最大值,确定计数数组的大小。
  2. 统计每个元素的出现次数,存入计数数组。
  3. 计算每个元素的累积计数。
  4. 根据计数数组构建输出数组。

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)

逻辑解释

基数排序通过逐位比较数字进行排序,适用于整数和字符串的排序。其基本思路是:

  1. 从最低位开始,对每一位进行计数排序。
  2. 先对个位进行排序,再对十位进行排序,以此类推,直到最高位。

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),需要额外的空间存储计数数组和输出数组。

稳定性分析

基数排序是稳定的,因为在对每一位进行计数排序时,相同元素的相对位置不会改变。

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值