归并排序算法(基于Java实现)


title: 归并排序算法(基于Java实现)
tags: 归并算法


归并排序算法的原理与代码实现:

一、归并排序算法的原理

其核心思想其实蛮简单的,就是如果我们要排序一个数组,我们先数组从中间分成前后两部分,然后对前后两部分分别排序,然后再将排好序的两部分合并在一起,这样整个数组就都有序了。总而言之,归并排序使用的就是分治思想,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。而分治算法一般都是用递归来实现的,分而治之是一种解决问题的处理思想,递归则是一种编程技巧。

可以先来看一段伪代码:

// 归并排序算法, A是数组,n表示数组大小
merge_sort(A, n) {
  merge_sort_c(A, 0, n-1)
}

// 递归调用函数
merge_sort_c(A, p, r) {
  // 递归终止条件
  if p >= r  then return

  // 取p到r之间的中间位置q
  q = (p+r) / 2
  // 分治递归
  merge_sort_c(A, p, q)
  merge_sort_c(A, q+1, r)
  // 将A[p...q]和A[q+1...r]合并为A[p...r]
  merge(A[p...r], A[p...q], A[q+1...r])
}

你可能已经发现了,merge(A[p…r], A[p…q], A[q+1…r]) 这个函数的作用就是,将已经有序的 A[p…q]和 A[q+1…r]合并成一个有序的数组,并且放入 A[p…r]。那这个过程具体该如何做呢?

如图所示,我们申请一个临时数组 tmp,大小与 A[p…r]相同。我们用两个游标 i 和 j,分别指向 A[p…q]和 A[q+1…r]的第一个元素。比较这两个元素 A[i]和 A[j],如果 A[i]<=A[j],我们就把 A[i]放入到临时数组 tmp,并且 i 后移一位,否则将 A[j]放入到数组 tmp,j 后移一位。

继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并之后的结果了。最后再把临时数组 tmp 中的数据拷贝到原数组 A[p…r]中。

二、归并排序算法的代码实现
package com.company;

import java.util.Arrays;

public class MergeSort2 {

    public static void mergeSort(int[] a, int p, int r){
        if(p < r){
            int mid = p + (r - p)/2;
            mergeSort(a, p, mid);//左排序
            mergeSort(a,mid+1, r);//右排序
            merge(a, p, mid, r);//左右合并
        }
    }

    public static void merge(int[] a, int p, int m, int r){

        int[] help = new int[r - p + 1];
        int i = 0;
        int p1 = p;
        int p2 = m + 1;
        //这个地方代码写法很巧妙!!!
        while(p1 <= m && p2 <= r){
            if(a[p1] < a[p2]){
                help[i++] = a[p1++];
            }else{
                help[i++] = a[p2++];
            }
        }

        //将剩余的数据拷贝至help数组
        while(p1 <= m){
            help[i++] = a[p1++];
        }

        while(p2 <= r){
            help[i++] = a[p2++];
        }

        //再将help中的数组拷贝回数组a中
        for(i = 0; i<help.length; i++){
            a[p++] = help[i];
        }
    }

    public static void main(String[] args) {
        int[] a = {10, 9, 7, 4, 3, 2, 1, 8};

        System.out.println("之前的排序:");
        System.out.println(Arrays.toString(a));

        mergeSort(a, 0, 7);
        System.out.println("之后的排序:");
        System.out.println(Arrays.toString(a));
    }
}

输出的结果为:

之前的排序:
[10, 9, 7, 4, 3, 2, 1, 8]
之后的排序:
[1, 2, 3, 4, 7, 8, 9, 10]
三、代码优化

可以利用“哨兵”简化编程的技巧,如果上述的merge()合并函数借助“哨兵”,代码就会简洁很多。

哨兵方法:

  1. 先将要合并的两个放到tmpLeft和tmpRight数组,其中每个数组都多出一个位置放哨兵 ;

  2. tmpLeft[leftSize] = int.MaxValue; 
    tmpRight[rightSize] = int.MaxValue; 
    
  3. 比较两个tmp数组,哪个小就放到原数组,使用哨兵不用再判断是否有剩下的有序数组。

     for (k=left,i=0,j=0; k<=right; k++){ 
       if(tmpLeft[i] < tmpRight[j]){ 
         arr[k] = tmpLeft[i]; i++; 
       }else{
         arr[k] = tmpRight[j]; j++; 
       }
     }
    

    完整的merge函数代码如下:(只需要将该段代码与上述的merge函数代码段替换即可,就能够实现代码的优化。)

    private static void mergeBySentry(int[] arr, int p, int q, int r){
      int[] leftArr = new int[q - p +2];
      int[] rightArr = new int[r - q + 1];
    
      for(int i=0; i<=q - p; i++){
        leftArr[i] = arr[p + i];
      }
    
      //第一个数组添加哨兵(最大值)
      leftArr[q - p + 1] = Integer.MAX_VALUE;
    
      for(int i = 0; i<r-q; i++){
        rightArr[i] = arr[q+1+i];
      }
    
      //第二个数组添加哨兵(最大值)
      rightArr[r-q] = Integer.MAX_VALUE;
    
      int i = 0;
      int j = 0;
      int k = 0;
      while(k<=r){
        //当左边数组达到哨兵值时,i不再增加,直到右边数组读取完剩余值,同理右边数组也一样
        if(leftArr[i] <= rightArr[j]){
          arr[k++] = leftArr[i++];
        }else{
          arr[k++] = rightArr[j++];
        }
      }
    }
    
四、归并排序的性能分析

结合我前面画的那张图和归并排序的伪代码,你应该能发现,归并排序稳不稳定关键要看 merge() 函数,也就是两个有序子数组合并成一个有序数组的那部分代码。在合并的过程中,如果 A[p…q]和 A[q+1…r]之间有值相同的元素,那我们可以像伪代码中那样,先把 A[p…q]中的元素放入 tmp 数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法

从我们的原理分析和伪代码可以看出,归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)

归并排序并没有像快排那样,应用广泛,这是为什么呢? 因为它有一个致命的“弱点”,那就是归并排序不是原地排序算法

实际上,递归代码的空间复杂度并不能像时间复杂度那样累加。刚刚我们忘记了最重要的一点,那就是,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值