【算法导论】归并排序

递归与分治

许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或多次递归地调用其自身以解决紧密相关的若干子问题。这些算法典型地遵循分治法的思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。

分治模式在每层递归时都有三个步骤:

  1. 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例。
  2. 解决这些子问题,递归地求解各子问题。然而,若子问题的规模足够小,则直接求解。
  3. 合并这些子问题的解成原问题的解。

归并排序

归并排序算法完全遵循分治模式。直观上其操作如下:

(1)分解:分解等排序的n个元素的序列成各具n/2个元素的两个子序列;

(2)解决:使用归并排序递归地排序两个子序列;

(3)合并:合并两个已排序的子序列以产生已排序的答案。

当待排序的序列长度为1时,递归"开始回升",在这种情况下不根做任何工作,因为长度为1的每个序列都已排好序。

归并排序算法的关键操作是"合并"步骤中两个已排序序列的合并。我们可以通过调用一个辅助过程Merge(A, p, q, r)来完成合并,其中A是一个数组,p、q和r是数组下标,满足 p ≤ q < r。该过程假设子数组A[p..q]和A[q+1..r]都已排好序。它合并这两个子数组形成单一的已排好序的子数组并代替当前的子数组A[p..r]。

Merge(A, p, q, r)
1    n1 = q - p + 1
2    n2 = r - q
3    let L[1..n1+1] and R[1..n2+1] be new arrays
4    for (i = 1; i <= n1; i++)
5        L[i] = A[p+i-1]
6    for (j = 1; j <= n2; j++)
7        R[j] = A[q+j]
8    L[n1+1] = INF
9    R[n2+1] = INF
10    i = 1
11    j = 1
12    for (k = p; k <= r; k++)
13        if (L[i] <= R[j])
14            A[k] = L[i]
15            i = i + 1
16        else A[k] = R[j]
17            j = j + 1

过程Merge的详细工作过程如下:

  1. 第1行计算子数组A[p..q]的长度n1,第2行计算子数组A[q+1..r]的长度n2;
  2. 在第3行,我们创建长度分别为n1+1和n2+1的数组L和R("左"和"右"),每个数组中额外的位置将保存哨兵;
  3. 第4-5行的for循环将子数组A[p..q]复制到L[1..n1],第6-7行的for循环将子数组A[q+1..r]复制到R[1..n2];
  4. 第8-9行将哨兵放在数组L和R的末尾;
  5. 第10-17行通过维持以下循环不变式,执行r-p+1个基本步骤:

   在开始第12-17行for循环的每次迭代时,子数组A[p..k-1]按从小到大的顺序包含L[1..n1+1]和R[1..n2+1]中的k-p个最小元素。进而,L[i]和R[j]是各自所在数组中未被复制回数组A的最小元素。

现在我们可以把Merge作为归并排序算法中的一个子程序来用。下面的过程Merge-sort(A, p, r)排序子数组A[p..r]中的元素。若 p ≥ r,则该子数组最多有一个元素,所以已经排好序。否则,分解步骤简单地计算一个下标q,将A[p..r]分成两个子数组A[p..q]和A[q+1..r]。

Merge-sort(A, p, r)
1    if (p < r)
2        q = (p + r) / 2
3        Merge-sort(A, p, q)
4        Merge-sort(A, q+1, r)
5        Merge(A, p, q, r)

时间复杂度分析

下面我们分析建立归并排序n个数的最坏情况运行时间T(n)的递归式。归并排序一个元素需要常量时间。当有n>1个元素时,我们分解运行时间如下:

分解:分解步骤仅计算子数组的中间位置,需要常量时间,因此,D(n) = O(1)。

解决:我们递归地求解两个规模均为n/2的子问题,将贡献2T(n/2)的运行时间。

合并:我们已经注意到在一个具有n个元素的子数组上过程Merge需要O(n)的时间,所以C(n) = O(n)。

则当n>1时,T(n) = 2T(n/2) + O(n)。递归式求解如下所示:

Exercise 2.3

1.说明归并排序在数组A=<3, 41, 52, 26, 38, 57, 9, 49>上的操作。

【解答】如下图所示:

2.重写过程Merge,使之不使用哨兵,而是一旦数组L或R的所有元素均被复制回A就立刻停止,然后把另一个数组的剩余部分复制回A。

【解答】参考伪代码如下所示

Merge(A, p, q, r)
1    n1 = q - p + 1
2    n2 = r - q
3    let L[1..n1] and R[1..n2] be new arrays
4    for (i = 1; i <= n1; i++)
5        L[i] = A[p+i-1]
6    for (j = 1; j <= n2; j++)
7        R[j] = A[q+j]
8    i = 1
9    j = 1
10    k = p
11    while (i <= n1 && j <= n2)
12        if (L[i] <= R[j])
13            A[k++] = L[i++]
14        else A[k++] = R[j++]
15    while (i <= n1) A[k++] = L[i++]
16    while (j <= n2) A[k++] = R[j++]

3.使用数组归纳法证明:当n刚好是2的幂时,以下递归式的解是T(n) = nlgn。

   T(n)=2T(n/2)+n,若n=2k,k>1,且T(2)=2。

【证明】略。

4.我们可以把插入排序表示为如下递归过程。为了排序A[1..n],我们递归地排序A[1..n-1],然后把A[n]插入已排序的数组A[1..n-1]。为插入排序的这个递归版本的最坏情况运行时间写一个递归式。

【解答】T(n) = T(n-1) + O(n) = O(n2)

5.回顾查找问题(参见练习2.1-3),注意到,如果序列A已排好序,就可以将该序列的中点与v进行比较。根据比较的结果,原序列中有一半就可以不用再做进一步的考虑了。二分查找算法重复这个过程,每次都将序列剩余部分的规模减半。为二分查找写出迭代或递归的伪代码。证明:二分查找的最坏情况运行时间为O(lgn)。

【解答】二分查找递归参考伪代码如下所示:

Binary-search(A, p, r, v)
1    if (p > r) return NIL
2    q = (p + r) / 2
3    if (A[q] == v) return q
4    else if (A[q] > v)
5        return Binary-search(A, p, q-1)
6    else return Binary-search(A, q+1, r)

迭代参考伪代码如下所示:

Binary-search(A, p, r, v)
1    while (p < r)
2        q = (p + r) / 2
3        if (A[q] == v) return q
4        else if (A[q] > v) r = q - 1
5        else p = q + 1
6    return NIL

时间复杂度:T(n) = T(n/2) + O(1) = O(lgn)。

6.注意到插入排序中的过程Insertion-sort的第5-7行的while循环彩一种线性查找来(反向)扫描已排好序的子数组A[1..j-1]。我们可以使用二分查找(参见练习2.3-5)来把插入排序的最坏情况总运行时间改进到O(nlgn)吗?

【解答】不能。因为虽然能够减少查找的时间,但右移的时间是不能够减少的,依然是O(n)。

#7.描述一个运行时间为O(nlgn)的算法,给定n个整数的集合S和另一个整数x,该算法能确定S中是否存在两个其和刚好为x的元素。

【解答】先排序,然后用双指针分别从数组的头和尾遍历查找即可。

附录:归并排序代码示例

/**
 * 如果这段代码好用,那么它是xiaoxxmu写的
 * 否则,我也不知道是谁写的
 */
#include <stdio.h>
#include <stdlib.h>

void merge(int arr[], int p, int q, int r)
{
    int len1 = q - p + 1, len2 = r - q, i, j, k;
    int *L = (int *) malloc (len1 * sizeof(int));
    int *R = (int *) malloc (len2 * sizeof(int));
    for (i = 0; i < len1; i++)
        L[i] = arr[p+i];
    for (j = 0; j < len2; j++)
        R[j] = arr[q+j+1];
    i = 0, j = 0, k = p;
    while (i < len1 && j < len2)
    {
        if (L[i] < R[j]) arr[k++] = L[i++];
        else arr[k++] = R[j++];
    }
    while (i < len1) arr[k++] = L[i++];
    while (j < len2) arr[k++] = R[j++];
}

void merge_sort(int arr[], int p, int r)
{
    if (p < r)
    {
        int q = (p + r) / 2;
        merge_sort(arr, p, q);
        merge_sort(arr, q+1,r);
        merge(arr, p, q, r);
    }
}

int main(int argc, char *argv[])
{
    int arr[] = {3, 41, 52, 26, 38, 57, 9, 49, 44};
    int length = (sizeof arr) / (sizeof arr[0]);
    merge_sort(arr, 0, length - 1);
    for (int i = 0; i < length; i++)
    {
        if (i == 0) printf("%d", arr[i]);
        else printf(" %d", arr[i]);
    }
    printf("\n");
    return 0;
}
时间: 2024-11-03 21:49:46

【算法导论】归并排序的相关文章

算法导论学习笔记(2)-归并排序

今天学习了算法导论上的归并排序算法,并且完成了在纸上写出伪代码,以前就学过归并但是理解的不够透彻,以 前还一直困惑:为什么明明归并排序比快排的时间复杂度更稳定,为什么库函数不用归并而用快排,现在知道原因了,因为归并排序必须开额外的空间,而且空间开销还比较大,下面介绍算法: 首先,归并排序用到了分治的思想,把大数据分成若干个小数据,然后再分别对小数据进行处理,最后把小数据 合并成大数据. 其次,归并排序用到了一个最重要的特点,就是把两组已经排序的数据合并成一组有序数据,并且该过程的时间复 杂度为O

算法导论之插入排序和归并排序

一.创建我们的测试工程 因为我们只理解相应算法,没有什么用户图形,也就用不到UI了,在这儿使用Xcode创建一个基于Mac开发的控制台工程即可,整个工程很简单,一个main函数一个排序类,如下所示. 在Sort类中我们写了关于排序的一些类方法,然后在main函数中进行调用. 二.插入排序 插入排序顾名思义,就是把无序的元素插入到有序的元素当中.<算法导论>中举了一个特为形象的例子,插入排序就如同你在打扑克时摸牌一样,手里的牌是有序的,而你刚摸得牌是是随机的,需要你插入到已经排好序的扑克牌中,这

《算法导论》读书笔记之排序算法—Merge Sort 归并排序算法

自从打ACM以来也算是用归并排序了好久,现在就写一篇博客来介绍一下这个算法吧 :) 图片来自维基百科,显示了完整的归并排序过程.例如数组{38, 27, 43, 3, 9, 82, 10}. 在算法导论讲分治算法一章的时候提到了归并排序.首先,归并排序是一个分治算法. 归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表, 即把待排序序列分为若干个有序的子序列,再把有序的子序列合并为整体有序序列. merg() 函数是用来合并两个已有序的数组.  是整个算法的关键. 那么归并

算法导论 (一)归并排序实现 c++

#include <iostream> using namespace std; void Merge_Sort(int *a,int p,int q ,int r)//归并 { int i,j,k; int n1=q-p+1; int n2=r-q; int *le=NULL; int *ri=NULL; le = new int [n1]; ri = new int[n2]; for(i=0;i<n1;i++) { le[i]=a[p+i]; } for(j=0;j<n2;j+

算法导论学习之插入排序+合并排序

最近准备花时间把算法导论详细的看一遍,强化一下算法和数据结构的基础,将一些总结性的东西写到博客上去. 一.插入排序 算法思想:如果一个数组A,从A[1–n-1]都是有序的,然后我们将A[n]插入到A[1–n-1]的某个合适的位置上去那么就可以保证A[1–n]都是有序的.这就是插入排序的思想:具体实现的时候我们将数组的第一个元素看出有序,然后从第二个元素开始按照上面的步骤进行插入操作,直到插入最后一个元素,然后整个数组都是有序的了. 时间复杂度分析:代码中有两重for循环,很容易看出时间复杂度是n

算法导论_第二章(1)

年前的时候去逛书店,久仰算法导论这本书的大名看见后也就买了下来.回家看了一段时间,发现看书的进度真的是极慢,书里的课后题很多,那些不会的问题也是通过网上搜别人的答案才得以解决的.所以,我就想把我看这本书的心得连带课后的解答分享给大家.同时也是给我坚持把算法导论这本书看完的一个动力 ^_^ 因为本书的第一章相当于一个导论就直接跳过了,那么,从第二章开始! 第二章主要介绍了插入排序和归并排序: 所谓的插入排序就像是一局扑克刚开始时的摸牌阶段,你对手中的扑克所做的整理排序一样.开始时,我们的左手为空并

算法导论6:排序小结和最值取法 2016.1.6

今天想做测试各个排序算法运行时间比较的程序,来对这几天学的排序算法小结一下.所以我先生成了1000000个1~150之间的随机数存到文件里.然后做了一个测试运行时间的程序.想看一下结构.但是结果效果并不太好.实践中,自己做的qsort函数和mergesort函数并没有理想中的那么快. 结果是这样:(可能并不准确,但却是是运行结果) 库函数快速排序:0.139000 seconds自制快速排序:0.375000 seconds归并排序:0.358000 seconds堆排序:0.525000 se

排序算法系列——归并排序

记录学习点滴,菜鸟成长记 归并排序的英文叫做Merge-Sort,要想明白归并排序算法,还要从“递归”的概念谈起. 1.递归 一般来讲,人在做决策行事的时候是往往是从已知出发,比如,我又要举个不恰当的例子了→_→: 看到漂亮姑娘→喜欢人家→追→女朋友→老婆 但是人家施瓦辛格不是这么想的,人家从小就立志当总统: 要当总统←先当州长←竞选州长要有钱←那得找个有钱妹子←妹子都喜欢明星←身材好能当明星←健身 递归,就像一个人对自己的发展有清晰的规划和坚定的信心一样,他知道每一步会有怎么样的结果,他需要仅

算法导论2:几个习题 2016.1.2

一.在归并排序中对小数组采用插入排序(放在上一篇里了): 二.冒泡排序 冒泡排序效率几乎是所有排序里最低的,但却很流行,就是因为它的变成复杂度也是最低的.大多数时候,效率还不及插入排序,其实冒泡排序.插入排序.选择排序基本上效果是差不多的(这个效果不是功能..功能上讲肯定差不多啊都是排序),只是过程略有区别.既然写到这里,就自己总结一下三者吧. 1.插入排序——摸扑克牌的过程 假定前一个是有序的,把第二个插进它应当在的位置,那么前两个就是有序的了,把第三个插进它应当在的位置,那么前三个就是有序的

算法导论基础(第一~五章)

插入排序 最好情况输入数组开始时候就是满足要求的排好序的,时间代价为θ(n): 最坏情况输入数组是按逆序排序的,时间代价为θ(n^2). 归并排序 归并排序采用了算法设计中的分治法,分治法的思想是将原问题分解成n个规模较小而结构与原问题相似的小问题,递归的解决这些子问题,然后再去合并其结果,得到原问题的解. 分治模式在每一层递归上有三个步骤: 分解(divide):将原问题分解成一系列子问题. 解决(conquer):递归地解答各子问题,若子问题足够小,则直接求解. 合并(combine):将子