LEETCODE 493. Reverse Pairs翻转对思路分析及源码分享

题目大意:

给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对

你需要返回给定数组中的重要翻转对的数量。

示例 1:

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

示例 2:

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

注意:

  1. 给定数组的长度不会超过50000
  2. 输入数组中的所有数字都在32位整数的表示范围内。

如果想查看本题目是哪家公司的面试题,请参考以下免费链接: https://leetcode.jp/problemdetail.php?id=493

解题思路:

这道题使用全体遍历循环的暴力算法肯定超时,所以考虑再三还是采用归并排序算法比较靠谱,归并排序英文叫做merge sort,顾名思义,边递归边排序。这里先要啰嗦几句归并排序的算法,因为这是本题的核心部分,当然老司机可以跳过。

归并排序的核心首先在于拆分数组,通常情况下会将数组从中间一分为二,如果数组的元素数为偶数,那么,拆分后的两个数组个数相同,如果是奇数,那么其中一个子数组的元素数会比另一个大1。

其次,要考虑到递归,因为数组只拆分一次不足以将整个数组排序,要把复杂问题简单化就要将数组拆分到极致。什么是极致,很简单,如果拆分后的两个数组分别只有一个元素,或是其中一个子数组有1个元素,另一个子数组为空,这时做排序比较就很简单了。比如:leftArray = {5}, rightArray={3},或者:leftArray = {5}, rightArray={}

假定我们需要将数组按照从小到大的正序排列,那么,我们可以新建一个数组int[] tempNums,作为排序后的新数组。这时只要比较两边的数字,先将小的一个放入新数组,再将大的放入数组tempNums,这样就完成了排序工作。当然,如果其中一个子数组为空,那么只要将存在元素的那个数组存入新数组就可以了。

因为使用了递归拆分数组并进行排序,所以数组会从最小的单元逐级向上进行排序,直到整个数组排序完成。大体的实现顺序如下所示

int num[] = {4,6,3,2,7,9,5,1};
// 递归拆分
{4,6,3,2}         {7,9,5,1}
{4,6}    {3,2}    {7,9}    {5,1}
{4} {6}  {3} {2}  {7} {9}  {5} {1}
// 递归排序
{4}{6}  {3}{2}  {7}{9}  {5}{1} ⇒ {4,6}  {2,3}  {7,9}  {1,5} 
{4,6}  {2,3}  {7,9}  {1,5} ⇒ {2,3,4,6}  {1,5,7,9}
{2,3,4,6}  {1,5,7,9} ⇒ {1,2,3,4,5,6,7,9}

归并排序的实现代码:

// 起始start为0,end为数组长度-1
public void find(int[] nums, int start, int end) {
	if (start >= end) {
		return;
	}
        // 数组中间index
	int middle = (end + start) / 2;
        
	int leftArrayStart = start; // 拆分后左边子数组起始点
	int leftArrayEnd = middle; // 拆分后左边子数组终点
	int rightArrayStart = middle + 1; // 拆分后右边子数组起始点
	int rightArrayEnd = end; // 拆分后右边子数组终点
        // 继续递归拆分左边子数组
        find(nums, leftArrayStart, leftArrayEnd);
        // 继续递归拆分右边子数组
        find(nums, rightArrayStart, rightArrayEnd);


        // 定义一个临时数组用于存放两个子数组合并排序后的数组
	int[] tempNums = new int[end - start + 1];
	int i = 0;
        // 因为两边数组都是分别排序好的,所以合并排序比较简单
	while (leftArrayStart <= leftArrayEnd && rightArrayStart <= rightArrayEnd) {
                // 如果左边的数小于右边的,那么将左边的数放入临时数组中。
                // 同时左边起始index加一
                // 反之则将右边数字放入临时数组,同时右边起始index加一
                // 直到一边数组全部放入临时数组中为止
		if (nums[leftArrayStart] < nums[rightArrayStart]) {
			tempNums[i] = nums[leftArrayStart];
			leftArrayStart++;
		} else {
			tempNums[i] = nums[rightArrayStart];
			rightArrayStart++;
		}
		i++;
	}
        // 如果左边数组有剩余,则将左边数组剩余的元素全部放入临时数组
	while (leftArrayStart <= leftArrayEnd) {
		tempNums[i] = nums[leftArrayStart];
		leftArrayStart++;
		i++;
	}
        // 如果右边数组有剩余,则将右边数组剩余的元素全部放入临时数组
	while (rightArrayStart <= rightArrayEnd) {
		tempNums[i] = nums[rightArrayStart];
		rightArrayStart++;
		i++;
	}
        // 将排序好的临时数组更新到原数组中
	for (int j = 0; j < tempNums.length; j++) {
		nums[start + j] = tempNums[j];
	}
}

到此为止,归并排序算法的思路就非常清晰了。那么回到本题,如何利用该算法找到所有的翻转对呢?其实通过上面的find()函数的代码,我们可以得到几个比较有用的信息:

第一,递归排序的执行循序是,先将大数组一步一步的拆分成最小数组,然后再一步一步的将小数组合并排序成大数组的一个过程。因此,拆分后的两个数组分别是有序排列的(从小到大)。

第二,根据第一点,我们可以想到,问题其实变得简单了,我们循环左边子数组的元素,如果该元素比右边子数组的某个元素大两倍以上的话,那么我们就找到了一个翻转对,同时将右边的数组index加一继续向后寻找,直到没有翻转对出现时,左边数组index加一,继续执行同样的比较,直到循环结束为止,这样,所有翻转对就都可以被找到。不过,有人会问,为什么左边子数组中的元素不需要在左边的子数组中找翻转对呢?因为我们采用的递归算法,不论左边还是右边的子数组都会再次被拆分执行同样的逻辑,因此不需要在同数组中寻找答案。

第三,算法可以稍微进行优化,因为左右两边的子数组都是进行过排序的,所以,当Left[i] * 2 > Right[j]时,那么 Left[i + 1] * 2也一定大于 Right[j]。这样在进行循环时,每当右边的数组index加一后,右边数组的index是不需要重置的。

第四,最后有个小细节需要注意一下,因为用到了乘法 (*2)操作,所以应考虑到int类型乘以2会超过int最大值,所以结果应转为long型比较。

思路讲完了,最后还是看看完整的代码吧:

public int reversePairs(int[] nums) {
        // 返回结果,翻转对总个数
	int res = 0;
        // 数组的长度
	int length = nums.length;
        // 数组为空还找个毛线
	if (length <= 1) {
		return res;
	}
        // 利用归并排序算法查找所有翻转对
	return find(nums, 0, nums.length - 1);
}

public int find(int[] nums, int start, int end) {
        // 找到的翻转对个数
	int result = 0;
        // 当起始位和结束位相同时,说明已将数组拆分至极致,结束递归
	if (start >= end) {
		return 0;
	}
        // 找到数组的中间位
	int middle = (end + start) / 2;
        // 分别递归寻找左右两个数组中存在的翻转对个数
	result = find(nums, start, middle) + find(nums, middle + 1, end);
        // 以下代码为寻找左右两个数组之间存在的翻转对个数
	int leftArrayStart = start; // 拆分后左边子数组起始点
	int leftArrayEnd = middle; // 拆分后左边子数组终点
	int rightArrayStart = middle + 1; // 拆分后右边子数组起始点
	int rightArrayEnd = end; // 拆分后右边子数组终点
        // 循环左边数组,直到起始点等于终点为止
	while (leftArrayStart <= leftArrayEnd) {
                // 如果左边数组当前元素是右边数组当前元素的2倍以上
		if ((long) nums[leftArrayStart] > (long) nums[rightArrayStart] * 2) {
                        // 右边数组起始位加一继续向后寻找
			rightArrayStart++;
                        // 如果加一后右边数组越界,则取消加一操作
                        // 将右边数组起始inex设为结束index即可
			if (rightArrayStart > rightArrayEnd) {
				rightArrayStart = rightArrayEnd;
                                // 因为数组是从小到大排序的,
                                // 所以右数组当前数之前的数字均小于当前数字,
                                // 所以之前的所有数字均为左数组当前数字的翻转对
                                // 当前左边数组元素找到的翻转对个数应该为:
                                // 右边数组的起始index数。
				result += (rightArrayStart - middle);
                                // 因为有数组已经到头,所以左数组加一继续循环
				leftArrayStart++;
			}
		} else {
                        // 如果做数组当前数不是有数组当前数的两倍以上
                        // 那么有数组当前index之前的数都是左数组当前数的翻转对
			result += (rightArrayStart - middle - 1);
                        //左数组加一继续循环
			leftArrayStart++;
		}
	}
        // 到此为止,翻转对查找工作已经完成,接下来是归并排序逻辑,为了之后的递归处理做准备。
	leftArrayStart = start;
	rightArrayStart = middle + 1;
	int[] tempNums = new int[end - start + 1];
	int i = 0;
	while (leftArrayStart <= leftArrayEnd && rightArrayStart <= rightArrayEnd) {
		if (nums[leftArrayStart] < nums[rightArrayStart]) {
			tempNums[i] = nums[leftArrayStart];
			leftArrayStart++;
		} else {
			tempNums[i] = nums[rightArrayStart];
			rightArrayStart++;
		}
		i++;
	}
	while (leftArrayStart <= leftArrayEnd) {
		tempNums[i] = nums[leftArrayStart];
		leftArrayStart++;
		i++;
	}
	while (rightArrayStart <= rightArrayEnd) {
		tempNums[i] = nums[rightArrayStart];
		rightArrayStart++;
		i++;
	}
	for (int j = 0; j < tempNums.length; j++) {
		nums[start + j] = tempNums[j];
	}
	return result;
}

本网站文章均为原创内容,并可随意转载,但请标明本文链接
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/leetcode-493-reverse-pairs翻转对思路分析及源码分享/
此条目发表在leetcode分类目录,贴了, , , 标签。将固定链接加入收藏夹。

LEETCODE 493. Reverse Pairs翻转对思路分析及源码分享》有1条回应

    发表评论

    您的电子邮箱地址不会被公开。