归并排序算法

文章目录

  • 一、归并排序基本概念
    • 分治策略
  • 二、归并排序的具体实现步骤
  • 三、代码示例
    • `merge`函数解析
    • `mergeSort`函数解析
  • 四、时间复杂度和空间复杂度
  • 五、归并排序的应用场景

一、归并排序基本概念

归并排序(Merge Sort)是一种基于分治策略的高效排序算法。它的基本思想是将一个数组分成两个子数组,分别对这两个子数组进行排序,然后将排好序的子数组合并成一个最终的排序数组。

分治策略

  • 分解:把待排序的数组不断地划分成更小的子数组,直到每个子数组只包含一个元素。例如,对于数组{5, 2, 4, 7, 1, 3, 2, 6},会先将其分成{5, 2, 4, 7}{1, 3, 2, 6},然后继续划分,直到每个子数组只有一个元素。
  • 解决:当子数组的大小为 1 时,因为只有一个元素,所以可以认为它是已经排好序的。
  • 合并:将排好序的子数组合并起来,形成更大的排好序的子数组,最终合并成一个完整的排好序的数组。

二、归并排序的具体实现步骤

  1. 划分阶段
    • 首先,确定数组的中间位置,通常使用mid = (left + right)/2(其中left是子数组的左边界,right是子数组的右边界)来计算。例如,对于数组{1, 2, 3, 4, 5, 6, 7, 8},第一次划分时,left = 0right = 7mid = 4,就将数组划分为{1, 2, 3, 4}{5, 6, 7, 8}
    • 递归地对划分后的子数组进行划分,直到子数组的长度为 1。
  2. 合并阶段
    • 假设我们有两个已经排好序的子数组,比如leftArray = {1, 3, 5, 7}rightArray = {2, 4, 6, 8}
    • 创建一个临时数组来存储合并后的结果,大小为两个子数组长度之和。
    • 同时设置两个指针ij,分别指向两个子数组的起始位置(在这里i = 0j = 0)。
    • 比较两个子数组当前指针所指的元素(即leftArray[i]rightArray[j]),将较小的元素放入临时数组中。例如,比较12,将1放入临时数组,然后i++
    • 继续这个过程,直到其中一个子数组的所有元素都被放入临时数组。例如,当leftArray中的元素都放入临时数组后,就将rightArray中剩余的元素依次放入临时数组。
    • 最后,将临时数组中的元素复制回原数组对应的位置。

三、代码示例

#include 
#include 

// 合并两个已排序的子数组
void merge(int arr[], int left, int mid, int right) {
    int i, j, k;
    int n1 = mid - left + 1;
    int n2 = right - mid;
    // 创建临时数组
    int L[n1], R[n2];
    // 填充临时数组L
    for (i = 0; i < n1; i++)
        L[i] = arr[left + i];
    // 填充临时数组R
    for (j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];
    i = 0;
    j = 0;
    k = left;
    // 合并临时数组到原数组
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    // 复制L中剩余元素
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    // 复制R中剩余元素
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

// 归并排序主函数
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);
    }
}
int main() {
    int arr[] = {12, 11, 13, 5, 6, 7};
    int arr_size = sizeof(arr)/sizeof(arr[0]);
    printf("原始数组: ");
    for (int i = 0; i < arr_size; i++)
        printf("%d ", arr[i]);
    mergeSort(arr, 0, arr_size - 1);
    printf("\n排序后的数组: ");
    for (int i = 0; i < arr_size; i++)
        printf("%d ", arr[i]);
    return 0;
}

merge函数解析

  1. 变量定义与子数组长度计算
    • 首先定义了整型变量 ijk,它们将分别用于遍历两个临时数组以及合并后的原数组索引。
    • 计算两个待合并子数组的长度 n1n2,其中 n1 = mid - left + 1 表示左子数组(以 mid 划分)的长度,n2 = right - mid 表示右子数组的长度。例如,对于数组 {1, 2, 3, 4, 5, 6},若 left = 0mid = 2right = 5,那么 n1 = 3(对应子数组 {1, 2, 3}),n2 = 3(对应子数组 {4, 5, 6})。
  2. 创建临时数组
    • 根据前面计算出的长度 n1n2,创建两个临时数组 LR,用于存放两个待合并的子数组元素。
  3. 填充临时数组
    • 通过循环,将原数组中对应的左子数组元素复制到临时数组 L 中,循环条件 i < n1 确保了所有左子数组元素都能被正确复制。例如,原数组 {1, 2, 3, 4, 5, 6} 划分后的左子数组 {1, 2, 3} 会依次存入 L 数组。
    • 同样,使用另一个循环将原数组中的右子数组元素复制到临时数组 R 中,完成两个临时数组的填充工作。
  4. 合并临时数组到原数组
    • 使用 while 循环来比较两个临时数组 LR 当前位置(由 ij 指向)的元素大小。
    • 只要i < n1j < n2(即两个临时数组都还有未合并的元素),就进行比较:
      • 如果 L[i] <= R[j],说明左子数组当前元素较小,将其放入原数组 arr 中当前合并位置(由 k 指向),然后 i 自增,准备处理 L 数组的下一个元素。
      • 反之,如果 L[i] > R[j],则将右子数组当前元素放入原数组 arr 中当前合并位置,同时 j 自增。每放入一个元素,k 都自增,以指向下一个要放置合并元素的位置。
  5. 复制剩余元素
    • 当上述 while 循环结束后,可能存在某个临时数组还有剩余元素未合并到原数组中。
    • 通过两个单独的 while 循环,分别将 L 数组(如果有剩余)和 R 数组(如果有剩余)中剩余的元素依次复制到原数组 arr 中相应位置,确保所有元素都被正确合并回原数组。

mergeSort函数解析

  1. 递归终止条件判断
    • 函数首先判断 left 是否小于 right,如果不满足(即 left >= right),说明子数组中只有一个元素或者没有元素了,按照归并排序的逻辑,单个元素的子数组本身就是有序的,所以不需要再进行划分和排序,直接返回即可,这就是递归的终止条件。
  2. 计算中间位置并划分子数组
    • left < right 时,计算当前子数组的中间位置 mid,通过公式 mid = left + (right - left)/2 来获取。这个公式的目的是为了避免整数溢出问题(相比于直接使用 (left + right)/2),能准确地将当前子数组划分成左右两个子数组。
  3. 递归划分子数组
    • 接着,通过递归调用 mergeSort 函数分别对左子数组(范围是 [left, mid])和右子数组(范围是 [mid + 1, right])进行进一步的划分,不断将子数组规模缩小,直到每个子数组只有一个元素为止。这个过程体现了归并排序的 “分治” 思想,把大问题(对整个数组排序)逐步分解成小问题(对单个元素的子数组排序)。
  4. 合并子数组
    • 在左右子数组都已经递归排序完成后(也就是子数组都达到单个元素的有序状态后),调用 merge 函数将这两个已经排好序的子数组合并成一个更大的有序子数组,通过不断合并,最终整个数组就会变成有序的。

四、时间复杂度和空间复杂度

  • 时间复杂度:归并排序的时间复杂度在最好、最坏和平均情况下都是。这是因为每次划分数组的时间复杂度是(因为每次划分都是将数组规模减半),而每次合并操作的时间复杂度是(因为需要遍历合并的元素)。
  • 空间复杂度:归并排序的空间复杂度是,因为在合并过程中需要创建临时数组来存储子数组的元素。不过,可以通过一些优化手段,如在原数组上进行原地合并,来降低空间复杂度。

五、归并排序的应用场景

  • 归并排序适用于对大规模数据进行排序,特别是当数据量较大且对稳定性有要求时。例如,在数据库系统中对大量记录进行排序,或者在处理外部存储设备中的数据排序等场景。
  • 它也常用于一些需要高效排序并且能够接受一定空间开销的算法和数据结构中,如外部排序算法中的多路归并等。

你可能感兴趣的:(算法,排序算法)