4. 寻找两个有序数组的中位数

4. 寻找两个有序数组的中位数

4. 寻找两个有序数组的中位数

给定两个大小为 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

分析

对于一个有序数组,如果元素个数为奇数,中位数即中间元素的值;
若元素个数为偶数,中位数为中间两个元素的平均值。
对于两个或多个有序数组,其合并后的中位数并非每个数组中位数的平均值,如:

[1, 3, 5] // 中位数3
[8, 10] // 中位数9
// 合并后的数组
[1, 3, 5, 8, 10] // 中位数5, 并非3和9的平均数

所以,必须对两个数组合并,合并后依然有序

这样朴素实现的时间与空间复杂度均为O(m+n)。

1. 改进朴素实现,不用真的 merge

func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
	m, n := len(nums1), len(nums2)
	pre, cur := -1, -1
	i, j := 0, 0
	for left := (m+n)/2; left >= 0; left-- {
		pre = cur
		if i < m && (j >= n || nums1[i] < nums2[j]) {
			cur = nums1[i]
			i++
		} else {
			cur = nums2[j]
			j++
		}
	}
	if (m+n)%2 == 1 {
		return float64(cur)
	}
	return float64(pre+cur) * 0.5
}

时间复杂度无太大改进,空间复杂度改进为 O(1)。

2. 问题转化为求数组第 k个元素

对于两个数组,假设长度分别是m、n,求合并后的中位数即求:

i. 合并后第(m+n)/2 + 1 个元素(m+n为奇数)
ii. 合并后第(m+n)/2 个元素与第(m+n)/2 + 1个元素的平均值(m+n为偶数)

令 k = (m+n)/2,可以分别取两个数组第k/2个元素,通过比较这两个元素的大小,可以批量地减少搜索范围

1.如果 nums1[k/2] < nums2[k/2], 说明合并后的第 k 个元素肯定不在 nums1[0:k/2+1] 区间里
可以继续在 nums1[k/2+1:] 和 nums2 中搜索第 k-(k/2+1) 个元素
2.反之,可以排除 nums2 的前 k/2 个元素继续搜索

对于总数为奇数和偶数的两种情况需要稍作区分。

时间 O(log(m+n)),空间O(1)

func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
	size := len(nums1) + len(nums2)
	if size == 0 {
		return 0.0
	}
	if size%2 == 1 {
		return getKth(nums1, nums2, size/2+1)
	}
	return (getKth(nums1, nums2, size/2) + getKth(nums1, nums2, size/2+1)) * 0.5
}
func getKth(nums1, nums2 []int, k int) float64 {
	m, n := len(nums1), len(nums2)
	if m > n {
		return getKth(nums2, nums1, k)
	}
	if m == 0 {
		return float64(nums2[k-1])
	}
	if k == 1 {
		return float64(min(nums1[0], nums2[0]))
	}
	i, j := min(m-1, k/2-1), min(n-1, k/2-1)
	if nums1[i] > nums2[j] {
		return getKth(nums1, nums2[j+1:], k-(j+1))
	}
	return getKth(nums1[i+1:], nums2, k-(i+1))
}

3. 二分法

用i,j两个指针将两个数组分别划分为两部分,
将 nums1 的左半部分和 nums2 的左半部分合起来看作合并后的左半部分,
同样可以得到合并后右半部分

                        |
nums1       0, ..., i-1,| i, ..., m-1
                        |
nums2 0, 1, ...,    j-1,| j, ..., n-1
                        |
              左半部分   |  右半部分

如果能保证左右部分的大小相当(m+n为偶数则左右部分大小相等;为奇数则左半部分比右半部分多一个),
也就找到了合并后的中位数

m+n为偶数时:
i+j = m-i + n-j 即i+j = (m+n)/2
m+n为奇数时:
i+j = m-i + n-j + 1也就是 i+j = (m+n+1)/2
因整数除法特性,可以统一为i+j = (m+n+1)/2

注意到确定了i,就确定了j, j = (m+n+1)/2 - i;

数组已排序,用二分搜索法来确定i:

因为两个数组都是有序的,所以 nums1[i-1] <= nums1[i],nums2[i-1] <= nums2[i] 是天然具备的,
所以只需要保证 nums2[j-1] < = nums1[i] 和 nums1[i-1] <= nums2[j];对不满足的情况分两种情况讨论:
nums2[j-1] > nums1[i]
此时需要增加i
nums1[i-1] > nums2[j]
此时要减少i

时间O(log(min(m,n))),空间O(1)

func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
	m, n := len(nums1), len(nums2)
	if m > n {
		// 降低时间复杂度,同时方便后边边界处理
		return findMedianSortedArrays(nums2, nums1)
	}
	lo, hi := 0, m
	for lo <= hi {
		i := lo + (hi-lo)/2
		j := (m+n+1)/2 - i
		if i < m && j > 0 && nums1[i] < nums2[j-1] {
			lo = i + 1
		} else if i > 0 && j < n && nums1[i-1] > nums2[j] {
			hi = i - 1
		} else {
			return cal(nums1, nums2, i)
		}
	}
	return 0
}

func cal(nums1, nums2 []int, i int) float64 {
	m, n := len(nums1), len(nums2)
	j := (m+n+1)/2 - i
	leftMax := 0
	if i == 0 {
		leftMax = nums2[j-1]
	} else if j == 0 {
		leftMax = nums1[i-1]
	} else {
		leftMax = max(nums1[i-1], nums2[j-1])
	}
	if (m+n)%2 == 1 {
		return float64(leftMax)
	}
	
	rightMin := 0
	if i == m {
		rightMin = nums2[j]
	} else if j == n {
		rightMin = nums1[i]
	} else {
		rightMin = min(nums1[i], nums2[j])
	}
	return float64(leftMax+rightMin) * 0.5
}