寻找两个正序数组的中位数 图解题解
两个有序数组找中位数,不用合并——在短数组上二分「切一刀」,O(log min(m,n)) 直接定位。
找两个有序书架合并后的中间那本,不用真的把两排书全搬到一起再数。只在短书架上「切一刀」:左边凑够合并总数的一半,另一刀位置由此反推;再验一个条件——左半最大值都不超过右半最小值。条件不满足就朝违规方向挪切点,满足就直接从四个边界值里算出中位数,全程只在短数组上二分。
这道题到底在问什么
- 输入
- A = [1,3,8,12],B = [2,7,9,10,15]
- 输出
- 8(合并后 [1,2,3,7,8,9,10,12,15],正中间是第 5 个数 8)
最优解:为什么这么做
一句话答案:LeetCode 4 寻找两个正序数组的中位数用双指针归并:两边本来就有序,每轮比两个头取较小的往前推,一趟拉链式合并到正中间就是中位数,时间 O(m+n)、空间 O(m+n)。
两串各自有序,合起来的正中间是哪个数
给两个各自从小到大排好的数组 A 和 B,把它们合起来看成一整串,求正中间那个数,也就是中位数(所有数排好序后站在最中央的那个;总个数是偶数、没有单独一个正中时,取挨着中间的两个数求平均)。题面 A=[1,3,8,12]、B=[2,7,9,10,15],合起来 9 个排好是 [1,2,3,7,8,9,10,12,15],正中间第 5 个是 8,答案就是 8。
拼一起再整体排序,白付的是哪一笔
把 A、B 拼成一个大数组、再从头到尾整体排一次序取正中间,这条路要 O((m+n)log(m+n))(m、n 是两数组长度,大 O 记号衡量数据变大时操作量的放大速度)。排序里那个 log 因子是白花的——A、B 本来各自已经有序,拼一起重排等于把这张现成的牌撕了重发。
两边各自排好,一次只需比两个头
两个数组都已经有序。想象两只手分别按在 A、B 的开头,谁的当前头小,接下来该进合并结果的最小数就一定是它——它比自己身后的都小、也比对方的头小,全局没有更小的了。取走它、那只手往后挪一格,再比新的两个头。像拉链一样一格格咬合,一趟扫下来两串就并成一整串有序数组,这就是双指针归并(两个指针各扫一边、边比边合)。
一只手先走完怎么办,中位数又落在哪个下标
走起来是这样:两个指针 i、j 各指 A、B 的头,每轮比 A[i] 和 B[j],把较小的接进合并数组、对应指针前移,相等时接谁都行。循环到某一只手先走完——比如 A 全取光了,B 剩下那截本来就有序,直接整段接到尾巴上。合并数组凑齐后看它的长度 n:n 是奇数,正中只有一个,取下标 n//2(下标从 0 数起);n 是偶数,正中是挨着的两个,取下标 n//2−1 和 n//2 两数求平均。
两串归并到第 5 个数,一步步看
i、j 都从 0 起。A[0]=1 ≤ B[0]=2,接 1,i 到 1;A[1]=3 比 B[0]=2 大,接 2,j 到 1;A[1]=3 ≤ B[1]=7,接 3,i 到 2;A[2]=8 比 B[1]=7 大,接 7,j 到 2;A[2]=8 ≤ B[2]=9,接 8,i 到 3;A[3]=12 比 B[2]=9 大,接 9,j 到 3;12 又比 B[3]=10 大,接 10,j 到 4;A[3]=12 ≤ B[4]=15,接 12,i 到 4。此时 A 走完,B 剩的 15 整段接上。合并数组是 [1,2,3,7,8,9,10,12,15],长度 9 是奇数,取下标 9//2=4,正是第 5 个数 8。
归并完漏接剩下那截,尾巴上的数就没了
复杂度上,i、j 加起来把 A、B 各扫一遍、共 m+n 步,时间 O(m+n);额外开一个合并数组装结果,空间 O(m+n)。几个容易栽的地方:那个 while 只在两手都没走完时才转,必有一只先停,另一只剩的那截忘了接上,尾部的数会整片丢失。中位数别一律拿 n//2 收口,那只对奇数长度成立,偶数长度得取中间两数求平均,漏了会少算一个数。某个数组为空则一次循环都不进,非空那串整段接上就退化成单个有序数组取中位数,照样对、不必特判。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记住归并的核心:比较两个数组当前的头,谁小取谁。合并出的数组从小到大排好,正中间就是中位数。
- 4下方这排格子是归并后会得到的有序数组,现在还没确定(变暗)。指针 ai、bi 分别站在 A、B 的开头,准备逐格填充。
- 5比较两个数组当前的头:A 这边是 1,B 这边是 2。较小的是 1,它来自 数组 A,放进第 1 格。
- 6第 1 格定为 1(标绿表示已确定)。对应指针前移,继续比较下一对。
- 7比较两个数组当前的头:A 这边是 3,B 这边是 2。较小的是 2,它来自 数组 B,放进第 2 格。
- 8第 2 格定为 2(标绿表示已确定)。对应指针前移,继续比较下一对。
- 9比较两个数组当前的头:A 这边是 3,B 这边是 7。较小的是 3,它来自 数组 A,放进第 3 格。
- 10第 3 格定为 3(标绿表示已确定)。对应指针前移,继续比较下一对。
- 11比较两个数组当前的头:A 这边是 8,B 这边是 7。较小的是 7,它来自 数组 B,放进第 4 格。
- 12第 4 格定为 7(标绿表示已确定)。对应指针前移,继续比较下一对。
- 13比较两个数组当前的头:A 这边是 8,B 这边是 9。较小的是 8,它来自 数组 A,放进第 5 格。
- 14第 5 格填入 8——这正好是合并后正中间的位置(总长 9 个,正中是第 5 个)。中位数就是 8,归并到这里就能停了。
- 15比较两个数组当前的头:A 这边是 12,B 这边是 9。较小的是 9,它来自 数组 B,放进第 6 格。
- 16第 6 格定为 9(标绿表示已确定)。对应指针前移,继续比较下一对。
- 17比较两个数组当前的头:A 这边是 12,B 这边是 10。较小的是 10,它来自 数组 B,放进第 7 格。
- 18第 7 格定为 10(标绿表示已确定)。对应指针前移,继续比较下一对。
- 19比较两个数组当前的头:A 这边是 12,B 这边是 15。较小的是 12,它来自 数组 A,放进第 8 格。
- 20第 8 格定为 12(标绿表示已确定)。对应指针前移,继续比较下一对。
- 21A 这边已经取完了,B 剩下的本来就有序,直接把 B[4]=15 接到第 9 格。
- 22第 9 格定为 15(标绿表示已确定)。对应指针前移,继续比较下一对。
- 23整个归并完成,得到有序数组 [1,2,3,7,8,9,10,12,15]。一共 9 个数,正中间(第 5 个,标红)就是中位数 8。
⚠️ 容易写错的地方
✗ 错:把两数组拼起来再整体排序
✓ 对:利用「都已有序」直接归并
整体排序是 O((m+n)log(m+n)),浪费了「已有序」这个条件;归并只要 O(m+n)
✗ 错:中位数下标用 n/2 一概而论
✓ 对:按奇偶分别处理
奇数长度取 merged[n//2];偶数长度要取中间两个 merged[n//2-1] 和 merged[n//2] 的平均
✗ 错:一边走完后忘了接上另一边的剩余
✓ 对:归并后把剩余部分补到尾部
while 只在两边都没走完时执行,必有一边提前结束,剩余元素要单独接上否则丢数
完整代码(Python / C++ / Java)
Python
def findMedianSortedArrays(A, B):
merged = []
i = j = 0 # A、B 的指针
while i < len(A) and j < len(B):
if A[i] <= B[j]: # 取较小的放进来
merged.append(A[i]); i += 1
else:
merged.append(B[j]); j += 1
merged += A[i:] # 剩下的直接接上
merged += B[j:]
n = len(merged)
if n % 2: # 奇数: 正中那个
return merged[n // 2]
return (merged[n//2 - 1] + merged[n//2]) / 2 # 偶数取平均C++
double findMedianSortedArrays(vector<int>& A, vector<int>& B){
vector<int> m;
int i = 0, j = 0;
while (i < A.size() && j < B.size())
if (A[i] <= B[j]) m.push_back(A[i++]);
else m.push_back(B[j++]);
while (i < A.size()) m.push_back(A[i++]);
while (j < B.size()) m.push_back(B[j++]);
int n = m.size();
if (n & 1) return m[n / 2];
return (m[n/2 - 1] + m[n/2]) / 2.0;
}Java
class Solution {
public double findMedianSortedArrays(int[] A, int[] B) {
int[] m = new int[A.length + B.length];
int i = 0, j = 0, k = 0;
while (i < A.length && j < B.length)
m[k++] = A[i] <= B[j] ? A[i++] : B[j++];
while (i < A.length) m[k++] = A[i++];
while (j < B.length) m[k++] = B[j++];
int n = m.length;
if (n % 2 == 1) return m[n / 2];
return (m[n/2 - 1] + m[n/2]) / 2.0;
}
}复杂度
时间
O(m+n)
归并把两个数组各扫一遍,m、n 是两数组长度
空间
O(m+n)
用一个合并数组存放归并结果
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 寻找两个正序数组的中位数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
总长度是偶数时,中位数到底怎么取?+
取正中间两个数求平均,不再是单独一个。比如合并后是 6 个数,正中夹在第 3 和第 4 之间,中位数 =(第 3 个 + 第 4 个)÷ 2,写成下标就是 (merged[2]+merged[3])/2。只有奇数长度才有唯一的正中一个数,偶数长度必须两数平均,直接取某一个会偏向一边。
归并是 O(m+n),可听说这题要 O(log(m+n)),差在哪、要不要追?+
诚实说,LeetCode 这题官方进阶要求确实是 O(log(m+n)),比归并更快。做法叫划分二分:在较短的那个数组上二分一条切割线,把两数组各切成左右两半,让左半的元素个数正好凑成总长一半、且左半的最大值 ≤ 右半的最小值,中位数就落在切割线两侧。它每次砍掉一半可能的切法,能到 O(log(min(m,n)))。不过本页的动画和参考代码走的是更好懂、更不容易写错的归并版 O(m+n),把归并吃透后再去啃划分二分会顺很多。
如果其中一个数组是空的,会不会出错?+
不会,也不用特判。归并的 while 要求两只手都没走完才转,只要有一个数组是空的,它的指针一开始就到头,循环一次都不进,直接把另一串非空数组整段接到尾部,问题就退化成『在单个有序数组里取中位数』,再按奇偶取正中,结果依然正确。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 寻找两个正序数组的中位数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。