归并排序(转)

合并排序,顾名思义,就是通过将两个有序的序列合并为一个大的有序的序列的方式来实现排序。合并排序是一种典型的分治算法:首先将序列分为两部分,然后对每一部分进行循环递归的排序,然后逐个将结果进行合并。

合并排序最大的优点是它的时间复杂度为O(nlgn),这个是我们之前的选择排序和插入排序所达不到的。他还是一种稳定性排序,也就是相等的元素在序列中的相对位置在排序前后不会发生变化。他的唯一缺点是,需要利用额外的N的空间来进行排序。

一 原理

合并排序依赖于合并操作,即将两个已经排序的序列合并成一个序列,具体的过程如下:

  1. 申请空间,使其大小为两个已经排序序列之和,然后将待排序数组复制到该数组中。
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较复制数组中两个指针所指向的元素,选择相对小的元素放入到原始待排序数组中,并移动指针到下一位置
  4. 重复步骤3直到某一指针达到序列尾
  5. 将另一序列剩下的所有元素直接复制到原始数组末尾

该过程实现如下,注释比较清楚:

private static void Merge(T[] array, int lo, int mid, int hi)
{
    int i = lo, j = mid + 1;
    //把元素拷贝到辅助数组中
    for (int k = lo; k <= hi; k++)
    {
        aux[k] = array[k];
    }
    //然后按照规则将数据从辅助数组中拷贝回原始的array中
    for (int k = lo; k <= hi; k++)
    {
        //如果左边元素没了, 直接将右边的剩余元素都合并到到原数组中
        if (i > mid)
        {
            array[k] = aux[j++];
        }//如果右边元素没有了,直接将所有左边剩余元素都合并到原数组中
        else if (j > hi)
        {
            array[k] = aux[i++];
        }//如果左边右边小,则将左边的元素拷贝到原数组中
        else if (aux[i].CompareTo(aux[j]) < 0)
        {
            array[k] = aux[i++];
        }
        else
        {
            array[k] = aux[j++];
        }
    }
}

下图是使用以上方法将EEGMR和ACERT这两个有序序列合并为一个大的序列的过程演示:

二 实现

合并排序有两种实现,一种是至上而下(Top-Down)合并,一种是至下而上 (Bottom-Up)合并,两者算法思想差不多,这里仅介绍至上而下的合并排序。

至上而下的合并是一种典型的分治算法(Divide-and-Conquer),如果两个序列已经排好序了,那么采用合并算法,将这两个序列合并为一个大的序列也就是对大的序列进行了排序。

首先我们将待排序的元素均分为左右两个序列,然后分别对其进去排序,然后对这个排好序的序列进行合并,代码如下:

public class MergeSort<T> where T : IComparable<T>
{
    private static T[] aux; // 用于排序的辅助数组
    public static void Sort(T[] array)
    {
        aux = new T[array.Length]; // 仅分配一次
        Sort(array, 0, array.Length - 1);
    }
    private static void Sort(T[] array, int lo, int hi)
    {
        if (lo >= hi) return; //如果下标大于上标,则返回
        int mid = lo + (hi - lo) / 2;//平分数组
        Sort(array, lo, mid);//循环对左侧元素排序
        Sort(array, mid + 1, hi);//循环对右侧元素排序
        Merge(array, lo, mid, hi);//对左右排好的序列进行合并
    }
    ...
}

以排序一个具有15个元素的数组为例,其调用堆栈为:

我们单独将Merge步骤拿出来,可以看到合并的过程如下:

三 图示及动画

如果以排序38,27,43,3,9,82,10为例,将合并排序画出来的话,可以看到如下图:

下图是合并排序的可视化效果图:

对6 5 3 1 8 7 24 进行合并排序的动画效果如下:

下图演示了合并排序在不同的情况下的效率:

四 分析

1. 合并排序的平均时间复杂度为O(nlgn)

证明:合并排序是目前我们遇到的第一个时间复杂度不为n2的时间复杂度为nlgn(这里lgn代表log2n)的排序算法,下面给出对合并排序的时间复杂度分析的证明:

假设D(N)为对整个序列进行合并排序所用的时间,那么一个合并排序又可以二分为两个D(N/2)进行排序,再加上与N相关的比较和计算中间数所用的时间。整个合并排序可以用如下递归式表示:

D(N)=2D(N/2)+N,N>1;

D(N)=0,N=1; (当N=1时,数组只有1个元素,已排好序,时间为0)

因为在分治算法中经常会用到递归式,所以在CLRS中有一章专门讲解递归式的求解和证明,使用主定理(master theorem)可以直接求解出该递归式的值,后面我会简单介绍。这里简单的列举两种证明该递归式时间复杂度为O(nlgn)的方法:

Prof1:处于方便性考虑,我们假设数组N为2的整数幂,这样根据递归式我们可以画出一棵树:

可以看到我们对数组N进行MergeSort的时候,是逐级划分的,这样就形成了一个满二叉树,树的每一及子节点都为N,树的深度即为层数lgN+1,满二叉树的深度的计算可以查阅相关资料,上图中最后一层子节点没有画出来。这样,这棵树有lgN+1层,每一层有N个节点,所以

D(N)=(lgN+1)N=NlgN+N=NlgN

Prof2:我们在为递归表达式求解的时候,还有一种常用的方法就是数学归纳法,

首先根据我们的递归表达式的初始值以及观察,我们猜想D(N)=NlgN.

  1. 当N=1 时,D(1)=0,满足初始条件。
  2. 为便于推导,假设N是2的整数次幂N=2k, 即D(2k)=2klg2k = k*2k
  3. 在N+1 的情况下D(N+1)=D(2k+1)=2k+1lg2k+1=(k+1) * 2k+1,所以假设成立,D(N)=NlgN.

2. 合并排序需要额外的长度为N的辅助空间来完成排序

如果对长度为N的序列进行排序需要<=clogN 的额外空间,认为就是就地排序(in place排序)也就是完成该排序操作需要较小的,固定数量的额外辅助内存空间。之前学习过的选择排序,插入排序,希尔排序都是原地排序。

但是在合并排序中,我们要创建一个大小为N的辅助排序数组来存放初始的数组或者存放合并好的数组,所以需要长度为N的额外辅助空间。当然也有前人已经将合并排序改造为了就地合并排序,但是算法的实现变得比较复杂。

需要额外N的空间来辅助排序是合并排序的最大缺点,如果在内存比较关心的环境中可能需要采用其他算法。

五 几点改进

对合并排序进行一些改进可以提高合并排序的效率。

1. 当划分到较小的子序列时,通常可以使用插入排序替代合并排序

对于较小的子序列(通常序列元素个数为7个左右),我们就可以采用插入排序直接进行排序而不用继续递归了),算法改造如下:

private const int CUTOFF = 7;//采用插入排序的阈值
private static void Sort(T[] array, int lo, int hi)
{
    if (lo >= hi) return; //如果下标大于上标,则返回
    if (hi <= lo + CUTOFF - 1) Sort<T>.SelectionSort(array, lo, hi);
    int mid = lo + (hi - lo) / 2;//平分数组
    Sort(array, lo, mid);//循环对左侧元素排序
    Sort(array, mid + 1, hi);//循环对右侧元素排序
    Merge(array, lo, mid, hi);//对左右排好的序列进行合并
}

2. 如果已经排好序了就不用合并了

当已排好序的左侧的序列的最大值<=右侧序列的最小值的时候,表示整个序列已经排好序了。

算法改动如下:

private static void Sort(T[] array, int lo, int hi)
{
    if (lo >= hi) return; //如果下标大于上标,则返回
    if (hi <= lo + CUTOFF - 1) Sort<T>.SelectionSort(array, lo, hi);
    int mid = lo + (hi - lo) / 2;//平分数组
    Sort(array, lo, mid);//循环对左侧元素排序
    Sort(array, mid + 1, hi);//循环对右侧元素排序
   if (array[mid].CompareTo(array[mid + 1]) <= 0) return;
    Merge(array, lo, mid, hi);//对左右排好的序列进行合并
}

3. 并行化

分治算法通常比较容易进行并行化,在浅谈并发与并行这篇文章中已经展示了如何对快速排序进行并行化(快速排序在下一篇文章中讲解),合并排序一样,因为我们均分的左右两侧的序列是独立的,所以可以进行并行,值得注意的是,并行化也有一个阈值,当序列长度小于某个阈值的时候,停止并行化能够提高效率,这些详细的讨论在浅谈并发与并行这篇文章中有详细的介绍了,这里不再赘述。

六 用途

合并排序和快速排序一样都是时间复杂度为nlgn的算法,但是和快速排序相比,合并排序是一种稳定性排序,也就是说排序关键字相等的两个元素在整个序列排序的前后,相对位置不会发生变化,这一特性使得合并排序是稳定性排序中效率最高的一个。在Java中对引用对象进行排序,Perl、C++、Python的稳定性排序的内部实现中,都是使用的合并排序。

七 结语

本文介绍了分治算法中比较典型的一个合并排序算法,这也是我们遇到的第一个时间复杂度为nlgn的排序算法,并简要对算法的复杂度进行的分析,希望本文对您理解合并排序有所帮助,下文将介绍快速排序算法。

转自:http://www.cnblogs.com/yangecnu/p/Introduce-Merge-Sort.html

时间: 2024-12-28 19:16:46

归并排序(转)的相关文章

算法 排序NB二人组 堆排序 归并排序

参考博客:基于python的七种经典排序算法     常用排序算法总结(一) 序前传 - 树与二叉树 树是一种很常见的非线性的数据结构,称为树形结构,简称树.所谓数据结构就是一组数据的集合连同它们的储存关系和对它们的操作方法.树形结构就像自然界的一颗树的构造一样,有一个根和若干个树枝和树叶.根或主干是第一层的,从主干长出的分枝是第二层的,一层一层直到最后,末端的没有分支的结点叫做叶子,所以树形结构是一个层次结构.在<数据结构>中,则用人类的血统关系来命名,一个结点的分枝叫做该结点的"

codeforces 414C C. Mashmokh and Reverse Operation(归并排序求逆序对)

题目链接: C. Mashmokh and Reverse Operation time limit per test 4 seconds memory limit per test 512 megabytes input standard input output standard output Mashmokh's boss, Bimokh, didn't like Mashmokh. So he fired him. Mashmokh decided to go to university

归并排序(Merge Sort)

更详细的请看这篇博文:http://www.cnblogs.com/jingmoxukong/p/4308823.html 我只做个人学习和补充 基于分治法的排序,比较简单.个人感觉其核心是数组左右拆分之后类似队列的比较.类似两两合并之类的算法都可以参考 分为3个步骤,拆开为树结构遍历->向上合并 特别做了一张gif来更好的理解归并排序 左边集合比较通过就左边索引前进,右边集合比较通过则右边索引前进 比较结果放入结果数组中 最后如果多出一个就把这一个直接加入结果数组 为了易于学习,代码直接使用了

两路归并排序

链表两路归并 #include<iostream> #include<assert.h> using namespace std; struct node { int val; node * next; node(int v) { val=v; next=NULL; } }; node * merge(node* list1 , node * list2) { assert(list1!=NULL&&list2!=NULL);//括号中是希望出现的正确的情况  no

算法基础:分治模式,归并排序ΘΘΘΘΘΘ知识小结

1.分治模式在每层递归时都有三个步骤:分解,解决,合并 2.归并排序算法完全遵循分治模式: 分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列 解决:使用归并排序递归的排序两个子序列 合并:合并两个已排序的子序列以产生已排序的答案 3.分析分治算法所需要的时间计算: 假设T(n)是规模为n的一个问题的运行时间,若问题足够小,如对某个常量c,n≦c,则直接求解需要常量时将,我们将其写作Θ(1).假设吧原问题分解成a个子问题,每个子问题的规模是原问题的1/b(对归并排序,a和b都为2,然

归并排序

基本思想: 1)首先需要一个临时空间temp用来存放合并后的元素,长度为排列好的元素总个数. 2)取两个指针,分别为左右部分数组的头元素. 3)比较指针指向元素,将较小(较大)的那个放入temp(注意相等时不做元素位置交换),并将两指针向后移动一个step. 4)循环步骤3直到其中一段数组没有剩余元素(其中一段被遍历完). 5)将还有元素剩余的另外一半数组元素直接放入temp尾部 代码实现: public static void merge(int[] list, int left, int m

基本排序算法(冒泡排序 选择排序 插入排序 快速排序 归并排序 基数排序 希尔排序)

冒泡排序 public static void bubbleSort(int[] arr){ int lgn = arr.length; for (int i = 0; i < lgn - 1; i++) { for (int j = 0; j < lgn - 1 - i; j++) { if(arr[j] > arr[j + 1]){ int temp = arr[j + 1]; arr[j + 1] = arr[j]; arr[j] = temp; } } } } 选择排序 publ

利用Python实现归并排序

利用python进行归并排序,摘抄自http://blog.csdn.net/minxihou/article/details/51821052 "代码真的不是一气呵成的,而且也不是想当然写出来的.可能需要反复断点中断来查看是否有逻辑错误.在理解了问题的基础下我们需要先把大体的代码框架最好先写出来,特别是主要的逻辑判断语句.但是不需要太care我循环体或者判断里面语句怎么实现,当你把这一步做到的时候能避免很多不必要的错误发生." 1 import random 2 3 def Conf

面试必考三大排序算法C++代码(快速排序、归并排序、堆排序)

前两个为C++风格代码,后一个为C风格代码,除了输入输出,其它无差别,但C输入输出要比C++更快. 快速排序 1 #include <iostream> 2 using namespace std; 3 4 void swap(int num[], int i, int j) 5 { 6 int temp = num[i]; 7 num[i] = num[j]; 8 num[j] = temp; 9 } 10 11 int partition(int num[], int left, int

python实现归并排序,归并排序的详细分析。

学习归并排序的过程是十分痛苦的.它并不常用,看起来时间复杂度好像是几种排序中最低的,比快排的时间复杂度还要低,但是它的执行速度不是最快的.很多朋友不理解时间复杂度低为什么运行速度不一定快,这个不清楚的伙伴可以看下我之前发表的文章http://www.cnblogs.com/Lin-Yi/p/7301535.html看完之后也许你会对时间复杂度有一个新的认识. 我谈的观点往往不是官方的定义,我希望能帮助更多基础薄弱的同学读懂思想~ 归并排序: 先分开再合并,分开成单个元素,合并的时候按照正确顺序合