4. 寻找两个正序数组的中位数
一、问题描述
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。要求找出并返回这两个正序数组的中位数。算法的时间复杂度应该为
O
(
l
o
g
(
m
+
n
)
)
O(log (m+n))
O(log(m+n))。
二、示例说明
- 输入:
nums1 = [1,3]
,nums2 = [2]
,输出:2.00000
。- 合并数组为
[1,2,3]
,中位数是 2。
- 合并数组为
- 输入:
nums1 = [1,2]
,nums2 = [3,4]
,输出:2.50000
。- 合并数组为
[1,2,3,4]
,中位数是 ( 2 + 3 ) / 2 = 2.5 (2 + 3) / 2 = 2.5 (2+3)/2=2.5。
- 合并数组为
三、提示信息
nums1.length == m
:表示数组nums1
的长度为m
。nums2.length == n
:表示数组nums2
的长度为n
。0 <= m <= 1000
:数组nums1
的长度范围。0 <= n <= 1000
:数组nums2
的长度范围。1 <= m + n <= 2000
:两个数组长度之和的范围。-106 <= nums1[i], nums2[i] <= 106
:数组中元素的取值范围。
四、解题思路
- 为了满足时间复杂度为 O ( l o g ( m + n ) ) O(log (m+n)) O(log(m+n)),可以使用二分查找的思想。
- 假设两个数组的总长度为
len
,如果len
是奇数,则中位数是第k
个数(如果len
是偶数,中位数是第k
和第k + 1
数的平均值)。 - 以上都是基于假设将两个数组按照正序结合,实际上如果真的结合,这一步的时间复杂度就已经超了。怎么不用真的结合来达成目的?
- 通过在两个数组中分别进行二分查找,确定一个划分位置,使得左边的元素个数之和为
k
,并且左边的所有元素都小于等于右边的最小元素。 - 根据划分位置判断中位数在哪个部分,并调整二分查找的范围,直到找到中位数。
以上思路的123都非常清楚,但是具体执行是45,如何实现这个思路?
- 首先确定,我们的整体思路是不断收缩两个数组
的范围来找到第k
小的数,从而确定中位数(这个根据判断len
奇偶很好实现)
下面我们来实现这个逻辑,判断总长度是奇是偶,判断要返回k是第 (total_length // 2 + 1) 小的数,还是(self.findKthSmallest(nums1, nums2, total_length // 2) + self.findKthSmallest(nums1, nums2, total_length // 2 + 1)) / 2
class Solution:
# 处理奇偶数判断的部分,找第`K`小的数的部分交给下一个函数逻辑
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
# 获取两个数组的长度
m = len(nums1)
n = len(nums2)
total_length = m + n
# 如果总长度是奇数,中位数就是第 (total_length // 2 + 1) 小的数
if total_length % 2 == 1: # 奇数
return self.findKthSmallest(nums1, nums2, total_length // 2 + 1)
# 如果总长度是偶数,中位数是第 total_length // 2 和第 total_length // 2 + 1 小的数的平均值
else: # 偶数
return (self.findKthSmallest(nums1, nums2, total_length // 2) + self.findKthSmallest(nums1, nums2, total_length // 2 + 1)) / 2
下面来面临最难的一部分,实现findKthSmallest逻辑:
findKthSmallest
函数的逻辑是在两个有序数组中找到第k
小的数。
具体步骤如下:
-
边界情况处理:
- 如果
nums1
为空数组,那么直接返回nums2
中第k - 1
个元素,因为此时只需要在nums2
中找第k
小的数。 - 同理,如果
nums2
为空数组,返回nums1
中第k - 1
个元素。 - 如果
k
为 1,意味着要找最小的数,比较nums1
和nums2
的第一个元素,返回较小的那个。
- 如果
-
确定比较位置:
- 分别取两个数组中第
k//2
位置的元素,但要确保不超出数组的长度。如果超出了数组的长度,就取该数组的最后一个元素。这里取k//2
位置的元素是为了将问题规模缩小一半。
- 分别取两个数组中第
-
比较并缩小范围:
- 如果
nums1
中第k//2
位置的元素小于nums2
中第k//2
位置的元素,说明nums1
的前k//2
个元素都不可能是第k
小的数,可以把问题转化为在nums1[k//2:]
和nums2
中找第k - k//2
小的数。 - 反之,如果
nums2
中第k//2
位置的元素小于等于nums1
中第k//2
位置的元素,说明nums2
的前k//2
个元素都不可能是第k
小的数,可以把问题转化为在nums1
和nums2[k//2:]
中找第k - k//2
小的数。
- 如果
通过不断地缩小范围,重复这个过程,直到找到第k
小的数或者满足边界条件。这种方法利用了两个数组都是有序的性质,每次都能将问题规模减半,从而实现了时间复杂度为
O
(
l
o
g
(
m
+
n
)
)
O(log(m + n))
O(log(m+n)),其中m
和n
分别是两个数组的长度。
# k是根据上一个函数,根据总长度奇偶来传入的,(奇数)第 k = len // 2 + 1
def findKthSmallest(self, nums1: List[int], nums2: List[int], k: int) -> int:
# 如果第一个数组为空,直接返回第二个数组中第 k 小的数
if not nums1:
return nums2[k - 1]
# 如果第二个数组为空,直接返回第一个数组中第 k 小的数
if not nums2:
return nums1[k - 1]
# 如果 k 为 1,比较两个数组的第一个元素,返回较小的那个
if k == 1:
return min(nums1[0], nums2[0])
一个通俗解释
我们可以把这个问题想象成一个游戏。
假设有两个队伍,队伍 1 是 nums1
,队伍 2 是 nums2
。我们的目标是从这两个队伍中找出第 k
个最厉害的人(也就是第 k
小的数)。
一、边界情况
- 如果队伍 1 没人了(
nums1
为空数组),那我们就直接从队伍 2 里找第k
个厉害的人,也就是返回nums2[k - 1]
。- 就好比队伍 1 弃权了,那我们只需要在队伍 2 中继续比赛,找第
k
个选手就行。
- 就好比队伍 1 弃权了,那我们只需要在队伍 2 中继续比赛,找第
- 同理,如果队伍 2 没人了,就从队伍 1 里找第
k
个厉害的人,返回nums1[k - 1]
。 - 如果我们要找的是最厉害的那个人(
k = 1
),那就比较两个队伍的第一个人,谁更厉害就选谁,也就是返回min(nums1[0], nums2[0])
。- 因为是找最厉害的,所以只需要比较两个队伍的第一个人就行。
二、一般情况
我们想个办法来缩小搜索范围。想象我们把两个队伍分别分成两部分,然后猜测第 k
个厉害的人可能在哪个部分。
- 我们分别从两个队伍中挑出大约
k//2
个人(如果队伍人数不够k//2
,就有多少算多少)。- 比如队伍 1 有 10 个人,队伍 2 有 8 个人,我们要找第 7 个厉害的人,那么就从队伍 1 中挑出前 3 个人(因为
7//2 = 3
),从队伍 2 中挑出前 3 个人。
- 比如队伍 1 有 10 个人,队伍 2 有 8 个人,我们要找第 7 个厉害的人,那么就从队伍 1 中挑出前 3 个人(因为
- 然后比较这两部分人里最厉害的人。
- 假设队伍 1 挑出的这部分人中最厉害的人不如队伍 2 挑出的这部分人中最厉害的人。
- 那就说明队伍 1 中我们挑出的这部分人以及更不厉害的人都不可能是第
k
个厉害的人,所以我们可以把队伍 1 的这部分人排除掉,只在队伍 1 的剩下部分和队伍 2 中继续找第k - k//2
个厉害的人。- 比如我们要找第 7 个厉害的人,现在排除了队伍 1 的前 3 个人,那就变成在队伍 1 的后 7 个人和队伍 2 的 8 个人中找第
7 - 3 = 4
个厉害的人。
- 比如我们要找第 7 个厉害的人,现在排除了队伍 1 的前 3 个人,那就变成在队伍 1 的后 7 个人和队伍 2 的 8 个人中找第
不断重复这个过程,直到找到第 k
个厉害的人。
而找中位数的时候,先判断总长度的奇偶性确定要找第几个数,然后调用找第 k
小的数的方法就可以了。如果总长度是奇数,就找中间那个数;如果是偶数,就找中间两个数求平均。