题目大意:
给定一个数组 nums
,如果 i < j
且 nums[i] > 2*nums[j]
我们就将 (i, j)
称作一个重要翻转对。
你需要返回给定数组中的重要翻转对的数量。
示例 1:
输入: [1,3,2,3,1] 输出: 2
示例 2:
输入: [2,4,3,5,1] 输出: 3
注意:
- 给定数组的长度不会超过
50000
。 - 输入数组中的所有数字都在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 493. Reverse Pairs翻转对思路分析及源码分享》有1条回应