数据结构与算法 11 归并排序

3.6 归并排序


归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。(归并排序 属于 高级排序的一种

  • 需求:
    排序前:{8,4,5,7,1,3,6,2}
    排序后:{1,2,3,4,5,6,7,8]

  • 排序原理︰

  1. 尽可能的将一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止。
  2. 将相邻的两个子组进行合并成一个有序的大组;
  3. 不断的重复步骤2,直到最终只有一个组为止。

在这里插入图片描述

  • 归并排序API设计:

在这里插入图片描述
归并原理

  1. 必须知道 排序是在哪里进行的,又是怎么进行的。排序是在合并两个子组的时候进行的! 而合并两个子组的时候又是怎么进行的呢? 只做两个子组每个对应元素的大小比较,比如A组1元素和B组1元素大小比较,如果A组1元素小,那么就把这个A组1元素放在 assist辅助数组的1元素位置!
  2. 辅助元素作用 是用来存储 A组和B组两个 子组进行 各个对应元素比较后,筛选的正确元素的位置 然后用 辅助数组存储进去,呈现的过程!
  3. 需要注意的是,我们要设定三个int变量 来代替所谓的指针思想。期初 p1指针指向A组lo位置p2指向B组mid+1元素i指针指向 辅助数组的 lo位置 元素当子组中的元素知道自己在哪个正确的位置后,该组的指针就应该往后移动一位,而另一个组的指针不要动。 这样才能继续进行下一次的比较和筛选。
  4. 在合并子组的过程中,我们可能会遇到一些特殊的情况,比如说 p1指针已经走到了mid 处,即 p1 的尽头,但是 辅助数组里面还缺少B组的元素,这个时候 我们知道 B组本身就是有序的呀!!!而且造成这个的原因就是B组的数据比较大,所以直接 遍历 从 p2 指针开始 B 组的所有元素,填入到 辅助数组里即可。
  5. 在合并子组的过程中,我们可能会遇到一些特殊的情况,比如说 p2指针已经走到了hi 处,即 p2 的尽头,但是 辅助数组里面还缺少A组的元素,这个时候 我们知道 B组本身就是有序的呀!!!而且造成这个的原因就是A组的数据比较大,所以直接 遍历 从 p1 指针开始 A 组的所有元素,填入到 辅助数组里即可。
  • 归并排序 代码
package com.muquanyu.algorithm.sort;

public class Merge {
    //归并所需要的辅助数组
    private static Comparable[] assist;

    /*
    比较 元素 v 是否 小于 元素 w
     */
    private static boolean less(Comparable v,Comparable w){
        return v.compareTo(w) < 0;
    }

    /*
    数组元素 i 和 j 交换位置
     */
    private static void exch(Comparable[] a,int i,int j)
    {
        Comparable temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    /*
    对数组 a 中的元素 进行排序
     */
    public static void sort(Comparable[] a)
    {
        //1.初始化 辅助数组 assist
        assist = new Comparable[a.length];
        //2.定义一个 lo 变量 和 hi 变量,分别记录数组中 最小的索引和最大的索引
        int lo = 0;
        int hi = a.length - 1;
        //3.调用 sort 的重载方法 完成 数据 a 中,从索引 lo 到 索引 hi 的元素排序
        sort(a,lo,hi);
    }

    /*
    对数组 a 中 从 lo 到 hi 的元素 进行排序
     */
    private static void sort(Comparable[] a,int lo,int hi)
    {
        //1.做安全性的校验
        if(hi <= lo)
        {
            return;
        }
        //2.对lo 到 hi 之间的数据 分为两个组
        int mid = lo + (hi - lo)/2;//5 + (9 - 5)/2 = 7;
        //3.分别对每一组的数据 进行排序
        sort(a,lo,mid);
        sort(a,mid+1,hi);
        //4.再把两个组中的数据进行 归并
        merge(a,lo,mid,hi);
    }

    /*
    对数组中 从 lo 到 mid 为一组,从 mid + 1 到 hi 为一组,对着两组数据进行归并
     */
    private static void merge(Comparable[] a,int lo,int mid,int hi)
    {
        int p1 = lo;
        int p2 = mid+1;
        int i = lo;

        while(p2 <= hi && p1 <= mid)
        {
            if(less(a[p1],a[p2]))
            {
                assist[i++] = a[p1++];
            }else{
                assist[i++] = a[p2++];
            }
        }
        //遍历 如果 p1 的指针没有 走完,那么 顺序移动 p1 指针,把对应的元素都放在辅助数组对应索引的后面即可
        while(p1 <= mid)
        {
            assist[i++] = a[p1++];
        }
        //同理 如果 p2 也没走完,那也要 顺序添进去 即可。
        while(p2 <= hi)
        {
            assist[i++] = a[p2++];
        }
        //把辅助数组拷贝到原数组中
        for(int index=lo;index <= hi;index++)
        {
            a[index] = assist[index];
        }
    }
}
  • 归并排序 算法测试
package com.muquanyu.algorithm.test;

import com.muquanyu.algorithm.sort.Merge;

import java.util.Arrays;

public class MergeTest {
    public static void main(String[] args) {
        Integer[] arr = {8,4,5,7,1,3,6,2};
        Merge.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

在这里插入图片描述

正常的归并排序,是必须要有 辅助数组 来 协助操作的。这样也方便 理解。而不要 直接 在 原数组里面 进行 操作!这样很容易 混乱还不说,反而效率 不一定 有 带有辅助数组的高,写起来也不是那么的快!!

  • 归并一词的意思是什么 ?

答:“归” 指的是 在 压入的那些函数,跳栈的时候(即归的时候),我们进行 “子组的合并”子组的合并 也被 简称为 “并” 这个字。而 由于 递归的时候,每次压栈我们都 将 子组不断的 二分!所以 最小的 A组和B组 各自仅仅只有 一个 元素,那么在进行 合并的时候,自然而然 就相当于 完成了排序的工作。 这是很奇妙的一种写法!!!在最小子组持续到最大子组的过程中 不断的进行合并,最后居然也完成了 “排序” 这个 重大的工程!


3.6.1 归并排序 时间复杂度分析

归并排序是分治思想的最典型的例子,上面的算法中,对a[lo…hi]进行排序,先将它分为a[lo…mid]和a[mid+1…hi]两部分,分别通过递归调用将他们单独排序,最后将有序的子数组归并为最终的排序结果。该递归的出口在于如果一个数组不能再被分为两个子数组,那么就会执行merge进行归并,在归并的时候判断元素的大小进行排序(而这里的排序是很奇妙的,它用一种最简单的方式实现了排序。也就是我们合并的过程,就实现了排序的过程)。
在这里插入图片描述
用树状图来描述归并,如果一个数组有8个元素,那么它将每次除以2找最小的子数组,共拆log8次,值为3,所以树共有3层,那么自顶向下第k层有2*k个子数组每个数组的长度为2(3-k)*,归并最多需要2(3-k)次比较。因此 每层的I较次数为 2k * 2(3-K) = 243, 那么 3层总共为 3 * 23

假设元素的个数为n,那么使用归并排序拆分的次数为 log2(n)所以共log2(n)层,那么使用log2(n)替换上面3*23中的 3 这个层数,最终得出的归并排序的时间复杂度为:== log2(n) * 2(log2n)=log2(n) * n ==,根据大O推导法则,忽略底数,最终归并排序的时间复杂度为 (nlogn);

nlogn 是 非常接近于 n 的 时间复杂度,所以我们说 nlogn 已经很快了!!

对于 高级排序算法,归并 算法 的代码难度和理解难度 是继 “希尔排序” 之后 第二个最简单的了。

  • 归并排序的缺点

需要申请额外的数组空间 还有额外的多个函数开辟的空间,还要申请额外的指针变量空间,导致空间复杂度提升,是典型的 "以空间换时间的算法"

这是我们第一个 接触的 空间换时间的 算法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值