《算法导论》——十分钟了解归并排序!

注:本文为《算法导论》中排序相关内容的笔记。对此感兴趣的读者还望支持原作者。

基本概念

与插入排序相同,归并排序也是一种常见的排序算法。归并排序是建立在归并操作上的一种高效且稳定的排序算法。该算法是采用分治法将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

算法思想

归并算法的核心思想即分治法。所谓分治法,就是将问题分而治之。它将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来求解出原问题的解。
分治法在每层递归中都有三个步骤:

  • 分解原问题为若干问题,这些子问题是原问题的规模较小的类似问题;
  • 求解这些子问题,各自递归地求解各子问题。然而,若子问题的规模足够小,则直接求解;
  • 合并这些子问题的解以建立原问题的解。

而具体到归并排序,其完全遵循此模式:

  • 分解待排序的n个元素的序列成各具 n / 2 n/2 n/2个元素的两个子序列。(若无法对2整除,则分别上下取整);
  • 使用归并排序递归地排序求解两个子序列;
  • 合并两个已排序的子序列以产生已排序的答案。

其实,说白了归并排序首先将原序列递归地分解为足够小的子序列,一般为1,然后比较两个相邻的子序列的元素大小,每次取较大值或者较小值,直至两个子序列的元素都被取完,合并成一个序列,最后,不断地重复此过程,直至原序列有序。

代码示例

千言万语,不如代码一段,废话少说,直接上代码。此代码段给出了归并排序的Java实现版本。

/**
 * 归并排序的简单实现,非降序(整型数组)
 * @author 爱学习的程序员
 * @version V1.0
 * */
import java.lang.Integer;
import java.util.Random;

public class MergeSort{
    // 排序
    public static void merge(int[] arr, int n, int mid, int m){
        // 划分后的左数组
        int[] left = new int[mid - n + 2];
        // 划分后的右数组
        int[] right = new int[m - mid + 1];
        // 为划分数组设置哨兵
        left[left.length - 1] = right[right.length - 1] = Integer.MAX_VALUE;
        // 复制
        int i = 0;
        for(; i < left.length - 1; i++)
            left[i] = arr[n+i];
        for(i = 0; i < right.length - 1; i++)
            right[i] = arr[mid+1+i];
        // 归并排序(左右两数组比较,取较小的)
        int j = 0, k = 0;
        for(i = n; i <= m; i++){
            if(left[j] < right[k]){
                arr[i] = left[j];
                j++;
            }
            else{
                arr[i] = right[k];
                k++;
            }
        }
    }
    
    // 归并(递归)
    public static void split(int[]arr, int n, int m){
        // 数组错误
        if(arr.length <= 0) return;
        // 无需排序
        if(arr.length == 1) return;
        // 划分结束
        if(n >= m) return;
        int mid = (n + m) / 2;
        split(arr, n, mid);
        split(arr, mid+1, m);
        merge(arr, n, mid, m);
    }

    public static void main(String[] args){
        //测试数组生成
        int[] arr = new int[10];
        Random rand = new Random();
        System.out.println("测试数组:");
        int i = 0;
        int length = arr.length;
        for(i = 0; i < arr.length; i++){
            arr[i] = rand.nextInt(100);
            System.out.print(arr[i]+" ");
        }
        // 归并排序
        split(arr, 0, length-1);
        // 输出排序结果
        System.out.println("\n"+"排序结果");
        for(i = 0; i < arr.length; i++)
            System.out.print(arr[i]+" ");
    }
}

值得一提的是,本程序在合并两个子序列时使用了设置哨兵的技巧。哨兵为一特殊值,用于简化代码。这里,使用整型的最大值作为哨兵值,结果每当一个序列的元素值为哨兵值时,该子序列的元素全部取完。

算法分析

从示例代码中不难看出,归并排序的运行时间为分治法的三个基本步骤。假设把原问题分解为 a a a个子问题,每个子问题的规模是原问题的 1 / b 1/b 1/b(对于归并排序, a = b = 2 a=b=2 a=b=2)。为了求解一个规模为 n / b n/b n/b的子问题,需要 T ( n / b ) T(n/b) T(n/b)的时间,所以需要 a ( n / b ) a(n/b) a(n/b)的时间来求解 a a a个子问题。如果分解成子问题需要时间 D ( n ) D(n) D(n),合并子问题的解成原问题的解需要时间 C ( n ) C(n) C(n),那么可有 T ( n ) = a T ( n / b ) + D ( n ) + C ( n ) T(n)=aT(n/b)+D(n)+C(n) T(n)=aT(n/b)+D(n)+C(n)。而由代码可得,分解步骤仅仅计算子数组的中间位置,需要常量时间,因此 D ( n ) = O ( 1 ) D(n)=O(1) D(n)=O(1)。而合并子问题的解成原问题的解的过程需要 O ( n ) O(n) O(n)的时间,又有 a = n = 2 a=n=2 a=n=2,所以最坏情况下,原公式可改写为 T ( n ) = 2 T ( n / 2 ) + n T(n)=2T(n/2)+n T(n)=2T(n/2)+n,使用归纳法可求得 T ( n ) = O ( n lg ⁡ n ) T(n)=O(n \lg n) T(n)=O(nlgn)

算法总结

与插入排序相比,归并排序同样简单,而且它高效,排序结果稳定,广泛应用于实际生活中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值