归并排序
参考: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; }
四,算法分析
① 算法的稳定性
如果 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; }
========== ========= ======== ======== ====== ====== ==== === == =