常见排序的学习

排序


文章目录


排序的概念及引用

排序是计算机科学中最基本且常用的操作之一,其核心是将一组数据按照特定的顺序(如升序、降序)重新排列。

排序的基本概念

  1. 定义

    • 排序是将一组数据元素按照某种规则(如数值大小、字母顺序)重新排列成有序序列的过程。
    • 输入:无序的元素集合
    • 输出:有序的元素序列
  2. 排序的稳定性

    • 稳定排序:相等元素的相对顺序在排序后保持不变。例如,冒泡排序、插入排序、归并排序。
    • 不稳定排序:相等元素的相对顺序可能改变。例如,选择排序、快速排序、堆排序。

二、常见排序算法分类

类别 算法名称 时间复杂度(平均) 空间复杂度 稳定性
插入类 插入排序 (O(n^2)) (O(1)) 稳定
希尔排序 (O(n^{1.3})) (O(1)) 不稳定
选择类 选择排序 (O(n^2)) (O(1)) 不稳定
堆排序 (O(n \log n)) (O(1)) 不稳定
交换类 冒泡排序 (O(n^2)) (O(1)) 稳定
快速排序 (O(n \log n)) (O(\log n)) 不稳定
归并类 归并排序 (O(n \log n)) (O(n)) 稳定

三、排序算法的应用场景

  1. 插入排序

    • 适合小规模数据或部分有序数据。
  2. 希尔排序

    • 插入排序的优化版,适合中等规模数据。
  3. 选择排序

    • 交换次数少,适合交换成本高的场景。
  4. 堆排序

    • 高效且稳定,适合大规模数据。
    • 应用:Top-K问题。
  5. 冒泡排序

    • 简单但效率低,适合教学演示或小规模数据。
  6. 快速排序

    • 实际应用中最快的排序算法之一。
  7. 归并排序

    • 稳定且适合外排序。

四、排序算法的选择策略

  1. 数据规模

    • 小规模数据:插入排序、冒泡排序
    • 中等规模数据:希尔排序、归并排序
    • 大规模数据:快速排序、堆排序、归并排序
  2. 数据特性

    • 部分有序数据:插入排序
    • 随机数据:快速排序、归并排序
    • 要求稳定排序:归并排序、插入排序
  3. 空间限制

    • 内存充足:归并排序、快速排序
    • 内存受限:堆排序、选择排序

五、排序算法的优化

  1. 快速排序优化

    • 三数取中法:选择左、中、右三个元素的中位数作为基准值,避免最坏情况
    • 插入排序优化:当子数组长度较小时,改用插入排序
  2. 归并排序优化

    • 原地归并:通过链表实现原地归并,减少空间复杂度
    • 混合排序:结合插入排序处理小规模子数组
  3. 堆排序优化

    • 多叉堆:使用四叉堆或八叉堆,减少树的高度,提高效率

总结

排序算法是计算机科学的基础,不同的算法适用于不同的场景。选择合适的排序算法需要考虑数据规模、数据特性和空间限制等因素。在实际应用中,通常会结合多种排序算法的优点,以达到最佳性能

常见排序介绍

插入排序

插入排序是一种简单直观的比较排序算法,其基本思想类似于我们在整理扑克牌时,将新拿到的牌插入到手中已有牌的合适位置,使手中的牌始终保持有序。以下是关于插入排序的详细介绍:

算法原理

插入排序将待排序的数组分为已排序区间和未排序区间两部分。初始时,已排序区间只有数组的第一个元素,其余元素都在未排序区间。算法从第二个元素开始,依次将未排序区间的元素插入到已排序区间的合适位置,使得已排序区间不断扩大,直到未排序区间为空,此时整个数组就完成了排序。

具体步骤

  1. 从数组的第二个元素(索引为1)开始,将其视为待插入元素。
  2. 把待插入元素与已排序区间(即数组中索引0到当前元素前一个位置的元素)的元素从后往前依次比较。
  3. 如果待插入元素小于已排序区间中的元素,就将已排序区间中该元素向后移动一位,为待插入元素腾出位置。
  4. 重复步骤3,直到找到一个小于或等于待插入元素的位置,或者已排序区间的所有元素都被比较过。
  5. 将待插入元素插入到找到的位置。
  6. 对数组中剩余的未排序元素(即索引从2到数组末尾的元素),重复步骤2 - 5,直到所有元素都被插入到已排序区间,数组变为有序

代码实现:

java 复制代码
public class InsertionSort {
    public static void insertionSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int key = arr[i];  // 待插入元素
            int j = i - 1;
            // 将已排序区间中大于key的元素后移
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;  // 将key插入到合适位置
        }
    }

    public static void main(String[] args) {
        int[] arr = {5, 2, 4, 6, 1, 3};
        insertionSort(arr);
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

时间复杂度

  • 最好情况 :当数组本身已经是有序状态时,每一次插入操作只需要比较一次,不需要移动元素。对于长度为n的数组,只需要进行n - 1次比较,时间复杂度为 O ( n ) O(n) O(n)。
  • 最坏情况 :当数组是逆序状态时,每一次插入操作都需要与已排序区间的所有元素进行比较,并且移动相应数量的元素。第i次插入时,需要比较i次,移动i次。对于长度为n的数组,总的比较和移动次数为 1 + 2 + 3 + ⋯ + ( n − 1 ) = n ( n − 1 ) 2 1 + 2 + 3 + \cdots + (n - 1) = \frac{n(n - 1)}{2} 1+2+3+⋯+(n−1)=2n(n−1),时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
  • 平均情况 :假设数组中元素的初始排列是随机的,平均情况下插入排序的时间复杂度也是 O ( n 2 ) O(n^2) O(n2)。

空间复杂度

插入排序是一种原地排序算法,它只需要几个额外的变量来进行元素的比较和移动,不需要额外的存储空间来存储整个数组,因此空间复杂度为 O ( 1 ) O(1) O(1)。

稳定性

插入排序是一种稳定的排序算法,因为在比较和移动元素时,相等元素的相对顺序不会改变。例如,对于数组[2, 2, 1],排序后两个2的相对顺序仍然保持不变。

适用场景

插入排序适用于小规模数据或者部分有序的数据。由于其简单的实现方式,在数据量不大时,插入排序的性能表现尚可;而对于大规模的无序数据,插入排序的时间复杂度较高,效率不如快速排序、归并排序等时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的算法

希尔排序

希尔排序是插入排序的一种改进版本,也称为缩小增量排序 。它通过引入"增量"概念,将原数组分割成多个子序列分别进行插入排序,逐步缩小增量直至为1,最终完成整个数组的排序。其核心思想是先让数组整体接近有序,再进行精细排序,从而克服插入排序在处理大规模无序数据时效率低下的问题

算法原理

  1. 增量序列 :选择一个增量序列(如 n/2, n/4, ..., 1,其中 n 为数组长度),增量决定了子序列的划分方式。
  2. 分组排序 :对于每个增量 gap,将数组分割成若干个长度为 gap 的子序列(每个子序列由下标相差 gap 的元素组成),对每个子序列单独进行插入排序。
  3. 缩小增量:逐步减小增量,重复分组排序过程,直到增量为1。此时,整个数组被视为一个子序列,进行最后一次插入排序,最终得到有序数组。

通过逐步缩小增量,数组会逐渐变得接近有序,当增量为1时,插入排序的效率会非常高(接近 O ( n ) O(n) O(n))。

具体步骤

以数组 [8, 9, 1, 7, 2, 3, 5, 4, 6, 0] 为例,使用增量序列 5, 2, 1 演示:

  1. 初始增量 gap=5

    将数组分为5个子序列:[8,3][9,5][1,4][7,6][2,0],对每个子序列插入排序后得到:
    [3, 5, 1, 6, 0, 8, 5, 4, 7, 9](整体更接近有序)。

  2. 增量 gap=2

    按增量2分组:[3,1,0,5,7][5,6,8,4,9],插入排序后得到:
    [0, 4, 1, 5, 3, 6, 5, 7, 7, 9](进一步接近有序)。

  3. 增量 gap=1

    对整个数组进行插入排序,最终得到有序数组:
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

代码实现

java 复制代码
public class ShellSort {
    public static void shellSort(int[] arr) {
        int n = arr.length;
        // 初始增量为n/2,每次缩小为原来的1/2
        for (int gap = n / 2; gap > 0; gap /= 2) {
            // 对每个子序列进行插入排序
            for (int i = gap; i < n; i++) {
                int key = arr[i]; // 待插入元素
                int j = i - gap;  // 子序列中前一个元素的索引
                // 移动子序列中大于key的元素
                while (j >= 0 && arr[j] > key) {
                    arr[j + gap] = arr[j];
                    j -= gap;
                }
                arr[j + gap] = key; // 插入待排序元素
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        shellSort(arr);
        for (int num : arr) {
            System.out.print(num + " "); // 输出:0 1 2 3 4 5 6 7 8 9
        }
    }
}

时间复杂度

希尔排序的时间复杂度依赖于增量序列的选择,目前尚未有精确的数学表达式,常见分析如下:

  • 若选择增量序列为 n/2, n/4, ..., 1,时间复杂度约为 O ( n 2 ) O(n^2) O(n2)。

总体而言,希尔排序的时间复杂度在 O ( n log ⁡ n ) O(n \log n) O(nlogn) 到 O ( n 2 ) O(n^2) O(n2) 之间,优于插入排序

空间复杂度

希尔排序是原地排序算法,仅需常数个额外变量(如 gapkey),因此空间复杂度为 O ( 1 ) O(1) O(1)。

稳定性

希尔排序是不稳定 的排序算法。因为在分组排序时,相同元素可能被分到不同子序列中,导致相对顺序被改变。例如,数组 [3, 2, 2] 以增量 gap=2 分组时,第一个 2 会被移到 3 之前,破坏稳定性。

适用场景

希尔排序适用于中等规模的无序数据,其优点是实现简单、空间开销小,且对部分有序数据的处理效率较高。

总结

希尔排序通过分组排序和逐步缩小增量,平衡了插入排序的简单性和高效性,是插入排序的重要优化版本,在实践中具有广泛应用价值

直接选择排序

直接选择排序是一种简单的选择排序算法,其核心思想是每次从待排序区间中选出最小(或最大)的元素,将其与待排序区间的第一个元素交换位置,逐步扩大已排序区间,直至整个数组有序。

算法原理

直接选择排序将数组分为已排序区间未排序区间

  • 初始时,已排序区间为空,未排序区间为整个数组。
  • 重复以下步骤:
    1. 在未排序区间中找到最小元素的索引
    2. 将该最小元素与未排序区间的第一个元素交换位置
    3. 已排序区间长度增加1,未排序区间长度减少
  • 直至未排序区间为空,数组完全有序

具体步骤

以数组 [5, 3, 8, 4, 2] 为例:

  1. 初始状态 :已排序区间 [],未排序区间 [5, 3, 8, 4, 2]

    找到未排序区间最小值 2,与第一个元素 5 交换 → [2, 3, 8, 4, 5]

  2. 已排序区间 [2] ,未排序区间 [3, 8, 4, 5]

    找到最小值 3,与未排序区间第一个元素 3 交换(无需变动)→ [2, 3, 8, 4, 5]

  3. 已排序区间 [2, 3] ,未排序区间 [8, 4, 5]

    找到最小值 4,与 8 交换 → [2, 3, 4, 8, 5]

  4. 已排序区间 [2, 3, 4] ,未排序区间 [8, 5]

    找到最小值 5,与 8 交换 → [2, 3, 4, 5, 8]

  5. 排序完成,数组有序。

代码实现

java 复制代码
public class SelectionSort {
    public static void selectionSort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            // 未排序区间的起始索引为i,找到最小值的索引
            int minIndex = i;
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j; // 更新最小值索引
                }
            }
            // 交换最小值与未排序区间的第一个元素
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }

    public static void main(String[] args) {
        int[] arr = {5, 3, 8, 4, 2};
        selectionSort(arr);
        for (int num : arr) {
            System.out.print(num + " "); // 输出:2 3 4 5 8
        }
    }
}

时间复杂度

  • 最好情况 :数组已有序,但仍需遍历未排序区间找最小值,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 最坏情况 :数组逆序,同样需要 O ( n 2 ) O(n^2) O(n2) 的比较次数
  • 平均情况 :时间复杂度为 O ( n 2 ) O(n^2) O(n2)

无论数组初始状态如何,直接选择排序的比较次数固定为 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1),因此时间复杂度稳定为 O ( n 2 ) O(n^2) O(n2)。

空间复杂度

仅需常数个额外变量(如 minIndextemp),属于原地排序,空间复杂度为 O ( 1 ) O(1) O(1)。

稳定性

直接选择排序是不稳定 的排序算法。例如,数组 [2, 2, 1] 中,第一个 2 会与 1 交换,导致两个 2 的相对顺序改变。

适用场景

适用于数据量较小交换成本较高 的场景(如元素体积大、交换耗时)。由于其交换次数少(最多 n − 1 n-1 n−1 次),在某些特定场景下比冒泡排序更高效,但整体效率低于插入排序(尤其是部分有序数据)。

总结

直接选择排序是一种直观但效率较低的排序算法,其核心是"选最小、放前面",适合小规模数据或对交换操作敏感的场景,因稳定性差和时间复杂度较高,大规模数据排序中较少使用

堆排序

堆排序是一种基于堆数据结构的高效排序算法,利用了堆的特性(大根堆或小根堆)来实现排序,时间复杂度稳定为 O(n log n),且是原地排序算法。

堆的基本概念

  1. 堆的定义:堆是一种完全二叉树,满足以下性质:

    • 大根堆 :每个父节点的值 大于等于 其左右子节点的值(根节点为最大值)。
    • 小根堆 :每个父节点的值 小于等于 其左右子节点的值(根节点为最小值)。
  2. 堆与数组的映射:堆通常用数组实现,对于下标为 i 的节点:

    • 左子节点下标:2i + 1
    • 右子节点下标:2i + 2
    • 父节点下标:(i - 1) / 2(整数除法)

堆排序的核心思想

堆排序的核心是 "利用大根堆选出最大值,逐步构建有序序列",步骤分为:

  1. 建堆:将无序数组构建成大根堆(根节点为最大值)
  2. 排序
    • 交换堆顶(最大值)与堆的最后一个元素,将最大值放到数组末尾(已排序)
    • 缩小堆的范围(排除已排序的末尾元素),重新调整剩余元素为大根堆
    • 重复上述过程,直至堆中只剩一个元素,数组完全有序

具体步骤(以数组 [4, 6, 8, 5, 9] 为例)

步骤1:构建大根堆

  • 从最后一个非叶子节点(下标 (n/2 - 1) = 1,值为6)开始,依次向上调整堆:
    • 调整下标1的节点:6的右子节点9更大,交换后堆为 [4, 9, 8, 5, 6]
    • 调整下标0的节点:4的左子节点9更大,交换后堆为 [9, 4, 8, 5, 6];继续调整4的子节点,最终大根堆为 [9, 6, 8, 5, 4]

步骤2:排序过程

  1. 交换堆顶9与末尾元素4 → 数组 [4, 6, 8, 5 | 9](| 后为已排序)。
    调整剩余元素为大根堆 → [8, 6, 4, 5 | 9]
  2. 交换堆顶8与末尾元素5 → 数组 [5, 6, 4, 8 | 9]
    调整剩余元素为大根堆 → [6, 5, 4, 8 | 9]
  3. 交换堆顶6与末尾元素4 → 数组 [4, 5, 6, 8 | 9]
    调整剩余元素为大根堆 → [5, 4, 6, 8 | 9]
  4. 交换堆顶5与末尾元素4 → 数组 [4, 5, 6, 8, 9],排序完成。

代码实现

java 复制代码
public class HeapSort {
    // 堆排序主方法
    public static void heapSort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        int n = arr.length;

        // 步骤1:构建大根堆(从最后一个非叶子节点向上调整)
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }

        // 步骤2:排序(交换堆顶与末尾元素,重新调整堆)
        for (int i = n - 1; i > 0; i--) {
            // 交换堆顶(最大值)与当前堆的最后一个元素
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;

            // 缩小堆范围,重新调整堆
            heapify(arr, i, 0);
        }
    }

    // 调整堆(确保以i为根的子树是大根堆)
    private static void heapify(int[] arr, int heapSize, int i) {
        int largest = i; // 初始化最大值为根节点
        int left = 2 * i + 1; // 左子节点
        int right = 2 * i + 2; // 右子节点

        // 比较左子节点与根节点
        if (left < heapSize && arr[left] > arr[largest]) {
            largest = left;
        }
        // 比较右子节点与当前最大值
        if (right < heapSize && arr[right] > arr[largest]) {
            largest = right;
        }

        // 若最大值不是根节点,交换并递归调整
        if (largest != i) {
            int temp = arr[i];
            arr[i] = arr[largest];
            arr[largest] = temp;
            heapify(arr, heapSize, largest);
        }
    }

    public static void main(String[] args) {
        int[] arr = {4, 6, 8, 5, 9};
        heapSort(arr);
        for (int num : arr) {
            System.out.print(num + " "); // 输出:4 5 6 8 9
        }
    }
}

复杂度分析

  • 时间复杂度

    • 建堆:O(n)(通过数学推导,所有节点的调整时间总和为 O(n))。
    • 排序:O(n log n)(每次调整堆耗时 O(log n)),共 n-1 次)。
    • 整体:O(n og n)(无论最好/最坏情况,复杂度稳定)。
  • 空间复杂度:O(1)(原地排序,仅需常数个额外变量)。

稳定性

堆排序是 不稳定 的排序算法。例如,数组 [2, 2, 1] 建堆后交换元素可能导致两个 2 的相对顺序改变。

适用场景

堆排序适用于 大规模数据排序,尤其是对时间复杂度稳定性要求高的场景(如数据库排序、大数据处理)。其优点是效率高且不依赖额外空间,但实现较复杂,且不适合小规模数据(相比插入排序无优势)。

总结

堆排序通过堆的特性实现高效排序,是一种原地、时间复杂度稳定的算法,适合处理大规模数据,在实际工程中应用广泛(如Top-K问题求解等)

冒泡排序

冒泡排序是一种简单直观的排序算法,其核心思想是通过重复遍历数组,每次比较相邻的两个元素,若顺序错误则交换它们的位置

算法原理

冒泡排序的工作过程如下:

  1. 从数组的第一个元素开始,依次比较相邻的两个元素(下标 ii+1
  2. 如果前一个元素大于后一个元素,则交换它们的位置(确保较大元素向后移动)
  3. 遍历完一次遍历后,最大的元素会"冒泡"到数组的末尾(已排序区间)
  4. 缩小未排序区间(排除已就位的最大元素),重复上述过程,直到所有元素有序

可以优化的点:如果某轮遍历中若未发生任何次交换交换,说明数组已完全有序,可提前结束排序

具体步骤(以数组 [5, 3, 8, 4, 2] 为例)

  1. 第一轮遍历 (未排序区间 [5, 3, 8, 4, 2]):

    • 比较5和3 → 交换 → [3, 5, 8, 4, 2]
    • 比较5和8 → 不交换 → [3, 5, 8, 4, 2]
    • 比较8和4 → 交换 → [3, 5, 4, 8, 2]
    • 比较8和2 → 交换 → [3, 5, 4, 2, 8]
      结果:最大元素8已就位,未排序区间变为 [3, 5, 4, 2]
  2. 第二轮遍历

    • 比较3和5 → 不交换
    • 比较5和4 → 交换 → [3, 4, 5, 2, 8]
    • 比较5和2 → 交换 → [3, 4, 2, 5, 8]
      结果:第二大元素5就位,未排序区间 [3, 4, 2]
  3. 第三轮遍历

    • 比较3和4 → 不交换
    • 比较4和2 → 交换 → [3, 2, 4, 5, 8]
      结果:第三大元素4就位,未排序区间 [3, 2]
  4. 第四轮遍历

    • 比较3和2 → 交换 → [2, 3, 4, 5, 8]
      结果:数组完全有序。

代码实现(含优化)

java 复制代码
public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        int n = arr.length;
        boolean swapped; // 标记是否发生交换

        for (int i = 0; i < n - 1; i++) {
            swapped = false;
            // 每轮遍历后,最大元素已就位,无需再比较
            for (int j = 0; j < n - i - 1; 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;
        }
    }

    public static void main(String[] args) {
        int[] arr = {5, 3, 8, 4, 2};
        bubbleSort(arr);
        for (int num : arr) {
            System.out.print(num + " "); // 输出:2 3 4 5 8
        }
    }
}

复杂度分析

  • 时间复杂度

    • 最好情况:数组已完全有序,仅需1轮遍历(无交换),时间复杂度为 (O(n))。
    • 最坏情况:数组逆序,需 (n-1) 轮遍历,每轮比较 (n-i-1) 次,总次数为 (n(n-1)/2),时间复杂度为 (O(n^2))。
    • 平均情况:时间复杂度为 (O(n^2))。
  • 空间复杂度 :仅需常数个额外变量(如 tempswapped),属于原地排序,空间复杂度为 (O(1))。

稳定性

冒泡排序是 稳定 的排序算法。因为当两个元素相等时,不会发生交换,它们的相对顺序得以保持。例如,数组 [2, 2, 1] 排序后仍为 [1, 2, 2],两个2的相对位置不变。

适用场景

冒泡排序适用于 小规模数据教学演示。由于其时间复杂度较高O(n^2),在实际开发中很少用于大规模数据排序,但因实现简单、易于理解,常作为入门级排序算法学习。

总结

冒泡排序通过相邻元素的比较和交换实现排序,核心是"大数后移",优点是简单稳定,缺点是效率低。仅推荐在数据量小或对性能要求不高的场景使用

快速排序

快速排序一种高效的排序算法,基于分治法思想,通过选择一个"基准值"将数组分为两部分,再递归排序这两部分,平均时间复杂度为 O(n log n),是实际应用中性能最佳的排序算法之一

算法原理

快速排序的核心步骤是"分区",具体过程如下:

  1. 选择基准值(Pivot):从数组中选择一个元素作为基准(通常选最后一个元素,或随机元素)。
  2. 分区操作:将数组重新排列,使所有小于基准值的元素移到基准值左侧,所有大于基准值的元素移到右侧(等于基准值的元素可放任意一侧)。此时基准值的位置已固定。
  3. 递归排序:对基准值左侧和右侧的子数组分别重复步骤1-2,直至子数组长度为1(天然有序)

具体步骤(以数组 [3, 6, 8, 10, 1, 2, 1] 为例,基准值选最后一个元素1)

  1. 第一次分区

    • 目标:小于1的元素放左,大于1的放右(基准值1)。
    • 过程:遍历数组,交换元素后分区为 [1, 6, 8, 10, 3, 2 | 1](| 为基准值位置)。
    • 结果:基准值1固定,左侧子数组 [1,6,8,10,3,2],右侧子数组为空。
  2. 递归排序左侧子数组(基准值选最后一个元素2):

    • 分区后:[1, 2 | 8, 10, 3, 6],基准值2固定。
    • 左侧 [1] 有序,右侧子数组 [8,10,3,6] 继续排序。
  3. 递归排序 [8,10,3,6](基准值选6):

    • 分区后:[3, 6 | 10, 8],基准值6固定。
    • 左侧 [3] 有序,右侧子数组 [10,8] 继续排序。
  4. 递归排序 [10,8](基准值选8):

    • 分区后:[8 | 10],基准值8固定。
    • 两侧子数组 [8][10] 均有序。
  5. 最终结果[1, 1, 2, 3, 6, 8, 10]

代码实现(含优化)

java 复制代码
import java.util.Random;

public class QuickSort {
    private static final Random random = new Random();

    public static void quickSort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        quickSortHelper(arr, 0, arr.length - 1);
    }

    // 递归辅助函数
    private static void quickSortHelper(int[] arr, int low, int high) {
        if (low < high) {
            // 分区操作,返回基准值位置
            int pivotIndex = partition(arr, low, high);
            // 递归排序左侧子数组
            quickSortHelper(arr, low, pivotIndex - 1);
            // 递归排序右侧子数组
            quickSortHelper(arr, pivotIndex + 1, high);
        }
    }

    // 分区操作(优化:随机选择基准值,避免极端情况)
    private static int partition(int[] arr, int low, int high) {
        // 随机选择基准值并与末尾元素交换
        int randomIndex = low + random.nextInt(high - low + 1);
        swap(arr, randomIndex, high);
        
        int pivot = arr[high]; // 基准值
        int i = low - 1; // 小于基准值区域的边界
        
        for (int j = low; j < high; j++) {
            // 若当前元素小于等于基准值,扩展小于区域
            if (arr[j] <= pivot) {
                i++;
                swap(arr, i, j);
            }
        }
        
        // 将基准值放到正确位置(小于区域的下一个位置)
        swap(arr, i + 1, high);
        return i + 1; // 返回基准值索引
    }

    // 交换元素
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr = {3, 6, 8, 10, 1, 2, 1};
        quickSort(arr);
        for (int num : arr) {
            System.out.print(num + " "); // 输出:1 1 2 3 6 8 10
        }
    }
}

复杂度分析

  • 时间复杂度

    • 最好/平均情况:每次分区能将数组均匀分为两部分,递归深度为 (O(\log n)),总时间复杂度为 (O(n \log n))。
    • 最坏情况:数组已排序且选择两端元素为基准,每次分区后子数组长度仅减1,递归深度为 (O(n)),时间复杂度为 (O(n^2))(可通过随机选择基准值避免)。
  • 空间复杂度:主要来自递归栈,平均为 (O(\log n)),最坏为 (O(n))(可通过尾递归优化至 (O(\log n)))。

稳定性

快速排序是 不稳定 的排序算法。分区过程中,相等元素的相对顺序可能被交换。例如,数组 [2, 2, 1] 排序时,第一个2可能被交换到1后面,破坏稳定性。

适用场景

快速排序适用于 大规模数据排序 ,是实际开发中应用最广泛的排序算法之一

其优势在于平均效率高、原地排序(空间开销小),但实现较复杂,且对小规模数据不如插入排序高效(可混合使用优化)。

优化技巧

  1. 随机选择基准值:避免数组有序时的最坏情况。
  2. 三数取中法:选择左、中、右三个元素的中位数作为基准,进一步优化分区平衡性。
  3. 小规模子数组用插入排序:当子数组长度小于阈值(如10)时,切换为插入排序,减少递归开销。
  4. 尾递归优化:减少递归栈深度,降低栈溢出风险。

总结

快速排序通过分治思想和高效分区实现排序,平均性能优异,是处理大规模数据的首选算法之一。其核心在于基准值的选择和分区操作,合理优化后可避免最坏情况,在工程实践中应用广泛

归并排序

归并排序一种基于分治法的高效排序算法,其核心思想是将数组分成两个子数组,分别排序后再合并为一个有序数组。归并排序具有稳定性好、时间复杂度稳定的特点,是处理大规模数据排序的常用算法

算法原理

归并排序的工作流程分为三个步骤:

  1. 分解:将待排序数组不断二分,直到子数组长度为1(单个元素天然有序)。
  2. 解决:递归地对每个子数组进行排序。
  3. 合并:将两个已排序的子数组合并为一个更大的有序数组。

整个过程类似"拆分-排序-合并"的流水线,通过递归实现对整个数组的排序。

具体步骤(以数组 [38, 27, 43, 3, 9, 82, 10] 为例)

  1. 分解阶段

    将数组反复二分,直至子数组长度为1:
    [38,27,43,3,9,82,10] → 分左右 → [38,27,43][3,9,82,10]

    继续二分 → [38][27,43][3,9][82,10]

    最终分解为:[38][27][43][3][9][82][10]

  2. 合并阶段

    从最小的子数组开始,两两合并为有序数组:

    • 合并 [27][43][27,43];合并 [38][27,43][27,38,43]
    • 合并 [3][9][3,9];合并 [82][10][10,82];再合并 [3,9][10,82][3,9,10,82]
    • 最后合并 [27,38,43][3,9,10,82][3,9,10,27,38,43,82](最终有序数组)。

代码实现

java 复制代码
public class MergeSort {
    // 归并排序主方法
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        int[] temp = new int[arr.length]; // 辅助数组,用于临时存储合并结果
        mergeSortHelper(arr, 0, arr.length - 1, temp);
    }

    // 递归辅助方法:排序 arr[left..right] 区间
    private static void mergeSortHelper(int[] arr, int left, int right, int[] temp) {
        if (left < right) { // 递归终止条件:子数组长度为1(left == right)
            int mid = left + (right - left) / 2; // 计算中间位置(避免溢出)
            
            // 递归排序左半部分
            mergeSortHelper(arr, left, mid, temp);
            // 递归排序右半部分
            mergeSortHelper(arr, mid + 1, right, temp);
            
            // 合并两个有序子数组
            merge(arr, left, mid, right, temp);
        }
    }

    // 合并两个有序子数组:arr[left..mid] 和 arr[mid+1..right]
    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;    // 左子数组的起始索引
        int j = mid + 1; // 右子数组的起始索引
        int k = left;    // 临时数组的起始索引

        // 比较两个子数组的元素,按顺序放入临时数组
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++]; // 左子数组元素更小,放入临时数组
            } else {
                temp[k++] = arr[j++]; // 右子数组元素更小,放入临时数组
            }
        }

        // 处理左子数组剩余元素
        while (i <= mid) {
            temp[k++] = arr[i++];
        }

        // 处理右子数组剩余元素
        while (j <= right) {
            temp[k++] = arr[j++];
        }

        // 将临时数组中的有序元素复制回原数组
        for (k = left; k <= right; k++) {
            arr[k] = temp[k];
        }
    }

    // 测试
    public static void main(String[] args) {
        int[] arr = {38, 27, 43, 3, 9, 82, 10};
        mergeSort(arr);
        for (int num : arr) {
            System.out.print(num + " "); // 输出:3 9 10 27 38 43 82
        }
    }
}

复杂度分析

  • 时间复杂度

    • 分解数组的递归深度为 O(log n)(每次二分)。
    • 每一层的合并操作总耗时为 (O(n)(每个元素被合并一次)。
    • 整体时间复杂度:最好/最坏/平均情况均为 O(n log n),稳定性极高
  • 空间复杂度

    需要一个与原数组等长的辅助数组(temp),因此空间复杂度为 O(n)

稳定性

归并排序是 稳定的排序算法 。在合并过程中,当两个子数组的元素相等时,会优先选择左子数组的元素,从而保持相等元素的相对顺序不变。例如,数组 [2, 2, 1] 排序后仍为 [1, 2, 2],两个 2 的原始顺序得以保留

适用场景

  1. 大规模数据排序:时间复杂度稳定为 (O(n \log n)),适合处理大数据量。
  2. 链表排序:无需辅助数组(通过指针操作合并),空间复杂度可优化至 (O(\log n))(递归栈)。
  3. 需要稳定排序的场景:如电商订单按金额排序时,需保持同金额订单的下单时间顺序。

优化方向

  1. 小规模子数组用插入排序:当子数组长度小于阈值(如15)时,切换为插入排序,减少递归和合并开销。
  2. 原地归并:通过数组移位实现合并,避免使用辅助数组(但会增加时间复杂度)。
  3. 并行化处理:分解后的子数组可并行排序,提高多核CPU环境下的效率。

总结

归并排序通过分治思想实现高效排序,具有时间复杂度稳定、稳定性好的优点,适合对排序稳定性有要求或处理大规模数据的场景。其主要缺点是需要额外的存储空间,这也是实际应用中与快速排序权衡的重要因素

排序总结

类别 算法名称 时间复杂度(平均) 空间复杂度 稳定性
插入类 插入排序 (O(n^2)) (O(1)) 稳定
希尔排序 (O(n^{1.3})) (O(1)) 不稳定
选择类 选择排序 (O(n^2)) (O(1)) 不稳定
堆排序 (O(n \log n)) (O(1)) 不稳定
交换类 冒泡排序 (O(n^2)) (O(1)) 稳定
快速排序 (O(n \log n)) (O(\log n)) 不稳定
归并类 归并排序 (O(n \log n)) (O(n)) 稳定
相关推荐
麦兜*2 分钟前
Spring Boot 与 Ollama 集成部署私有LLM服务 的完整避坑指南,涵盖 环境配置、模型管理、性能优化 和 安全加固
java·spring boot·后端·安全·spring cloud·性能优化
leo__5205 分钟前
Java的NIO体系详解
java·python·nio
烟沙九洲5 分钟前
服务之间远程Feign调用,出现参数丢失
java·spring boot
Yang-Never9 分钟前
Kotlin协程 ->launch构建协程以及调度源码详解
android·java·开发语言·kotlin·android studio
极客BIM工作室12 分钟前
C++返回值优化(RVO):高效返回对象的艺术
java·开发语言·c++
用户849137175471613 分钟前
JustAuth实战系列(第1期):项目概览与价值分析
java·架构·开源
序属秋秋秋20 分钟前
《C++初阶之STL》【模板参数 + 模板特化 + 分离编译】
开发语言·c++·笔记·学习·stl
自由的疯32 分钟前
Java 17 新特性之 instanceof 运算符
java·后端·架构
自由的疯36 分钟前
Java 17 新特性之 Switch 表达式改进
java·后端·架构
玄昌盛不会编程1 小时前
LeetCode——2683. 相邻值的按位异或
java·算法·leetcode