2019年7月22日 - LeetCode0004
https://leetcode-cn.com/problems/median-of-two-sorted-arrays/submissions/
我的解法:
我看到了那个log的要求,也第一时间想到了二分,但是实在想不出咋二分,最后只能写个暴力的...
1 class Solution { 2 public double findMedianSortedArrays(int[] nums1, int[] nums2) { 3 int m = nums1.length; 4 int n = nums2.length; 5 //特判有一个数组为空的时候 6 if(m==0){ 7 if(n%2==0) 8 return (nums2[n/2-1]+nums2[n/2])/2.0; 9 else 10 return nums2[n/2]; 11 } 12 if(n==0){ 13 if(m%2==0) 14 return (nums1[m/2-1]+nums1[m/2])/2.0; 15 else 16 return nums1[m/2]; 17 } 18 int curr1=0,curr2=0; 19 double ans = 0,ans2 = 0; 20 for(int i=0;i<Math.ceil((m+n)/2.0)+1;++i){ 21 ans = ans2; 22 //如果某个数组到尽头了 23 if(curr1>=m || curr2 >=n){ 24 ans2 = curr1>=m?nums2[curr2++]:nums1[curr1++]; 25 }else{ 26 ans2 = nums1[curr1]<nums2[curr2]?nums1[curr1++]:nums2[curr2++]; 27 } 28 } 29 if((m+n)%2==0) 30 ans = (ans + ans2)/2.0; 31 return ans; 32 } 33 }
中位数我理解为第k小的数,在长度为奇数时为k=ceil(len/2),在长度为偶数时则要求两个第k小的数再求他们的平均值,即k1=len/2,k2=len/2+1
说一下为啥我把特判长度为0写在最头部显得很臃肿,因为中位数的性质可以发现采用暴力方法时我们只需要遍历nums1与nums2长度和的一半个元素即可.
换句话说,除非两个数组全空,否则不可能出现两个数组同时到达末尾的情况.而题目已经把两个都空的情况排除了,所以只需要考虑一个空的情况
如果不像我这样把特判长度0写在最前面,那么就必须在循环里进行至少两个if去判断究竟是哪个数组走到了末尾,个人觉得这样的开销没有必要,
像现在这样用或判断数组越界结合长度0特判可以省掉很多无谓的if,给暴力这种原始方法带来些微的优化.
时间复杂度O((m+n)/2)=O(n),空间复杂度O(1)
执行错误若干次: 一开始没有很好的考虑某个数组空的情况,或者写法太不优雅导致难以检查出现了写错数组名的低级错误.
结果:
结果意外的还不错,可以发现LeetCode没有用数据去卡我们.只是题面要求做到O(log(n))时间复杂度而已.
官方题解:
https://leetcode-cn.com/problems/median-of-two-sorted-arrays/solution/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-shu-b/
方法: 递归法(?,个人感觉跟递归没关系,是一种分治的思想)
我之前理解的中位数是第k小/第k大的数(取决于从哪个方向去看),这个题解里对中位数有了更妙的理解
官方题解写的很好,但是很多地方可能有点绕,不那么通俗,我来尝试通俗的解释一下.
为了方便,我们首先假设两个数组的长度m和n均是偶数(且不同时为0),一奇一偶和双奇数长度的情况我们在讨论完双偶数后对其适当变形即可
偶数长度个数的中位数是按照从小到大顺序排列的最中间两个数的平均,那么我们这样想: 在这两个数间画一条竖线,将整个数组分成两个大小相等的部分:
左侧有len/2个数,其中最右侧的最靠近这条竖线的数是要用来计算出中位数的一个数,类似的,右侧有len/2个数,其中最左侧的最靠近这条竖线的数也是要用来计算中位数的一个数
我们把左侧的半个数组称为left,右侧的称为right
由于整个数组都是有序的,遵循从小到大排的规则,那么left的最右侧的数就是left中所有数的最大值,right中的最左侧的数就是right中所有数的最小值.
因此整个数组的中位数就可以表示为 (max(left)+min(right))/2,这样表示有什么好处呢?
它实质上取消了left和right各自的有序性要求,即弱化了数组的有序性要求(注意不是取消了有序性要求),现在我们不需要保证整个数组都有序了!
我们只需要保证两个部分间有序即可,不必考虑内部: 被称作right的部分中的任意数大于等于被称作left的部分中的所有数即可!
例如,我们获得了一个无序的数组[6,4,5,3,1,2],但是不难发现如果拆分成[6,4,5][3,1,2]这两个部分就满足了我们前面所叙述的要求.
right = [6,4,5],left = [3,1,2](注意left指较小的部分,与这个部分实际物理上的"左"还是"右"无关,right同理),因此整个数组的中位数就应当是(4+3)/2 = 3.5! 容易验证这确是正确答案.
但是注意到我们现在都在针对一个数组来说的,我们现在有nums1,nums2两个数组,因此要把他们"合起来",
此处不是指物理上合并起来,而是说把它们当成一个数组考虑: 把nums1拆成两部分left_1与right_1,把nums2拆成两部分left_2与right_2,再把这四部分两两结合,
left_1与left_2结合成为虚构的整体数组的left,right_1与right_2结合成为虚构的整体数组的right,而且这个left和这个right满足我们前文的叙述.
当两数组均非空时可以证明必然存在这样的划分: 前文我们已经证明了一个弱化有序的数组是可以通过拆分方法求中位数的,显而易见,两个独立的有序数组必然可以经过O(m+n)的时间复杂度合并(此处指物理合并了,归并排序中那种合并)为一个有序的数组,而弱化有序是有序的必要条件,因此可知这个合并后的数组必然可以通过拆分法求中位数.拆分法中的left中任意数都小于等于right中的所有数,且left中的任意数不是来自nums1就是来自nums2,把left中的所有来自nums1的数组成的集合就是left_1,按照此位置拆分nums1即可.nums2同理,不再赘述.
让我来举个例子,不妨令 nums1 = [-2,1,3,3,4,5,6,17,18,19], nums2 = [0,1,4,4,8,9],他们的长度分别是10和6,正确的中位数应当是(4+4)/2=4,那么我们期望做这样一件事情:
step1. nums1 => left_1=[-2,1,3,3] ,right_1=[4,5,6,17,18,19]
step2. nums2 => left_2=[0,1,4,4] ,right_2=[8,9]
step3. left = left_1 并 left_2 = [-2,1,3,3
0,1,4,4] , left长8
step4. right = right_1 并 right_2 = [4,5,6,17,18,19
8,9] , right长8
(step4.5. 验证right中任意数确大于等于left中所有数)
step5. 中位数 = (max(left)+min(right))/2 = (4+4)/2 = 4 ✔
到此为止,我们将问题转换成了: 如何找到正确的划分的位置,使得step4.5中的验证能正确?
我们用朴素的思维想一个暴力的想法: nums1的长度为m,根据简单的排列组合知识,砍一刀把它分成两个部分有C(1,m+1) = m+1 种方法,类似的nums2有n+1种拆分方法
我们把nums1拆分的位置称为i,nums2拆分的位置称为j,i属于[0,m],j属于[0,n],因此似乎我们只需要开两重循环暴力找到i和j就可以了?
慢着,我们这样浪费了太多信息了,而且复杂度也高达O(n2).别忘了left和right是等长的!在我们的假设里(m与n均为偶数)不难发现i与j的关系:
left长度 = right长度
i + j = m - i + n - j
即j = (m + n - 2*i)/2,假设保证了他是一个整数.注意到i与j的关系为i增j减,i减j增.为了使j是一个正数,我们应当使n大于等于m
因此我们减少了一重循环,只需要开i的循环即可.复杂度降到了O(n).到了这个时候就不难看出题目要求的O(log(n))应该在哪里二分了,就是在i的搜索中.
我们已知i属于[0,m],那么我们首先查找i=m/2时是否满足"弱化有序"的条件.因为我们从原来真正有序的数组上拆分,因此left_1天然的对right_1满足弱化有序.同理left_2对right_2也是.
所以我们只需要手动检查:
1. left_1中的最大值是否小于right_2中的最小值 下简称条件1
2. left_2中的最大值是否小于right_1中的最小值 下简称条件2
之后选择二分的[0,m/2)部分还是[m/2,m]部分呢?
我们考察一下i=m/2时,如果不满足条件1,说明划给left_1的数太多了,即i太大了,因此应当减小i,选择[0,m/2)部分继续二分
如果不满足条件2,说明划给left_2的数太多了,即j太大了,由i与j的关系,即i太小了,因此应当放大i,选择[m/2,m]部分继续二分
至此形成了完整的思路了,代码稍后奉上.
1 class Solution { 2 public double findMedianSortedArrays(int[] nums1, int[] nums2) { 3 int m = nums1.length; 4 int n = nums2.length; 5 if(n<m){ 6 int[] temp = nums1; 7 nums1 = nums2; 8 nums2 = temp; 9 int tempnum = m; 10 m = n; 11 n = tempnum; 12 } 13 int i, j; 14 int up_limit = m; 15 int down_limit = 0; 16 while (true) { 17 i = (up_limit + down_limit) / 2; 18 j = (m + n - 2 * i) / 2; 19 if (((0 < i && i <= m) ? nums1[i - 1] : Integer.MIN_VALUE) <= ((0 <= j && j < n) ? nums2[j] 20 : Integer.MAX_VALUE)) { 21 if (((0 < j && j <= n) ? nums2[j - 1] : Integer.MIN_VALUE) <= ((0 <= i && i < m) ? nums1[i] 22 : Integer.MAX_VALUE)) { 23 break; 24 } else { 25 down_limit = i + 1; 26 } 27 } else { 28 up_limit = i - 1; 29 } 30 } 31 if((m+n)%2==0) 32 return (Math.max(((0 < i && i <= m) ? nums1[i - 1] : Integer.MIN_VALUE), 33 ((0 < j && j <= n) ? nums2[j - 1] : Integer.MIN_VALUE)) 34 + Math.min(((0 <= j && j < n) ? nums2[j] : Integer.MAX_VALUE), 35 ((0 <= i && i < m) ? nums1[i] : Integer.MAX_VALUE))) 36 / 2.0; 37 else 38 return Math.min(((0 <= j && j < n) ? nums2[j] : Integer.MAX_VALUE), 39 ((0 <= i && i < m) ? nums1[i] : Integer.MAX_VALUE)); 40 } 41 }
时间复杂度O(log(n)),空间复杂度O(1).
结果:
具体解说与注释明天补上,累了先休息了.