题目详情
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
示例 1:
nums1 = [1, 3] nums2 = [2]
则中位数是 2.0 示例 2:
nums1 = [1, 2] nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5
来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/me… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
方法一
首先可以换一个角度来思考这个问题,把问题换做如何使用用寻找两个有序数组的第 K (本文的 K 的值可以为0,在K = 0 的时候,表示的就是最小值)小数的方式来解决这个问题。假设两个有序数组中一个有序数组 nums1 的长度为 M,另外一个有序数组 nums2 的长度为 N。此两个有序数组的中位数有如下两种情况:
- 在
M + N的长度为奇数的时候,则中位数是两个有序数组中第(M + N + 1)/2小的数 - 在
M + N的长度为偶数的时候,则中位数是两个有序数组的第(M + N + 1)/2小的数和第(M + N + 1)/2 + 1小的数之和除以2。
那么如何寻找两个有序数组的第 K 小的数呢?可以假设前 K 小的数的一半分布于数组 nums1,另外一半分布于数组 nums2. 那么整体分布可能如下图
那么这个过程是如何结束的呢?
- 在一个较短的数组 nums1 的长度为零的时候,则第 K 小的数就是较长数组的 nums2[K]。
- 在寻找的第 K 小的数的时候,如果 K = 0 则表示返回两个有序数组中的两个最小数之中的最小数即可。
额外的需要注意的就是较短数组中的第 K/2 小的数可能会超过数组的边界。假如在一个数组的长度为1,另外一个数组长度为4的时候,那么在假设按照上图平均分配前K小的数于两个数组中是不可能的,因为数组1的长度仅仅为1。
全部的代码内容如下,代码和上述描述有些不同,没有切除数组,而是使用索引代表此时需要搜索的数组边界。
func FindMedianSortedArraysAnswer(nums1, nums2 []int) float64 {
m, n := len(nums1), len(nums2)
idx, remain := (m+n-1)/2, (m+n-1)%2
if remain == 0 {
return float64(findKth(nums1, nums2, 0, m-1, 0, n-1, idx))
}
return float64(findKth(nums1, nums2, 0, m-1, 0, n-1, idx)+findKth(nums1, nums2, 0, m-1, 0, n-1, idx+1)) / 2
}
func findKth(nums1, nums2 []int, start1, end1, start2, end2, k int) int {
// nums1 处理的都是较短的数组
if end1-start1 > end2-start2 {
return findKth(nums2, nums1, start2, end2, start1, end1, k)
}
// 情况1. 数组 nums1 的长度为0
if start1 > end1 {
return nums2[start2+k]
}
// 情况2. 找到最小的数即可
if k == 0 {
return min(nums1[start1], nums2[start2])
}
// 注意点,较短的数组可能会越界
mid1 := min(end1, start1+((k+1)/2)-1)
mid2 := start2+((k+1)/2)-1
if nums1[mid1] > nums2[mid2] {
return findKth(nums1, nums2, start1, end1, mid2+1, end2, k-(mid2-start2+1))
}
return findKth(nums1, nums2, mid1+1, end1, start2, end2, k-(mid1-start1+1))
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
方法二
方法二比较直接,而且有些难懂,并且需要纠结的细节也比较多。
如果把两个数组放到一起考虑的话,那么整合在一起的有序数组中,中位数左侧的数据就会一部分分布在数组nums1,另外一部分分布在数组nums2;中位数右侧的也是这样的,一部分分布在数组nums1,另外一部分分布在数组nums2。这里面说的一部分是要划重点的,可能是 0 的。
如果数组num1表示为[a1,a2,a3,...an],数组nums2表示为[b1,b2,b3,...bn],那么组成的有序数组就可能是[nums1[:left1],nums2[:left2] | nums1[left1:], nums2[left2:]]。表达式中的|表示的一个分割符,把两个数组按照大小切割为左右两部分,这个就是下面常说的边界。整体来说左侧的所有值都是小于右侧的所有值的。
那么如果找到分割的边界呢?如果找到一个数组的边界,那么久可以找到另外一个数组的边界。因为根据两个数组的长度,可以找到分布于中位数左侧值的所有个数的。
那么如何找到一个数组的边界呢?这个时候就到了关键的一步了。回顾下两个有序数组的整体分布,是不是应该这样[nums1[:left1],nums2[:left2] | nums1[left1:], nums2[left2:]]?在不超越边界的情况下nums1[left1-1] <= nums1[left1]和nums2[left2-1] <= nums2[left2]是必然的,因为两个数组都是有序的。那么还需要满足的条件就是nums1[left1-1] <= nums2[left2]和nums2[left2-1] <= nums1[left1]。这个条件看似有两个,其实整合为一个就好了。比如说nums2[left2-1] <= nums1[left1],先找到了满足条件的left1和left2,但是呢left1左侧还是可能有值满足条件nums2[left2-1] <= nums1[left1]。那么如果找到了nums1最左侧的一个边界left1,可以满足条件nums1[left1] >= nums2[left2-1],此时如果left1如果再次减一,则此条件就不应该满足了,所以nums1[left1-1] < nums2[left2+1],则nums1[left1-1] <= nums2[left2]。这个时候也就两个条件都满足了,所以在找到了最小的left1满足条件nums1[left1-1] < nums2[left2]的时候,left1和left2正好对两个数组分割,展示此部分代码如下:
k := (m + n + 1) / 2
left, right := 0, m
for left < right {
mid1 := (right - left)/2 +left
mid2 := k - mid1
if nums1[mid1] < nums2[mid2-1] {
left = mid1 + 1
} else {
right = mid1
}
}
先说说代码的含义吧,其中变量left表示的是nums1中的可以选择的边界,而right也是nums1的边界,只不过是不可以选择的。那么循环可以执行的条件就必然是left < right了。然后就可以找到nums1的边界了,,
nums2的边界就是 ,这个含义是
nums1中有mid1个数是位于边界的左侧的,nums2中位于边界的左侧则有mid2个数。在条件下,表明
nums1中左侧的数太多了,以至于nums1中位于边界右侧的最小值比nums2中位于边界左侧的最大值要小,这个肯定是不满足条件的,所以需要left = mid + 1。
需要仔细思考的是当满足条件nums1[mid1] >= nums2[mid2-1]的时候,为什么right = mid1了?上段中开头就说了,right表示的是不可选择的右侧边界。如果right = mid1了,那么假如mid1正好是分割nums1的边界呢???这个其实是没有问题的,这种时候后续的条件就都是nums1[mid1] < nums2[nums2-1]了,所以left会一直增长,直到left = right = mid1(这个 mid1 是上面假设的边界)。
如果提出一个分割两个有序数组的函数,那么也可以如下的:
func DivideSortedArrays(nums1, nums2 []int) (int, int) {
m, n := len(nums1), len(nums2)
k := (m + n + 1) / 2
left, right := 0, m
for left < right {
mid1 := (right - left) / 2 + left
mid2 := k - mid1
if nums1[mid1] < nums2[mid2 - 1] {
if left == mid1 {
return left, k - left - 2
}
left = mid1
} else {
right = mid1
}
}
return right-1, k - right - 1
}
上面函数的具体含义大家如果感兴趣的话可以自己思考下,有问题可以留言,我就不具体介绍了。
下面就是整体的操作过程了。
import "math"
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
m, n := len(nums1), len(nums2)
if m > n {
// 选择长度较小的数组进行操作的原因是,数组更短,二分搜索操作的次数可以更少
return findMedianSortedArrays(nums2, nums1)
}
k := (m + n + 1) / 2
left, right := 0, m
for left < right {
nums1Mid := (right - left) / 2 + left
nums2Mid := k - nums1Mid
if nums1[nums1Mid] < nums2[nums2Mid - 1] {
left = nums1Mid + 1
} else {
right = nums1Mid
}
}
nums1Mid, nums2Mid := left, k - left
nums1LeftMargin := math.MinInt64
nums2LeftMargin := math.MinInt64
if nums1Mid > 0 {
nums1LeftMargin = nums1[nums1Mid - 1]
}
if nums2Mid > 0 {
nums2LeftMargin = nums2[nums2Mid - 1]
}
if (m + n + 1) % 2 == 0{
return float64(max(nums1LeftMargin, nums2LeftMargin))
}
nums2RightMargin := math.MaxInt64
nums1RightMargin := math.MaxInt64
if nums1Mid < m {
nums1RightMargin = nums1[nums1Mid]
}
if nums2Mid < n {
nums2RightMargin = nums2[nums2Mid]
}
return float64(max(nums1LeftMargin, nums2LeftMargin)+ min(nums1RightMargin, nums2RightMargin)) / 2
}
func max(a, b int) int {
if a >= b {
return a
}
return b
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}