引言:中位数的「C位之争」
如果把数组比作排队买奶茶的队伍,中位数就是那个站在正中间的幸运儿——不需要知道所有人的位置,只需要找到那个「刚刚好」的中间位置。这个问题看似简单,却藏着算法世界的「效率密码」,尤其是当要求时间复杂度达到O(log(m+n))时,就像要求兔子不仅要跑赢比赛,还要跑出博尔特的速度!
解法等级划分
青铜解法:暴力合并的「小学生算法」
直观思路:把两个数组合并成一个大数组,像小学生合并作业本一样简单粗暴
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {// 合并两个有序数组(像把两堆牌合并成一堆)merged := make([]int, 0, len(nums1) + len(nums2))i, j := 0, 0// 双指针合并(像两个兔子赛跑,谁小谁先走)for i < len(nums1) && j < len(nums2) {if nums1[i] < nums2[j] {merged = append(merged, nums1[i])i++} else {merged = append(merged, nums2[j])j++}}// 处理剩余元素(总有一个兔子先跑到终点)merged = append(merged, nums1[i:]...)merged = append(merged, nums2[j:]...)// 计算中位数(中间位置的幸运儿)n := len(merged)if n % 2 == 1 {return float64(merged[n/2])}return float64(merged[n/2-1] + merged[n/2]) / 2
}
复杂度分析:
- 时间复杂度:O(m+n) - 两个兔子跑完了所有路程,像乌龟一样慢
- 空间复杂度:O(m+n) - 额外数组存储所有元素,堪称「内存土豪」
青铜程序员的痛点:当m和n都是10⁴级别时,这个算法就像让蜗牛参加马拉松——LeetCode直接给你个超时大礼包!
提交结果,好像挺快的
白银解法:双指针的「优化版暴力」
优化思路:不需要完整合并数组,像聪明的兔子只跑一半路程
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {m, n := len(nums1), len(nums2)totalLength := m + nmid := totalLength / 2// 只需要记录中间位置附近的两个值prev, curr := 0, 0i, j := 0, 0for k := 0; k <= mid; k++ {prev = curr// 双指针移动(像两个兔子比赛,谁小谁前进)if i < m && (j >= n || nums1[i] < nums2[j]) {curr = nums1[i]i++} else {curr = nums2[j]j++}}if totalLength % 2 == 1 {return float64(curr)}return float64(prev + curr) / 2
}
复杂度分析:
- 时间复杂度:O((m+n)/2) - 兔子只跑了一半路程,比青铜快了一倍
- 空间复杂度:O(1) - 只需要几个变量,堪称「空间洁癖患者福音」
边界情况专题:当一个数组为空时,就像其中一个兔子请假了,直接返回另一个数组的中位数
提交结果
黄金解法:二分查找的「精准制导」
核心思想:把问题转化为「寻找第k小的元素」,像用导弹精准打击目标位置
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {// 确保nums1是较短的数组,减少二分次数(像让短腿兔子先跑)if len(nums1) > len(nums2) {return findMedianSortedArrays(nums2, nums1)}m, n := len(nums1), len(nums2)totalLeft := (m + n + 1) / 2 // +1确保奇数时取左中位数left, right := 0, mfor left <= right {// i: nums1中取前i个元素,j: nums2中取前j个元素i := left + (right - left) / 2j := totalLeft - i// 边界情况处理(兔子不能跑出跑道)nums1Left := math.MinInt32if i > 0 {nums1Left = nums1[i-1]}nums1Right := math.MaxInt32if i < m {nums1Right = nums1[i]}nums2Left := math.MinInt32if j > 0 {nums2Left = nums2[j-1]}nums2Right := math.MaxInt32if j < n {nums2Right = nums2[j]}// 二分调整(像调整望远镜焦距,直到看清目标)if nums1Left <= nums2Right && nums2Left <= nums1Right {// 找到合适的分割点if (m + n) % 2 == 1 {return float64(max(nums1Left, nums2Left))}return float64(max(nums1Left, nums2Left) + min(nums1Right, nums2Right)) / 2} else if nums1Left > nums2Right {// nums1取多了,左移right = i - 1} else {// nums2取多了,右移left = i + 1}}return 0
}// 辅助函数:取最大值
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
}
复杂度分析:
- 时间复杂度:O(log(min(m,n))) - 像兔子中的博尔特,速度快到模糊
- 空间复杂度:O(1) - 极致优化,没有多余内存占用
黄金程序员的优雅:通过二分查找,我们把两个数组的问题转化为单一数组的查找问题,就像把复杂的爱情问题简化为"你喜欢我还是她"的选择题
提交结果
王者进阶:实战中的性能优化与扩展
内存优化:预分配数组容量,避免动态扩容
// 预分配足够容量,避免动态扩容
func findMedianSortedArraysOptimized(nums1 []int, nums2 []int) float64 {// ... 与黄金解法相同的核心逻辑 ...// 性能优化点:预先计算可能用到的变量totalLength := m + nisOdd := totalLength % 2 == 1// ...
}
并发安全版:在Go并发场景中使用互斥锁
import "sync"var mu sync.Mutexfunc findMedianSortedArraysConcurrent(nums1 []int, nums2 []int) float64 {mu.Lock()defer mu.Unlock()return findMedianSortedArrays(nums1, nums2)
}
算法迁移:二分思想的「七十二变」
二分查找就像算法世界的「瑞士军刀」,除了找中位数,还能解决:
- 寻找两个数组的第k小元素:把中位数问题推广到任意k值
- 分割数组的最大值最小化:像切披萨一样公平分配
- 在旋转排序数组中查找目标:旋转数组中的"寻宝游戏"
结语:算法优化的本质是「精准打击」
从青铜的O(m+n)到王者的O(log(min(m,n))),我们见证了算法优化的「进化史」。就像从步行到高铁的交通革命,每一次优化都是对「 unnecessary work 」的无情删减。
面试金句:当面试官问你如何优化两个数组的中位数查找时,你可以自信地说:“我用二分查找实现O(log(min(m,n)))复杂度,比题目要求的O(log(m+n))还快——就像兔子中的博尔特,不仅赢了比赛,还打破了世界纪录!”
算个球的算法哲学:算法优化的精髓,在于找到问题的「阿喀琉斯之踵」——有时候少即是多,简单即是美。