Java教程

16 归并排序(Merge Sort)

本文主要是介绍16 归并排序(Merge Sort),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录
  • 1 题目
  • 2 思路
    • 2.1 图解
    • 2.2 时间复杂度
    • 2.3 空间复杂度
  • 3 源码

1 题目

题目:Sort Integer II

lintcode题号——464,难度——easy

描述:给一组整数,请将其在原地按照升序排序。可使用归并排序,快速排序,堆排序或者任何其他O(n*log n)的排序算法。

样例1:

输入:[3,2,1,4,5],
输出:[1,2,3,4,5]。

样例2:

输入:[2,3,1],
输出:[1,2,3]。

2 思路

  归并排序基于分治法,将复杂的大问题分解成足够简单的小问题,分而治之。先进行分解的步骤,将原本无序的序列至顶向下不断进行二分,直到不可再分,即每个分组都只剩一个元素;之后进行合并的步骤,将分组按照原来拆分的过程,两两合并,在合并的过程中需要按照升序或者降序的需求对元素进行比较,使得合并后的每个小序列都是有序的,直到合并成完整的大序列。

归并排序的步骤:

  1. 将序列不断向下二分;
  2. 直到不可再分,每个分组只有单个元素;(此时每个分组都是有序的)
  3. 将有序分组两两向上合并成有序;(合并两个有序数组)
  4. 直到所有分组合并成完整的序列。

  关于将两个原本就有序(假设升序)的分组合并成一个完整有序的分组,我们只需要依次比较分组的头元素,每次都将较小的元素有序放入新分组,最后一定有一个分组先放完,另一个分组剩下若干个元素,直接将不为空的分组剩余元素(原本就是有序的)放入新分组,这样新分组里的元素就全部都是有序的了。

合并两个有序数组的步骤:(新数组存放合并后的结果)

  1. 比较两个数组的头元素;
  2. 将较小的元素从原数组中取出,放入新数组;
  3. 重复上述1、2步骤,直到两个数组中的一个为空;
  4. 将另一个不为空的数组所有元素放入新数组;
  5. 此时的新数组内的元素即为有序的。

2.1 图解

合并两个有序数组的流程:

graph TD A[A--'1, 2, 5, 7'<br/>B--'3, 4, 6, 8, 9'] --> X X[/比较头元素'1'和'3', 较小元素放入:C--'1'\] --> A1 A1[A--'_, 2, 5, 7'<br/>B--'3, 4, 6, 8, 9'] --> X1 X1[/比较头元素'2'和'3', 较小元素放入:C--'1, 2'\]--> A2 A2[A--'_, _, 5, 7'<br/>B--'3, 4, 6, 8, 9'] --> X2 X2[/比较头元素'5'和'3', 较小元素放入:C--'1, 2, 3'\]--> A3 A3[A--'_, _, 5, 7'<br/>B--'_, 4, 6, 8, 9'] --> X3 X3[/比较头元素'5'和'4', 较小元素放入:C--'1, 2, 3, 4'\]--> A4 A4[A--'_, _, 5, 7'<br/>B--'_, _, 6, 8, 9'] --> X4 X4[/比较头元素'5'和'6', 较小元素放入:C--'1, 2, 3, 4, 5'\]--> A5 A5[A--'_, _, _, 7'<br/>B--'_, _, 6, 8, 9'] --> X5 X5[/比较头元素'7'和'6', 较小元素放入:C--'1, 2, 3, 4, 5, 6'\]--> A6 A6[A--'_, _, _, 7'<br/>B--'_, _, _, 8, 9'] --> X6 X6[/比较头元素'7'和'8', 较小元素放入:C--'1, 2, 3, 4, 5, 6, 7'\]--> A7 A7[A--'_, _, _, _'<br/>B--'_, _, _, 8, 9'] --> X7 X7(A已空, B直接放入:C--'1, 2, 3, 4, 5, 6, 7, 8, 9', 有序数组C)

归并排序的流程:

graph TD A1['3, 5, 7, 4, 9, 8, 2, 10, 1, 6'] A1 -- 二分拆分 --> B1['3, 5, 7, 4, 9'] A1 -- 二分拆分 --> B2['8, 2, 10, 1, 6'] B1 -- 拆分 --> C1['3, 5, 7'] B1 -- 拆分 --> C2['4, 9'] B2 -- 拆分 --> C3['8, 2, 10'] B2 -- 拆分 --> C4['1, 6'] C1 -- 拆分 --> D1['3, 5'] C1 -- 拆分 --> D2['7'] C2 -- 拆分 --> D3['4'] C2 -- 拆分 --> D4['9'] C3 -- 拆分 --> D5['8, 2'] C3 -- 拆分 --> D6['10'] C4 -- 拆分 --> D7['1'] C4 -- 拆分 --> D8['6'] D1 -- 拆分 --> E1['3'] D1 -- 拆分 --> E2['5'] D5 -- 拆分 --> E3['8'] D5 -- 拆分 --> E4['2'] E1 -- 合并有序数组 --> F1['3, 5'] E2 -- 合并有序数组 --> F1 E3 -- 合并有序数组 --> F2['2, 8'] E4 -- 合并有序数组 --> F2 F1 -- 合并 --> G1['3, 5, 7'] D2 -- 合并有序数组 --> G1 D3 -- 合并有序数组 --> G2['4, 9'] D4 -- 合并有序数组 --> G2 F2 -- 合并 --> G3['2, 8, 10'] D6 -- 合并有序数组 --> G3 D7 -- 合并有序数组 --> G4['1, 6'] D8 -- 合并有序数组 --> G4 G1 -- 合并有序数组 --> H1['3, 4, 5, 7, 9'] G2 -- 合并有序数组 --> H1 G3 -- 合并 --> H2['1, 2, 6, 8, 10'] G4 -- 合并 --> H2 H1 -- 合并 --> I1['1, 2, 3, 4, 5, 6, 7, 8, 9, 10'] H2 -- 合并 --> I1 style E1 fill: #f9f, stroke: #333, stroke-width: 4px; style E2 fill: #f9f, stroke: #333, stroke-width: 4px; style D2 fill: #f9f, stroke: #333, stroke-width: 4px; style D3 fill: #f9f, stroke: #333, stroke-width: 4px; style D4 fill: #f9f, stroke: #333, stroke-width: 4px; style E3 fill: #f9f, stroke: #333, stroke-width: 4px; style E4 fill: #f9f, stroke: #333, stroke-width: 4px; style D6 fill: #f9f, stroke: #333, stroke-width: 4px; style D7 fill: #f9f, stroke: #333, stroke-width: 4px; style D8 fill: #f9f, stroke: #333, stroke-width: 4px;

2.2 时间复杂度

  归并排序使用递归实现,对包含n个元素的数组,计算其时间复杂度,先分析拆分过程:

第一层将n个数划分为2个分组,每个分组包含n/2个元素;
第二层将n个数划分为4个分组,每个分组包含n/4个元素;
第三层将n个数划分为8个分组,每个分组包含n/8个元素;
……
第log n层将n个数划分为n个分组,每个分组包含1个元素。

  再看合并过程:

第log n层将n分组合并成n/2个分组,每个元素都会被遍历一次,每个元素耗时O(1),该层耗时O(n);
……
第3层将8分组合并成4个分组,每个元素都会被遍历一次,每个元素耗时O(1),该层耗时O(n);
第2层将4分组合并成2个分组,每个元素都会被遍历一次,每个元素耗时O(1),该层耗时O(n);
第1层将2分组合并成1个分组,每个元素都会被遍历一次,每个元素耗时O(1),该层耗时O(n);

  所以每层耗时为O(n),层数为log n,算法的时间复杂度为O(n*log n)。

2.3 空间复杂度

  算法不能原地排序,需要用到一个与原数组一样大的空间用来存放临时数据,算法的空间复杂度为O(n)。

3 源码

  注意事项:

  1. 需要使用一个临时数组,存放每次递归过程中合并两个有序数组之后的结果,排序完成再复制会原数组,否则直接原地排序会打乱原数组的元素。
  2. 归并排序是递归算法,注意递归的三要素,防止死循环。
  3. 合并有序数组时候,需要注意左右两个数组的起点位置。

  C++版本:

/**
* @param A: an integer array
* @return: nothing
*/
void sortIntegers2(vector<int> &A) {
    // write your code here
    if (A.size() <= 1)
    {
        return;
    }

    vector<int> temp(A.size());
    mergeSort(A, 0, A.size() - 1, temp);
}

// 递归的定义
void mergeSort(vector<int> & A, int start, int end, vector<int> & temp)
{
    if (start >= end) // 递归的出口
    {
        return;
    }

    int mid = start + (end - start) / 2; // 取中点
    mergeSort(A, start, mid, temp); // 递归的拆解
    mergeSort(A, mid + 1, end, temp);
    merge(A, start, mid, end, temp);
}

// 合并有序数组
void merge(vector<int> & A, int start, int mid, int end, vector<int> & temp)
{
    int left = start;
    int right = mid + 1; // 注意此处的起点是中点的下一个元素
    int index = start;
    while (left <= mid && right <= end)
    {
        if (A.at(left) < A.at(right))
        {
            temp.at(index++) = A.at(left++);
        }
        else
        {
            temp.at(index++) = A.at(right++);
        }
    }

    while (left <= mid)
    {
        temp.at(index++) = A.at(left++);
    }
    while (right <= end)
    {
        temp.at(index++) = A.at(right++);
    }

    // 将临时数组中排好序的元素复制会原数组
    for (int i = start; i <= end; i++)
    {
        A.at(i) = temp.at(i);
    }
}
这篇关于16 归并排序(Merge Sort)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!