归并排序

参考:https://www.bilibili.com/video/BV1Ax411U7Xx?spm_id_from=333.999.0.0

参考:https://baike.baidu.com/item/%E9%80%86%E5%BA%8F%E5%AF%B9/11035554?fr=aladdin

 

 

一,merge 函数

1,函数功能

  保证数组左半边序列和右半边序列皆有序,则将这两个子序列归并到第二个数组,再放回原来的数组

2,步骤

  ① 遍历两个子序列的元素,比较遍历到的元素哪个小,则哪个先放入新数组中,直至某一方遍历完

  ② 如果某一子序列的元素还有剩余,则将该子序列剩余的元素按顺序放回原数组。

  ③ 最后将新数组的元素按顺序赋值给原数组。

 

 

二,mergeSort 函数

1,引入

  merge 要处理的是数组左半边序列和右半边序列皆有序的数组,那么怎么保证这一点呢?

2,原理

  利用递归的回溯功能。

    ① 先是将区间不断细分至最多只有两个元素。此时,左右两个子序列最多只有一个元素,自然是有序

    ② 然后开始在回溯时利用 merge 函数。此时,使用完 merge 的区间全变成有序的,然后回溯到大的区间时,其左右序列的区间必然经历过回溯,自然就能保证有序。

    ③ 最终,回溯完整个数组,则整个数组有序。

  注意:

    使用递归函数二分区间时,当数组下标从 0 开始时,左区间的调用不能用 m-1,即 mergeSort(l, m-1),因为这样调用的话,区间 [0, 1] 会一直无法被二分,陷入无限递归。所以,只能用 mergeSort(l, m)。

 

 

三,代码

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 404
int a[N], b[N];
void merge(int L, int R)
{
    int mid = (L + R) >> 1;
    int i = L, j = mid + 1;
    int cnt = L;
    while (i <= mid&&j <= R) // 将 a 数组分成左右两边,归并到 c 数组
    {
        if (a[i] <= a[j])
            b[cnt++] = a[i++];
        else
            b[cnt++] = a[j++];
    }
    while (i <= mid)       // 前半数组还有剩余的情况
        b[cnt++] = a[i++];
    while (j <= R)         // 后半数组还有剩余的情况
        b[cnt++] = a[j++];
    for (i = L; i <= R; i++)  // 放回去
        a[i] = b[i];
}
void mergeSort(int L, int R)
{
    if (L == R)
        return;
    int mid = (L + R) >> 1;
    mergeSort(L, mid);
    mergeSort(mid + 1, R);
    merge(L, R);
}
int main(void)
{
    int n;
    while (scanf("%d", &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
            scanf("%d", &a[i]);
        mergeSort(1, n);

        for (int i = 1; i <= n; i++)
            printf("%d ", a[i]);
        puts("");
    }
    return 0;
}
View Code

 

 

四,算法分析

① 算法的稳定性

  如果 a[i] == a[j] && i < j,当两者在同一区间进行 merge 时,a[j] 总是跟在 a[i] 后面插入新数组;当两个不在同一区间进行 merge 时,不管 a[j] 怎样插入,都不可能插到 a[i] 前面。综上,归并排序是一种稳定的算法。

② 算法的时间复杂度

  对于长度为 n 的数组,由于其递归搜索分成左右两边,所以递归树的深度为 log2(n),而每次回溯时使用的 merge 函数的时间复杂度为 O(n),所以归并排序的时间复杂度为 O(n*log2(n))

 

 

五,快排与归并的比较

① 总体思想

  快排:利用递归的搜索分治,在搜索时使用 partition

  归并:利用递归的搜索分治,在回溯时使用 merge

② 功能函数  

  partition:将数组根据大小划分在支点两边。

  merge:将左右两边的数组归并到一起。

 

 

六,逆序对

1,定义:

  如果存在正整数 i,j 使得 1 ≤ i < j ≤ n && A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。

 

2,利用 merge 计算逆序对的个数

  ① 对于某一次 merge,每当右边序列的元素先于左边序列的元素插入新数组时,则该元素必可以与左边序列中所有未加入新数组的元素组成逆序对。

  ② 对于所有在回溯时调用的 merge 所统计的和就是 原数组逆序对的数量

 

3,逆序对数量 == 利用交换相邻元素的操作使数组有序的最小交换次数

  ① 我们对乱序的元素作如下处理:

    先将最大的元素交换到最后面,再交换第二大的元素到倒数第二的位置,依元素大小的顺序重复操作。

  ② 则对于元素 e,e 的当前位置与 e 在有序时的位置之间的元素,皆能与 e 组成逆序对,且只有这些元素能与 e 组成逆序对。

  ③ 证明:

    对于乱序时,如果 e 前面的元素大于 e,则与 ① 矛盾,所以不可能发生。

    对于乱序时,如果 e 前面的元素小于 e,则不可能与 e 组成逆序对。

    对于乱序时,如果 e 后面的元素大于 e,则依据 ①,说明该位置就是 e 在有序时的位置

    对于乱序时,如果 e 后面的元素小于 e,则可以与 e 组成逆序对。

    综上,所以 e 可以通过交换其后面小于它的元素直至遇到遇到一个大于它的元素,则此时 e 所在的位置就是 e 有序时的位置。所以 ② 得证

  ④ 综上,将所有元素移动到其有序时的位置的移动次数 == 逆序对数量,即 逆序对数量 == 利用交换相邻元素的操作使数组有序的最小交换次数 得证。

 

4,例题

http://poj.org/problem?id=1804

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 10005
int a[N], b[N], num;
void merge(int L, int R)
{
    int mid = (L + R) >> 1;
    int i = L, j = mid + 1;
    int cnt = L;
    while (i <= mid&&j <= R) // 将 a 数组分成左右两边,归并到 c 数组
    {
        if (a[i] <= a[j])
            b[cnt++] = a[i++];
        else
        {
            b[cnt++] = a[j++];
            num += mid - i + 1;
        }
    }
    while (i <= mid)       // 前半数组还有剩余的情况
        b[cnt++] = a[i++];
    while (j <= R)         // 后半数组还有剩余的情况
        b[cnt++] = a[j++];
    for (i = L; i <= R; i++)  // 放回去
        a[i] = b[i];
}
void mergeSort(int L, int R)
{
    if (L >= R)
        return;
    int mid = (L + R) >> 1;
    mergeSort(L, mid);
    mergeSort(mid + 1, R);
    merge(L, R);
}
int main(void)
{
    int t; scanf("%d", &t);
    for (int j = 0; j < t; j++)
    {
        int n; scanf("%d", &n);
        for (int i = 1; i <= n; i++)
            scanf("%d", &a[i]);

        num = 0;
        mergeSort(1, n);
        printf("Scenario #%d:\n", j + 1);
        printf("%d\n\n", num);
    }
    return 0;
}
View Code

 

 

 

 

========== ========= ======== ======== ====== ====== ==== === == =

      贺新郎 同父见和,再用韵答之
                          (宋)辛弃疾
  老大那堪说!似而今、元龙臭味,孟公瓜葛。我病君来高歌饮,惊散楼头飞雪。
笑富贵、千钧如发。硬语盘空谁来听?记当时、只有西窗月。重进酒,换鸣瑟。
  事无两样人心别。问渠侬:神州毕竟,几番离合?汗血盐车无人顾,千里空收骏骨。
正目断、关河路绝。我最怜君中宵舞,道男儿、到死心如铁!看试手,补天裂。

 

posted @ 2020-03-10 11:46  叫我妖道  阅读(377)  评论(0编辑  收藏  举报
~~加载中~~