分治策略之归并排序

    合并排序也叫归并排序,它的主要思想是分治法,把待排序序列分为若干有序子序列,然后将两个或两个以上的有序子序列进行合并,得到一个新的完整的有序序列。所以首先得先对子序列进行排序,得到有序子序列,然后再使序列段之间有序。

    二路归并排序主旨是“分解”与“归并”

    分解:  

    1、设序列长度为L,将序列分为两个长度为(L/2)的子序列

    2、继续递归地对两个子序列分割,直至不能再继续分割,此时每个子序列只有一个元素

    归并:

    3、对分割后的子序列进行递归地合并,按一定顺序组合成有序子序列,有序子序列之间继续合并

    4、最终合并成一个完整的长度为L的有序序列

    代码截图如下:

C++实现归并排序算法

    例子演示

    接下来举一个例子,假设我们的数组a有如下8个元素,分别为84,25,59,71,62,16,34,45,现在进行合并排序。

数组a

    首先进行分解,分解成两个子序列,为a[0:3],a[4:7],此时序列a[0:3]还可以继续分解,继续分解为两个子序列a[0:1]和a[2:3],序列a[0:1]继续分解,得到两个不能再划分的子序列a[0]和a[1]。如下图所示:

得到最小子序列a[0]和a[1]

    这个时候,我们对这两个不能再划分的子序列a[0]和a[1]进行合并,一个元素的序列可以看成是有序序列,我们把它们合并成有序序列。我们用一个临时的数组tmpArr用于辅助对数据进行存储,该数组长度与数组a长度一致。a[0]>a[1],于是把a[1]的值25存储于tmpArr[0],因为两个序列都只有一个元素,因此最后将84存储于tmpArr[1]。最后将tmpArr[0:1]复制到a[0:1],替换原先的值。如下图所示:

数组tmpArr[0:1]
a[0]和a[1]合并

    接下来,返回上一步。发现a[2:3]可以分解,继续分解为两个子序列a[2]和a[3],得到下图:

最小子序列a[2]和a[3]

    然后这两个子序列a[2]和a[3]进行合并。每次合并都会新建一个临时的数组tmpArr。a[2]

tmpArr[2:3]
a[2]和a[3]合并

    然后,我们对子序列a[0:1]和a[2:3]进行合并。同样借助临时创建的tmpArr数组来存储数据。序列a[0:1]中元素a[0]为25,小于序列a[2:3]中的a[2]【值为59】,于是25先提出来,存储于tmpArr[0]。接下来,继续将序列a[0:1]中a[1]和序列a[2:3]中的a[2]做比较,发现a[1]>a[2],因此,将59存储于tmpArr[1]中。继续将序列a[0:1]中a[1]和序列a[2:3]中的a[3]做比较,发现a[1]>a[3],因此,将71存储于tmpArr[2]中。最后将a[1]中存储的值84赋值给tmpArr[3]。此时可以发现tmpArr[0:3]已经排好序,将其复制到a[0:3]。这样就完成合并了。

数组tmpArr变化(从上往下)
a[0:1]和a[2:3]合并

    接下来,我们对序列a[4:7]继续进行分解,分解为a[4:5]和a[6:7]。然后序列a[4:5]继续分解为a[4]和a[5],得到下图:

最小子序列a[4]和a[5]

    这个时候,我们对这两个不能再划分的子序列a[4]和a[5]进行合并,a[4]>a[5],因此将a[5]的值16存储于tmpArr[4],将62存储于tmpArr[5]。最后将tmpArr[4:5]复制到a[4:5],替换原先的值。如下图所示:

tmpArr[4:5]
a[4]和a[5]合并

    接下来,返回上一步。发现a[6:7]可以分解,继续分解为两个子序列a[6]和a[7],得到下图:

最小子序列a[6]和a[7]

    然后这两个子序列a[6]和a[7]进行合并。a[6]

a[6]和a[7]合并

    然后,我们对子序列a[4:5]和a[6:7]进行合并。同样借助tmpArr数组来存储数据。序列a[4:5]中元素a[4]为16,小于序列a[6:7]中的a[6]【值为34】,于是16先提出来,存储于tmpArr[4]。接下来,继续将序列a[4:5]中a[5]和序列a[6:7]中的a[6]做比较,发现a[5]>a[6],因此,将34存储于tmpArr[5]中。继续将序列a[4:5]中a[5]和序列a[6:7]中的a[7]做比较,发现a[5]>a[7],因此,将45存储于tmpArr[6]中。最后将a[5]中存储的值62赋值给tmpArr[7]。此时可以发现tmpArr[4:7]已经排好序,将其复制到a[4:7]。这样就完成合并了。

数组tmpArr变化
a[4:5]和a[6:7]合并

    最后,就是对序列a[0:3]和a[4:7]进行合并。同样需要借助临时的数组tmpArr。将序列a[0:3]和a[4:7]中的数据分别从左往右进行比较。在序列a[0:3]中,a[0]大于序列a[4:7]中的a[4],因此,将tmpArr[0]赋值为a[4],即16;接下来,继续比较,发现序列a[0:3]中的a[0]小于序列a[4:7]中的a[5],因此,将a[0]中的值25赋值给tmpArr[1]。继续比较,发现序列a[0:3]中的a[1]大于序列a[4:7]中的a[5],因此,将a[5]中的值34赋值给tmpArr[2]。继续比较,发现序列a[0:3]中的a[1]大于序列a[4:7]中的a[6],因此,将a[6]中的值45赋值给tmpArr[3]。继续比较,发现序列a[0:3]中的a[1]小于序列a[4:7]中的a[7],因此,将a[1]中的值59赋值给tmpArr[4]。继续比较,发现序列a[0:3]中的a[2]大于序列a[4:7]中的a[7],因此,将a[7]中的值62赋值给tmpArr[5]。这个时候发现序列a[4:7]中数据已经无剩余,我们将序列a[0:3]中剩余的部分a[2:3]中存储的值复制到tmpArr[6:7]。最后得到排好序的有序数组tmpArr,并将其复制到数组a中,完成最后的合并。

a[0:3]和a[4:7]合并

    时间复杂度分析

    归并排序的时间复杂度主要是考虑两个函数的时间花销:

    1、数组划分函数mergesort()

    2、有序数组归并函数merge()

    merge()函数的时间复杂度为O(n),因为代码中有4个非嵌套的循环,所以时间复杂度则为O(n);调用mergesort()函数划分两部分,那每一小部分排序好所花时间则为T[n/2],而最后把这两部分有序的数组合并成一个有序的数组merge()函数所花的时间为O(n);

    故得到公式:T[n] =2T[n/2] + O(n);

    根据前面小编分享的主定理法,可简单快速得到T[n]=O(nlogn)

    当然可以采用下述的递归树方法来求得算法的时间复杂度。

    递归树

    递归树是一棵结点带权值的树。初始的递归树只有一个结点,它的权标记为T(n)。然后按照递归树的迭代规则不断进行迭代,每迭代一次递归树就增加一层,直到树中不再含有权值为函数的结点【即叶节点都为T(1)】。下面以递归方程来讲述递归树的迭代原则。

T(n)

    第一步:把根结点T(n)用根是cn,左节点为T(n/2)、右节点为T(n/2)的子树替代。(即:以分解、合并子问题需要的代价为根,分解得到的子问题为叶的子树。其中常量c代表求解规模为1的问题所需的时间);如下图中的(a)—(b)

    第二步:把叶结点按照“第一步”的方式展开;T(n/2)用根是cn/2,左节点为T(n/4),右节点为T(n/4)的子树替代

    第三步:反复按照“第一步”的方式迭代,每迭代一次递归树就增加一层,直到树中不再含有权值为函数的结点(即叶结点都为T(1))

递归树

    在得到递归树后,将树中每层中的代价求和,得到每层代价,然后将所有层的代价求和,得到所有层次的递归调用的总代价。在上图中,完全展开的递归树高度为lgn(树高为根结点到叶结点最长简单路径上边的数目)。所以递归树具有lgn+1层。总代价为cn*(lgn+1)。所以时间复杂度为O(nlgn)。

    同理可采用此方法得到归并排序的时间复杂度为O(nlgn)。

    空间复杂度

    归并排序每次递归需要用到一个辅助表,长度与待排序的表相等,虽然递归次数是O(log2n),但每次递归都会释放掉所占的辅助空间,所以下次递归的栈空间和辅助空间与这部分释放的空间就不相关了,因而空间复杂度还是O(n)。

    有误请指正,谢谢!

你可能感兴趣的:(分治策略之归并排序)