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

😎在之前文章中我们学习过了多种排序算法,现在我们来学习排序算法中的归并排序

👍欢迎关注我之前的作品👉【选择排序】

                                         👉【插入排序】

                                         👉【交换排序】


目录

💯引言

💯归并排序的原理

💯归并排序的实现步骤

⭐分解过程

⭐合并过程

💯时间复杂度分析

⭐最好情况

⭐最坏情况

⭐平均情况

💯空间复杂度分析

💯结论


💯引言

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

😀本文将深入探讨归并排序的原理、实现步骤、时间复杂度与空间复杂度分析,以及它在实际应用中的优势和局限性。


💯归并排序的原理

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

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

🌠1.分治策略

  • 将问题分解为规模更小的子问题,分别解决这些子问题,然后将子问题的解合并起来得到原问题的解。在归并排序中,就是不断地将数组分成两半,对两半分别进行排序,然后再合并。

🌠2.合并有序子数组

  • 已知两个有序子数组,将它们合并成一个更大的有序数组。这个过程通过比较两个子数组的元素大小,依次将较小的元素放入合并后的数组中,直到其中一个子数组的元素全部被放入,然后将另一个子数组剩余的元素依次放入合并后的数组中。 


💯归并排序的实现步骤

分解过程

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

👀图解如下: 

合并过程

  1. 当子数组的长度为 1 或 0 时,开始进行合并操作。以两个长度为 1 的子数组[8][4]为例,比较它们的元素大小,将较小的元素4放入一个新的临时数组中,然后比较8和下一个元素(此时[4]子数组已没有下一个元素),将8放入临时数组。这样就得到了一个有序的子数组[4, 8]
  2. 对于长度大于 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);
    }
}


💯时间复杂度分析

最好情况

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

最坏情况

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

平均情况

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


💯空间复杂度分析

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

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


💯结论

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


 💝💝💝感谢你看到最后,点个赞再走吧!💝💝💝我的主页👉【A Charmer】

😇亲爱的读者,您在学习和使用归并排序算法过程中有哪些心得体会呢?欢迎参与投票:  

评论 48
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值