1、归并排序
要点:(1)左部分排好序,右部分排好序,利用merge过程让左右整体有序
(2)时间复杂度:根据master公式:O(n*logn)
(3)归并排序为什么比O(n^2)的排序快?因为比较行为没有浪费
下面给出归并排序的代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class MergeSort {
public static int MAXN = 100001;
public static int[] arr = new int[MAXN];
public static int[] help = new int[MAXN];
public static int n;
public static void main(String args[]) {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
n = (int) in.nval;
for (int i = 0; i < n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
}
mergeSort1(0, n-1); //递归算法
//mergeSort2(0, n-1); //非递归算法
for (int i = 0; i < n - 1; i++) {
out.print(arr[i] + " ");
}
out.println(arr[n - 1]);
out.flush();
out.close();
br.close();
}
//递归排序,根据master公式,nlogn复杂度
public static void mergeSort1(int l, int r) {
if (l == r) {
return;
}
int m = (l + r) / 2;
mergeSort1(arr, l, m);
mergeSort1(arr, m+1, r);
merge(l, m, r);
}
//非递归排序,外层循坏复杂度logn,内层循坏复杂度n,因此为nlogn复杂度
public static void mergeSort2() {
for (int l, m, r, step = 1; step < n; step * 2) {
// 内部分组merge,时间复杂度O(n)
l = 0;
while (l < n) {
m = l + step - 1;
if (m + 1 >= n) {
break;
}
r = Math.min(l + (step << 1) - 1, n - 1);
merge(l, m, r);
l = r + 1;
}
}
}
//merge过程,总结一句话,谁小拷贝谁,直到左右两部分所有的数字耗尽
//参数l,m,r的意思是数组(l,m)和数组(m,r)合并
public static void merge(int l, int m, int r) {
int i=l, a = l, b = m+1;
while(a<=m && b<=r) {
help[i++] = arr[a] <= arr[b] ? arr[a++] : arr[b++];
}
while(a<=m) {
help[i++] = arr[a++];
}
while(b<=r) {
help[i++] = arr[b++];
}
for(int i = 0; i <= r, i++) {
arr[i] = help[i]
}
}
}
2、归并排序的应用:归并分治
原理:
1)思考一个问题在大范围上的答案,是否等于,左部分的答案 + 右部分的答案 + 跨越左右产生的答案。
2)计算“跨越左右产生的答案”时,如果加上左、右各自有序这个设定,会不会获得计算的便利性。
3)如果以上两点都成立,那么该问题很可能被归并分治解决(大部分情况下成立)
4)求解答案的过程只需要加入归并排序的过程即可,因为要让左、右各自有序,来获得计算的便利性。
例子:计算数组的小和
计算数组的小和_牛客题霸_牛客网 (nowcoder.com)
数组小和的定义如下:
例如,数组 s = [1, 3, 5, 2, 4, 6] ,在 s[0] 的左边小于或等于 s[0] 的数的和为 0 ; 在 s[1] 的左边小于或等于 s[1] 的数的和为 1 ;在 s[2] 的左边小于或等于 s[2] 的数的和为 1+3=4 ;在 s[3] 的左边小于或等于 s[3] 的数的和为 1 ;
在 s[4] 的左边小于或等于 s[4] 的数的和为 1+3+2=6 ;在 s[5] 的左边小于或等于 s[5] 的数的和为 1+3+5+2+4=15 。所以 s 的小和为 0+1+4+1+6+15=27
给定一个数组 s ,实现函数返回 s 的小和。
思考该问题是否符合归并分治基本原理?
给定一个数组,将其切割为左右两份,则整个数组的小和等于左数组的小和 + 右数组的小和 + 跨越左右数组的小和,且计算“跨越左右数组的小和”时,如果加上左、右各自有序这个设定,会获得计算的便利性。因此符合归并分治原理。
下面给出java代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Main {
public static int MAXN = 100001;
public static int[] arr = new int[MAXN];
public static int[] help = new int[MAXN];
public static int n;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
n = (int) in.nval;
for (int i = 0; i < n; i++) {
in.nextToken();
arr[i] = (int) in.nval;
}
out.println(smallSum(0, n - 1));
}
out.flush();
out.close();
}
// 结果比较大,用int会溢出的,所以返回long类型
// 特别注意溢出这个点,笔试常见坑
// 返回arr[l...r]范围上,小和的累加和,同时请把arr[l..r]变有序
// 时间复杂度O(n * logn)
public static long smallSum(int l, int r) {
if (l==r) {
return 0;
}
int m = (l + r) / 2;
return smallSum(l, m) + smallSum(m, r) + merge(l, m, r);
}
public static long merge(int l, int m, int r) {
long ans=0;
for (int j=m+1, i=1, sum=0; j<=r;j++) {
while(arr[j] >= arr[i] && i<=m) {
sum += arr[i++];
}
ans += sum;
}
// 正常merge
int i = l;
int a = l;
int b = m + 1;
while (a <= m && b <= r) {
help[i++] = arr[a] <= arr[b] ? arr[a++] : arr[b++];
}
while (a <= m) {
help[i++] = arr[a++];
}
while (b <= r) {
help[i++] = arr[b++];
}
for (i = l; i <= r; i++) {
arr[i] = help[i];
}
return ans;
}
}
例子:翻转对
给定一个数组 nums,如果i < j且 nums[i] > 2*nums[j]
我们就将 (i, j)
称作一个重要翻转对。
你需要返回给定数组中的重要翻转对的数量。
给出java代码:
public class Main {
public static int MAXN = 50001;
public static int[] help = new int[MAXN];
public static int reversePairs(int[] arr) {
return counts(arr, 0, arr.length - 1);
}
// 统计l...r范围上,翻转对的数量,同时l...r范围统计完后变有序
// 时间复杂度O(n * logn)
public static int counts(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int m = (l + r) / 2;
return counts(arr, l, m) + counts(arr, m+1, r) + merge(arr, l, m, r);
}
public static int merge(int[] arr, int l, int m, int r) {
int ans = 0;
for (int i=l, j=m+1, sum=0; i<=m; i++) {
while (j<=r && (long)nums[i] > (long)nums[j] * 2) {
sum+=1;
j++;
}
ans = ans + sum;
}
//下面为归并排序过程
int i = l;
int a = l;
int b = m + 1;
while (a <= m && b <= r) {
help[i++] = arr[a] <= arr[b] ? arr[a++] : arr[b++];
}
while (a <= m) {
help[i++] = arr[a++];
}
while (b <= r) {
help[i++] = arr[b++];
}
for (i = l; i <= r; i++) {
arr[i] = help[i];
}
return ans;
}
}