leetcode算法题:寻找两个正序数组的中位数 java&python解法
题目概述:
解题思路:
首先,题目是两个有序数组,然后寻找这两个数组中的中位数,我们如果用手算的话,肯定是把两个数组合并成一个,然后算其中的中位数。这是比较简单的解法,但是这道题是困难题目啊,不可能说用双指针合并一下就算完成任务了,这样并没有达到题目要求的log级的时间复杂度。但是不管怎么样,有解法总比没头绪好,我先从O(m+n)级别的时间复杂度来解这道题。
双指针遍历合并数组取中位数 思路:
1.如何合并数组?由于两个数组是从小到大有序的,我们需要将他们合并成一个同样有序的数组,双指针的方法就很方便了,我们可以定义两个指针,分别指向数组的第一个元素,比较两个指针所指向的值的大小,将值小的呢个数放进新的数组里,然后把当前指针后移一位,继续通过while循环进行比较。
2.while循环的终止条件?我们既然使用双指针遍历,指针当然不能越界,这就是中止条件,其中如果出现一个数组遍历完了,另外一个数组还有元素,我们只需要把剩下的元素放进新数组里就可以了。所以中止条件就是两个指针不能越界,而且是或的关系。
3.如何在新数组中找到中位数?这就很简单了,判断数组长度是奇数还是偶数个,如果是奇数,就取length/2呢个,如果是偶数,就取中间两个/2就ok了
这个算法简单易懂,但是时间复杂度达到O(m+n),由于我们申请了新数组,空间复杂度也达到了O(n),没有达到题目的要求,但是我们可以再此代码上优化,将空间复杂度降到O(1),也就是我后序要说的改进算法。
双指针代码比较简单,python代码我就不提供了,如果有需要可以评论,我会及时更新。
java代码实现 双指针遍历:
public static double findMedianSortedArrays(int [] nums1,int[] nums2) {
//定义双指针
int point1 = 0;
int point2 = 0;
//创建新数组
ArrayList<Integer> result = new ArrayList<Integer>();
//双指针遍历
while (point1 < nums1.length || point2 < nums2.length) {
//一个数组遍历结束,另一个数组还有元素
if (point1 == nums1.length) {
result.add(nums2[point2++]);
continue;
}
if (point2 == nums2.length) {
result.add(nums1[point1++]);
continue;
}
//将较小的值放入新数组
if (nums1[point1] <= nums2[point2]) {
result.add(nums1[point1++]);
} else {
result.add(nums2[point2++]);
}
}
//新数组的长度
int length = result.size();
//取中位数
if (length < 2) {
return length == 0 ? 0 : result.get(0);
}
if (length % 2 == 0) {
return (double) (result.get(length / 2 - 1) + result.get(length / 2)) / 2;
}
return result.get((length + 1) / 2 - 1);
}
上述已经提到,此算法的时间复杂度和空间复杂度对于这道题目来说都比较高,所以我们可以对此代码进行优化,因为双指针算法无法达到log级,呢我们就优化空间复杂度到O(1)
双指针算法优化思路:
1.我们如果要降低空间复杂度到O(1),呢就不需要取创建新数组,我们既然有两个指针去遍历,呢我们用变量去存储较小的值就好了。
2.我们也不需要去遍历完整的两个数组,因为是有序的,我们只需要遍历两个数组的长度之和/2就好了,遍历完成之后,我们用来存储值的变量就是我们需要的中位数。
3.偶数长度有两个值怎么办?我们可以使用两个变量去存储值,left和right 用right去存储较小的值,每次遍历都将right赋值给left,这样的话,如果有两个值,就一定是left和right。
这样的话我们只申请了几个变量,空间复杂度位常数阶O(1) ,时间复杂度虽然缩短了一半为O((m+n)/2+1)但同样还是O(m+n)。如何才能达到题目要求的log级别呢?我们首先就应该想到的是二分法,通过每次排除掉一半的元素 实现log(m+n)的时间复杂度,后面会有二分法的解法
java代码实现 双指针遍历优化:
public static double findMedianSortedArrays(int [] nums1,int[] nums2) {
//定义双指针
int point1 = 0;
int point2 = 0;
//两个变量用来存储值
int left = 0;
int right= 0;
//两个数组长度
int length = nums1.length+nums2.length;
//遍历length/2次
for (int i = 0; i <= length/2 ; i++) {
//更新left的值
left = right;
//以下的代码就和双指针大同小异了,就是把较小的值用right存起来
if (point1==nums1.length){
right = nums2[point2++];
continue;
}
if (point2==nums2.length){
right = nums1[point1++];
continue;
}
if (nums1[point1]<=nums2[point2]){
right = nums1[point1++];
}else {
right = nums2[point2++];
}
}
//判断长度奇偶性
if (length %2 == 0){
return (double)(left+right)/2;
}
return right;
}
Python代码实现 双指针遍历优化:
def findMedianSortedArrays(nums1: list, nums2: list) -> float:
# 定义双指针
point1 = 0
point2 = 0
# 两个变量存值
left = 0
right = 0
# 长度和
length = len(nums1) + len(nums2)
# 遍历length//2次
for _ in range(length // 2 + 1):
# 更新left的值
left = right
# 将较小的值存到right中
if point1 == len(nums1):
right = nums2[point2]
point2 += 1
continue
if point2 == len(nums2):
right = nums1[point1]
point1 += 1
continue
if nums1[point1] <= nums2[point2]:
right = nums1[point1]
point1 += 1
else:
right = nums2[point2]
point2 += 1
# 取中位数
if length % 2 == 0:
return (left + right) / 2
return right
二分法 思路:
题目中要求时间复杂度达到log级,我们就用二分法来解决它。核心问题就是怎么进行二分?从哪里分割?
根据题意,我们需要找到中位数,换句话说,我们需要找到有序数组中第 length/2 小的元素。两者同理,这样的话,我们可以进行二分了,我通俗易懂的讲,如果两个数组的长度都为6,一共长度为12,我们要找第12/2 = 6小的元素(先不考虑奇数长度),呢我们就可以比较两个数组的第 6/2=3个元素,为什么要取3呢?
我们来举个例子
两个数组 list1 = [1,2,3,7,8,9] list2 = [4,5,6,7,8,9]
我们把3和6比较,3<6 我们就可以把 list1中的1,2,3直接排除掉,因为3不可能是第6小的元素,3比6小,就算是极端情况,6之前的两个位置都比3小,3也是第5小的元素,所以可以直接把1,2,3排除掉。
更精确的来算的话,比如说我们要找第x小的元素,我们比较的就是list1[x/2]和list2[x/2]两个元素,如果list1[x/2]比较小,这两个元素前面各有x/2-1个元素,x/2-1+x/2-1 = x-2 我们需要找的是第x小的元素,但前面只有x-2个元素,加上list1[x/2]也才x-1个元素,所以list1前x/2个元素都可以排除掉。因为排除了一部分元素,需要把x做调整,x = x-被排除的元素个数 。然后我们再重新二分,再将x/2,将较小的元素以及它前面的元素剔除掉,直到x/2=1 我们需要找第1小的元素为止。 需要注意的是一些细节处理很重要!
我在此提供递归和非递归的两种代码,分别用python和java实现
java代码实现 正常二分法 非递归:
public static double findMedianSortedArrays(int [] nums1,int[] nums2) {
int length = nums1.length + nums2.length;
//如果是奇数,则只需要找到第length/2+1小的数
if (length % 2 == 1) {
int needEle = length / 2;
return getX(nums1, nums2, needEle + 1);
}
// 偶数需要找到 第 length/2+1 和 length/2 小的数
int needEle = length / 2 - 1, midIndex2 = length / 2;
return (getX(nums1, nums2, needEle + 1) + getX(nums1, nums2, midIndex2 + 1))/2.0;
}
public static int getX(int[] nums1, int[] nums2, int x) {
//定义双指针
int point1 = 0;
int point2 = 0;
// while循环二分
while (true) {
//终止条件
if (x == 1) {
return nums1[point1]>nums2[point2]?nums2[point2]:nums1[point1];
}
// 我们需要处理一些极端边界条件
if (point1 == nums1.length) {
return nums2[point2 + x - 1];
}
if (point2 == nums2.length) {
return nums1[point1 + x - 1];
}
//二分,如果x/2大于数组长度,我们取数组长度即可
int compare1 = point1 + x / 2 > nums1.length ? nums1.length : point1 + x / 2;
int compare2 = point2 + x / 2 > nums2.length ? nums2.length : point2 + x / 2;
//比较两个数,将小的元素 之前的元素全部排除,调整x的值
if (nums1[compare1] <= nums2[compare2]) {
x -= (compare1 - point1 + 1);
point1 = compare1 + 1;
} else {
x -= (compare2 - point2 + 1);
point2 = compare2 + 1;
}
}
}
python代码实现 二分法+递归
def findMedianSortedArrays(self, nums1: list, nums2: list) -> float:
# 我们可以把奇数和偶数情况合并,也就说都要找两次,只不过奇数情况是找两个同样的数
left = (len(nums1) + len(nums2) + 1) // 2
right = (len(nums1) + len(nums2) + 2) // 2
def get_x(nums1, nums2, x):
# 保证如果有空数组一定是nums1,且保证nums1最短
if len(nums1) > len(nums2):
return get_x(nums2,nums1,x)
# 边界条件处理
if len(nums1) == 0:
return nums2[x - 1]
# 终止条件处理
if x == 1:
return min(nums1[0], nums2[0])
# 保证索引不越界
compare = min(x // 2, len(nums1))
if nums1[compare - 1] <= nums2[compare - 1]:
return get_x(nums1[compare:], nums2, x - compare)
else:
return get_x(nums1, nums2[compare:], x - compare)
return (get_x(nums1, nums2, left) + get_x(nums1, nums2, right)) / 2