归并排序及其应用

 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;  
    }

}

例子:翻转对

493. 翻转对 - 力扣(LeetCode)

给定一个数组 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;   
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值