Leetcode_4
-
问题描述
详见Leetcode官网
补充说明:
- 中位数的定义如下:是按顺序排列的一组数据中居于中间位置的数,代表一个样本、种群或概率分布中的一个数值,其可将数值集合划分为相等的上下两部分。对于有限的数集,可以通过把所有观察值高低排序后找出正中间的一个作为中位数。如果观察值有偶数个,通常取最中间的两个数值的平均数作为中位数。
-
算法一
-
思路:
合并两个有序数组,在合并后的新数组中根据中位数的定义找出所求的中位数(需要分奇数和偶数讨论)
时间复杂度: O ( m + n ) O(m+n) O(m+n)
-
c#源代码:
public class Solution { public double FindMedianSortedArrays(int[] nums1, int[] nums2) { int m=nums1.Length; int n=nums2.Length; int[] res=new int[m+n];//合并后的新数组 int i=0; int j=0; int k=0; //合并两个有序数组的循环体,可以参考 归并排序 while(i<m&&j<n) { if(nums1[i]<nums2[j]){ res[k++]=nums1[i++]; //先用当前值,然后再自加1 } else{ res[k++]=nums2[j++]; } } while(i<m){ res[k++]=nums1[i++]; } while(j<n){ res[k++]=nums2[j++]; } int r=res.Length; //考虑合并后只有一个元素的情况 if(r==1){ return res[0]; } //合并后的数组元素个数为奇数,注意类型转换 else if(r%2==1){ return (double)res[r/2]; } //合并后的数组元素个数为偶数,注意类型转换 else{ return (res[r/2]+res[r/2-1])/2.0; } } }
-
战绩:
执行用时: 124 ms,c#中超过100% 内存消耗: 27.9 MB,c#中超过38%
-
-
算法二
-
思路:
-
根据中位数的数学定义,粗略地说,它是位于一个集合正中间的那个数,也就是说,中位数将一个集合分成两个部分,这两个部分 元素个数相等,且左边的元素均 “ ≤ \leq ≤” 右边的元素。
-
由此可知,对于集合中元素个数为奇数的情况,(若将中位数划归到左边部分),则左边部分的元素个数为:
m + n + 1 2 (1) \frac{m+n+1}{2}\tag{1} 2m+n+1(1)
对于集合中元素个数为偶数的情况,显然左边部分的元素个数为:
m + n 2 (2) \frac{m+n}{2}\tag{2} 2m+n(2)
由于计算机中整数除法是向下取整 的,所以可以将两个 公式合并为 ( 1 ) (1) (1)式。注意:
- 考虑到
(
m
+
n
)
(m + n)
(m+n)可能会导致 整数溢出 ,因此可以通过改写
(
1
)
(1)
(1)为如下公式避免。(但本题中有0 <= m <= 1000,0 <= n <= 1000的提示,所以可以 不做变换)
m + n − m + 1 2 m+\frac{n-m+1}{2} m+2n−m+1
- 考虑到
(
m
+
n
)
(m + n)
(m+n)可能会导致 整数溢出 ,因此可以通过改写
(
1
)
(1)
(1)为如下公式避免。(但本题中有0 <= m <= 1000,0 <= n <= 1000的提示,所以可以 不做变换)
-
由第一、二步的结论,迁移到本题中,为了不额外开辟存储空间,就形成了一个新的定义:
中位线:用两条分割线,将本题中两个正序数组(nums1,nums2)分成两部分,使得这两条分割线的左边部分 包含 m + n + 1 2 \frac{m+n+1}{2} 2m+n+1个元素,(且为了保证左边部分的元素都 ≤ \leq ≤ 右边部分的元素,充分利用正序数组的性质):
nums1分割线的左边第一个元素 ≤ \leq ≤ nums2分割线的右边第一个元素;KaTeX parse error: \tag works only in display equations
nums2分割线的左边第一个元素 ≤ \leq ≤ nums1分割线的右边第一个元素。KaTeX parse error: \tag works only in display equations
这样的两条分割线,我将它们统称为中位线 (图解)
【注意:在编程中,我们将每个数组分割线的右边第一个元素的数组索引用来表示分割线的位置】
-
前面三步都是本算法理论的推导,并且第三步在本算法中的重要性是不言而喻的:
通过第三步,问题转化为:只要找到一个正序数组中的分割线的位置,就可以通过上面的公式(1),找到另一个数组中分割线的位置(通过条件(3)、(4));
这里不难发现一个降低时间复杂度的技巧:选择长度小的数组,找到其中的分割线
-
这里我详细说一下用 二分法 找长度小的正序数组(不妨设为nums1)分割线的算法流程:
先来定义如下一些变量:
- [ l e f t , r i g h t ] [left,right] [left,right]为二分法的查找区间,其中left,right均为整数(int);
- 整型变量(int)a表示nums1中的 可能 分割线位置;
- 整型变量(int)b表示nums2中的 可能 分割线位置;
【思考:1、为什么left和right的初始值分别为0和m?(定义+索引)
2、为什么条件(3)、(4)在上面的流程图中只有一个被用作二分查找的调整判断?】
-
找到中位线(a,b)后,一切就变得简单了:
若(m+n)是***奇数***,则取中位线左边两个元素的最大值 ,即为所求中位数;
若(m+n)是***偶数***,则取中位线左边两个元素的最大值 与中位线右边两个元素的最小值 的平均值 ,即为所求中位数。(中位数的定义可推得)
注意:
- 若nums1中的分割线 a 为 0 或 m ,则需要在边界上做一些等价处理(不能用数组元素实现中位数的提取,需要增加整数的拷贝)
-
时间复杂度:
O ( l o g m i n ( m , n ) ) O(logmin(m,n)) O(logmin(m,n))
-
-
c#源代码
public class Solution { public double FindMedianSortedArrays(int[] nums1, int[] nums2) { //通过递归将两个数组中长度较小的,设定为要进行二分查找的数组 if (nums1.Length > nums2.Length) { return FindMedianSortedArrays(nums2, nums1); } //m记录短数组的长度,n记录长数组的长度 int m = nums1.Length; int n = nums2.Length; //bin用来记录分割线左边元素个数(两个数组),这里默认将中位数划归到分割线左边 int bin = (m + n + 1) / 2; int left = 0; int right = m; while (left < right) { //定义nums1中 可能 分割线的位置a int a = (left + right + 1) / 2; //注意:这里的“+1”是为了避免提取nums1元素时,索引越界,比如后面的nums1[a-1];显然nums2不会越界,因为nums2.Length>=nums1.Length //定义nums2中 可能 分割线的位置b int b = bin - a; //对nums1进行 二分查找 if (nums1[a - 1] < nums2[b]) { left = a; } else { right = a - 1; } } //不能用数组元素实现中位数的提取,需要增加整数的拷贝 //nums1中的分割线的拷贝 int i = right; //nums2中的分割线的拷贝 int j = bin - i; //需要考虑中位线的某个分割线左边或右边 无元素,因此需要做一些 等价补充 int ileft = i == 0 ? int.MinValue : nums1[i - 1]; int iright = i == m ? int.MaxValue : nums1[i]; int jleft = j == 0 ? int.MinValue : nums2[j - 1]; int jright = j == n ? int.MaxValue : nums2[j]; //对结果的选取需要分 奇数和偶数两种情况讨论 //注意 double类型转换 if ((m + n) % 2 == 1) { return (double)Math.Max(ileft, jleft); } else { return (Math.Max(ileft, jleft) + Math.Min(iright, jright)) / 2.0; } } }
-
战绩
执行用时:128 ms,在所有 C# 提交中击败了98.21%的用户 内存消耗:27.5 MB,在所有 C# 提交中击败了87.09%的用户
-
-
总结
本题作为一道hard题,其本身的问题如果使用归并排序中 合并两个有序数组的算法,很容易解决,但时间、空间复杂度较高;
本题难就难在它的进阶问题,在不创建新数组的前提下通过二分法完成中位数的查找,并使时间复杂度降到了 O ( l o g m i n ( m , n ) ) O(logmin(m,n)) O(logmin(m,n)),同时空间复杂度也有了显著的降低。**这里涉及到了对中位数 数学定义的迁移(例如,算法2 以中位数的定义为基础,创造了中位线的概念),以及各种边界条件、特殊情况的处理,最后还要注意元素个数为奇数和偶数的差别。**十分考验编码者的数学思想,创新思维,以及严谨程度。
总之,是一道既能巩固 数据结构与算法 知识(归并排序),又能 使大脑开窍的 不可多得之好题!