问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

深度解析归并排序(Merge Sort)

创作时间:
作者:
@小白创作中心

深度解析归并排序(Merge Sort)

引用
CSDN
1.
https://blog.csdn.net/2301_82213854/article/details/142951162

在计算机科学的众多排序算法中,归并排序以其高效性和稳定性而备受关注。它是一种基于分治策略的排序算法,能够在相对较短的时间内对大规模数据进行排序,并且在处理过程中不会改变相同元素之间的相对顺序。

归并排序的原理

归并排序的核心思想是将一个数组分成两个子数组,分别对这两个子数组进行排序,然后再将排好序的子数组合并成一个有序的数组。这个过程可以递归地进行,直到子数组的长度为 1 或 0,此时子数组本身就是有序的。

具体来说,归并排序的原理基于以下两点:

  1. 分治策略
  • 将问题分解为规模更小的子问题,分别解决这些子问题,然后将子问题的解合并起来得到原问题的解。在归并排序中,就是不断地将数组分成两半,对两半分别进行排序,然后再合并。
  1. 合并有序子数组
  • 已知两个有序子数组,将它们合并成一个更大的有序数组。这个过程通过比较两个子数组的元素大小,依次将较小的元素放入合并后的数组中,直到其中一个子数组的元素全部被放入,然后将另一个子数组剩余的元素依次放入合并后的数组中。

归并排序的实现步骤

分解过程

首先,将给定的数组不断地分成两半,直到每个子数组的长度为 1 或 0。这个过程可以通过递归实现。例如,对于数组
[8, 4, 5, 7, 1, 3, 6, 2]
,第一次分解得到
[8, 4, 5, 7]

[1, 3, 6, 2]
,然后继续对这两个子数组进行分解,直到得到单个元素的子数组。

合并过程

当子数组的长度为 1 或 0 时,开始进行合并操作。以两个长度为 1 的子数组
[8]

[4]
为例,比较它们的元素大小,将较小的元素
4
放入一个新的临时数组中,然后比较
8
和下一个元素(此时
[4]
子数组已没有下一个元素),将
8
放入临时数组。这样就得到了一个有序的子数组
[4, 8]

对于长度大于 1 的子数组,例如
[4, 8, 5, 7]

[1, 3, 6, 2]
,分别从两个子数组的开头开始比较元素大小,将较小的元素放入临时数组。依次类推,直到其中一个子数组的元素全部被处理完,然后将另一个子数组剩余的元素全部放入临时数组。最后,将临时数组中的元素复制回原数组对应的位置。

😊动图展示 :

✍以下是用 C++ 实现归并排序的代码:

// 归并函数,将两个有序的子数组合并为一个有序数组
void merge(int arr[], int left, int mid, int right) {
    // n1 是左子数组的长度
    int n1 = mid - left + 1;
    // n2 是右子数组的长度
    int n2 = right - mid;
    // 创建临时数组 L 和 R,分别存储左子数组和右子数组的元素
    int L[n1], R[n2];
    // 将原数组中左子数组的元素复制到临时数组 L
    for (int i = 0; i < n1; i++)
        L[i] = arr[left + i];
    // 将原数组中右子数组的元素复制到临时数组 R
    for (int j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];
    // i 用于遍历左子数组 L,j 用于遍历右子数组 R,k 用于遍历合并后的数组(原数组的指定区间)
    int i = 0, j = 0, k = left;
    // 当左子数组和右子数组都还有元素未被处理时
    while (i < n1 && j < n2) {
        // 如果左子数组当前元素小于等于右子数组当前元素
        if (L[i] <= R[j]) {
            // 将左子数组当前元素放入原数组的指定位置,并移动左子数组的指针 i
            arr[k] = L[i];
            i++;
        } else {
            // 如果右子数组当前元素小于左子数组当前元素,将右子数组当前元素放入原数组的指定位置,并移动右子数组的指针 j
            arr[k] = R[j];
            j++;
        }
        // 无论放入的是左子数组还是右子数组的元素,都要移动合并后数组的指针 k
        k++;
    }
    // 如果左子数组还有剩余元素,将其全部复制到原数组的指定位置
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    // 如果右子数组还有剩余元素,将其全部复制到原数组的指定位置
    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);
    }
}  

时间复杂度分析

最好情况

在最好情况下,即数组已经是有序的,归并排序的时间复杂度仍然是
。这是因为无论数组是否有序,归并排序都需要进行对数级别的分解和合并操作。每次分解都将数组分成大致相等的两部分,直到子数组的长度为 1,这个过程需要
次。而每次合并操作需要线性时间
(因为需要遍历所有元素),所以总的时间复杂度为

最坏情况

在最坏情况下,例如数组是逆序的,归并排序的时间复杂度同样是
。分解过程的时间复杂度是
,与最好情况相同。合并过程中,每次比较都需要进行次(因为两个子数组的长度之和为
),总共需要进行
次合并,所以合并过程的时间复杂度为
。因此,归并排序在最坏情况下的时间复杂度也是

平均情况

归并排序的平均时间复杂度也是
。在平均情况下,每次分解和合并的操作次数与最好情况和最坏情况大致相同。可以通过数学推导来证明其平均时间复杂度,但这超出了本文的范围。总之,归并排序在各种情况下都能保持相对稳定的时间复杂度,这是它的一个重要优点。

空间复杂度分析

归并排序的空间复杂度为
。这是因为在合并过程中,需要使用一个临时数组来存储合并后的结果。临时数组的长度最大为
(当合并整个数组时),所以空间复杂度为

需要注意的是,归并排序可以通过一些优化措施来减少空间复杂度,例如在原地进行合并(即不使用额外的临时数组),但这样会增加算法的复杂性和实现难度。

结论

归并排序是一种高效、稳定的排序算法,基于分治策略和合并有序子数组的原理,能够在
的时间复杂度内对数据进行排序,并且在各种情况下时间复杂度稳定。然而,归并排序的空间复杂度较高,数据移动较多,在内存有限和对数据移动操作敏感的场景中可能需要谨慎考虑。在实际应用中,我们需要根据具体的需求和数据特点来选择合适的排序算法,而归并排序无疑是排序算法家族中一个重要且值得深入理解的成员。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号