class 021 归并排序

这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。

这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
https://space.bilibili.com/8888480?spm_id_from=333.999.0.0

在这里插入图片描述

1. 原理与递归实现

1.1 逻辑实现

有一个数组:arr[] = [6, 4, 2, 3, 9, 4]. 通过归并排序的方式将其变得有序.

  1. 因为我们这里利用的是 递归 实现, 所以要先设置子过程 (分解), 还是分一半, 这个数组的下标是:
    0, 1, 2, 3, 4, 5, 最开始是:f(0, 5) 所以对应的, 要将左边分为:f(0, 2), 右边是:f(3, 5), 然后一直往下划分, 直到有序 (就是 l == r) 的时候, 因为这个时候只剩下 1 个数字了, 肯定是有序的.
  2. 然后开始进行 merge 过程, merge 过程就是将返回的左右两边的两个(或者以上的)值开辟一个数组进行单独排序, 然后再将排好序的数组刷回原数组中.
  3. 最后一直重复这个过程, 直到排好序.

1.2 代码实例

这里的代码是单拿出来的, 全局静态变量什么的都在左程云老师github 上, 直接下载就行了.

1.2.1 几个需要注意的点

  1. merge 为什么要传递 m 这个中间位置, 因为我们需要注意排序对应的位置, 我们进行传递的过程中, 已经认为是左侧和右侧都是有序的, merge 过程就是一个将左侧和右侧一起变得有序的过程. 比如:左侧是 6, 右侧是 4, 这两个都是有序的, 所以 merge 过程需要将这两个数字变得有序:-> [4, 6].

  2. 下面代码的后面两个 while()循环 只会执行其中一个, 因为一定会有一侧的数组中有数字没有进入到 help 数组中, 举一个极端一点的例子:左侧数组是 1, 2, 3 , 右侧数组是 5, 6, 7, 这样的话用第一个 while()循环 之后, 右侧数组中就没有一个数字进入到 help数组 中, 因为此时左侧数组已经越界了, 不会执行第一个 while()循环 了, 所以下面两个就是将没有进入到 help 数组中的数字放入的.

  3. 传递过去的是整个 arr数组 只是将 arr数组 的一小部分进行了排序, 然后继续将排好序的数字刷回了原来的数组.

public static void mergeSort1(int l, int r) {  
    if (l == r) {  
       return;        // 因为这里肯定是有序的, 所以什么都不用做, 直接返回就行了.
    }  
    int m = (l + r) / 2;  
    mergeSort1(l, m);      // 使用递归过程进行分析理解(最好是画递归决策图).
    mergeSort1(m + 1, r);  
    merge(l, m, r);        // merge过程是将到底返回之后的几个数字进行排序.(传递的时候需要将l, r传递过去, 因为我们需要注意排序对应的位置).
}

// merge过程.
public static void merge(int l, int m, int r) {  
    int i = l;  // 这里需要用i来表示help位置的索引值.需要用i表示help位置,要将需要排序的值放到对应位置上去.
    int a = l;  
    int b = m + 1;  
    while (a <= m && b <= r) {  // 在左侧和右侧的数组中, 哪一个小就将哪一个数字拷贝到help数组中去
       help[i++] = arr[a] <= arr[b] ? arr[a++] : arr[b++];  
    }  
    // 左侧指针、右侧指针,必有一个越界、另一个不越界  (所以只会执行一个while)
    while (a <= m) {  
       help[i++] = arr[a++];  
    }                           // 若是左侧数组没耗尽, 就将左侧数组放入, 若是右侧数组没耗尽, 就将右侧数组放入,
    while (b <= r) {  
       help[i++] = arr[b++];  
    }  
    for (i = l; i <= r; i++) {  
       arr[i] = help[i];          // 最后将已经排好序的数组放到原来的数组中.
    }  
}

1.3 复杂度分析

1.3.1 merge 过程

将两边的数组放入 help数组 中进行排序, 所以肯定会将两边数组的值过一遍, 假设有 n 个值, 那就将所有的值放入, 然后会将排好序的值放入原来的数组中, 所以还是 n 个值过一遍, 那对应的时间复杂度是:O(n) + O(n) -> 2 * O(n) -> O(n). (常数就直接忽略了, 不用管).

1.3.2 递归过程

利用上一节课讲过的 master公式:分析这个递归过程是:左边开一半, 右边开一半, 所以一半一半地分,
master公式前半部分是:T(N) = T(N/2) + T(N/2) -> T(N) = 2 * T(N/2). 所以:a = 2, b = 2.

我们刚刚也分析了 merge 过程, 时间复杂度是:O(n)

所以:T(n) = 2 * T(n/2) + O(n), a = 2, b = 2, c = 1 , 根据上一节课的 master公式:归并排序的时间复杂度是:O(n * logn).

1.3.3 空间复杂度分析

因为开了一个 help数组, 所以时间复杂度是:O(n).

2. 归并排序非递归实现

2.1 逻辑实现

有一个数组:arr[] = [6, 2, 3, 3, 4, 6, 9, 3, 1]. 将这个数组用非递归的归并排序的方式进行排序.

  1. 先设置一个步长 step = 1, 然后将步长 step * 2 直到步长 step >= arr.length. 例子中的 arr.length == 9, 所以 step == 16 才行.
  2. 然后在数组中找对应的步长, 比如:最开始 step = 1, 先找到 6(左边) 和 2(右边), 将 6, 4 进行 merge 过程进行排序, 然后继续向后寻找 3, 3 进行 merge过程进行排序, 最后到 1(左边) 位置, 没有 右边 位置了, 所以这个就不用管了, 就放着就行. (此时 arr数组:[2, 6, 3, 3, 4, 6, 3, 9, 1])
  3. 然后继续到第二步长:step = 2, 找到 2, 6(左边) 和 3, 3(右边) , 然后进行 merge过程 排序, 最后的 arr数组:2, 3, 3, 6, 3, 4, 6, 9, 1.
  4. 然后继续执行上述过程, 执行到 step == 8 的时候, 左侧是:2, 3, 3, 3, 4, 6, 6, 9 , 右侧的数字是:1, 依然可以进行 merge过程, 所以最后就直接排好序了.
  5. 当然每一次 merge过程 之后都是要将辅助数组中的数字刷回原来数组的. (这个过程已经包括在 merge)
  6. merge 过程哪怕是只有一侧有数字, 另一侧没有数字也能进行 merge, 不影响.

2.2 代码实例

merge 过程是一样的, 不用管, 还是直接去左程云老师github 下载就行了.

2.2.1 几个注意事项

  1. m = l + step - 1 // 这个是左侧的最后一个位置.
  2. m + 1 >= n 这个的意义是说明右侧已经没有可以拿到的值了, 所以可以跳出 while循环 到增加步长的下一步了. (还需要说明的是:这个不代表 for循环 结束了, 有可能是 step == 1, 此时数组长度是奇数, 所以没有拿到右侧的数字, 跳出循环了).
  3. Math.min(l + (step << 1) - 1, n - 1), 这个的意义和上面差不多, 有可能我此时想要拿到最右侧的边界:但是有可能数组的最右侧没有 l + (step << 1) - 1 这么大, 所以直接进行从 arr.length - 1l + (step << 1) - 1 选择一个, 哪一个小用哪一个. (毕竟是不同的步长).
public static void mergeSort2() {  
    // 一共发生O(logn)次                      // n是arr.length. 
    for (int l, m, r, step = 1; step < n; step <<= 1) {   // 这里写成:step *= 2 也行.
       // 内部分组merge,时间复杂度O(n)  
       l = 0;  
       while (l < n) {  
          m = l + step - 1;     // 这个是左侧的最后一个位置.
          if (m + 1 >= n) {  
             break;  
          }  
          r = Math.min(l + (step << 1) - 1, n - 1);  
          merge(l, m, r);  
          l = r + 1;           // 这一组的l~r已经排好序了, 该到下一组了. 
       }  
    }  
}

2.3 复杂度分析

2.3.1 时间复杂度分析

假设有数组的长度为:1000, 但是 for循环的 步长 step 还是从 1 开始的, 直到超过 1000, 所以这个的时间复杂度是 O(log(n)).

merge 过程只不过是将所有的数字分组进行排序, 最后所有的分组之后的数字相加还是等于:n, 所以时间复杂度是:O(n).

这样每进行一次 for循环, 然后执行 merge 所以最后的时间复杂度是:O(n * log(n)).

3. 总结和提醒

我们都知道:时间复杂度:O(n * log (n)) 肯定是要比:O(n^2) 快很多很多的,
举一个例子: 假设数组长度是:10^6 , 所以 O(n^2) 就是 10^12 时间才能排好序, 而 O(n * log (n)) 只需要 20 * 10^6 == 2 * 10^7 就行了, 肯定是要比 O(n^2) 快很多很多的.

原理解释:比较行为没有被浪费. 比如 冒泡排序 中, 全部都比较一次, 然后只是搞定了一个数字, 然后继续重新开始, 再解决一个数字, 这样存在大量的比较行为, 而且有很多都浪费了.

但是在归并排序中, 每一组比较之后都进行了重新排序, 没有浪费比较行为.

归并排序是一种常见的排序算法,用于对一个向量进行排序。它的主要思想是将向量递归地分成两个部分,然后将这两个部分分别排序,最后将排序好的两个部分合并成一个有序的向量。这个过程会一直递归下去,直到向量中只剩下一个元素或者没有元素为止。 具体实现中,归并排序首先通过递归将向量均分成两个部分。然后对这两个部分分别进行归并排序,直到每个部分只剩下一个或两个元素。接着,将这两个部分合并起来,通过比较元素的大小,将较小的元素放在前面,形成一个新的有序向量。最后,将所有的部分合并起来,得到最终的有序向量。 在代码实现中,归并排序的核心函数是mergeSort,它通过递归地将向量分成两个部分,然后再调用merge函数将这两个部分合并起来。而merge函数则是将两个有序的部分合并成一个有序的向量。 总结来说,归并排序就是将一个向量分成两个部分,然后对这两个部分分别排序,最后将排序好的两个部分合并成一个有序的向量。这个过程会一直递归下去,直到向量中只剩下一个元素或者没有元素为止。归并排序是一种时间复杂度为O(nlogn)的排序算法,适用于各种规模的数据集合。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [数据结构Vector之选择排序、冒泡排序、归并排序](https://blog.csdn.net/u011926277/article/details/49341317)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值