Median of Two Sorted Arrays
BINARY SEARCH · HARD
The problem
Section titled “The problem”Given two arrays nums1 (size m) and nums2 (size n), each already sorted ascending, return the median of the combined m + n values — the middle value if the combined length is odd, or the average of the two middle values if it’s even. Example: nums1=[1,3], nums2=[2] → combined is [1,2,3], median 2.0. nums1=[1,2], nums2=[3,4] → combined is [1,2,3,4], median (2+3)/2 = 2.5. The catch that makes this a Hard: you’re asked to do it in O(log(min(m,n))) time — actually constructing the combined array first is too slow, so the optimal solution below never builds it at all.
Solution 1: Brute force — linear merge, then read the middle
Section titled “Solution 1: Brute force — linear merge, then read the middle”This is the honest, obvious approach: since both inputs are already sorted, merge them into one sorted array using the same two-pointer walk from the merge step of merge sort — at each step, compare the two pointers’ current values and push whichever is smaller, advancing that pointer. Once one array is exhausted, append the remainder of the other. Once the full merged array exists, the median is just an index lookup: the middle element for odd length, or the average of the two middle elements for even length. It’s a real, correct algorithm — but it’s not the O(log(min(m,n))) the problem asks for, because it touches every element of both arrays at least once, no matter how the median is computed.
nums1 = [2, 4, 5], nums2 = [1, 6, 7, 9] — combined length 7 (odd), median: 5
NUMS1
NUMS2
MERGED (growing)
Press Play or Step to begin.
Step 0 / 13
The code
Section titled “The code”function medianBruteForce(nums1, nums2) { const merged = []; let i = 0, j = 0;
while (i < nums1.length && j < nums2.length) { if (nums1[i] <= nums2[j]) merged.push(nums1[i++]); else merged.push(nums2[j++]); } while (i < nums1.length) merged.push(nums1[i++]); while (j < nums2.length) merged.push(nums2[j++]);
const n = merged.length; if (n % 2 === 1) return merged[(n - 1) / 2]; return (merged[n / 2 - 1] + merged[n / 2]) / 2;}Line by line:
- The first
whileloop is the classic merge-sort merge step: as long as both pointers are still in range, compare the two current values and push whichever is smaller, advancing only that pointer. Using<=(not<) keeps the merge stable, though for a median that choice doesn’t actually change the answer. - Once one array runs out, exactly one of the next two
whileloops fires — it appends whatever’s left of the other array, which is already sorted, so no more comparisons are needed. n % 2 === 1picks the single middle element directly by index for an odd combined length; theelsebranch averages the two elements straddling the middle for an even length.- Every element of both inputs ends up inside
mergedat least once — that full construction, not the final index lookup, is what the complexity below is really paying for.
Complexity
Section titled “Complexity”| Time | Space | Approach |
|---|---|---|
| O(m+n) | O(m+n) | Two-pointer merge builds the full combined array, then an O(1) index lookup finds the median |
Solution 2: Optimal — binary search the smaller array’s partition point
Section titled “Solution 2: Optimal — binary search the smaller array’s partition point”The merged array never needs to exist — only its middle needs to be identified. Any way of splitting both arrays into a “left half” and a “right half” so that every left-half value is ≤ every right-half value, and the two halves are the correct sizes, has found the median without merging anything: it’s max(leftHalf) (odd total) or the average of max(leftHalf) and min(rightHalf) (even total). Binary search directly for that split.
First, swap nums1/nums2 if needed so nums1 is always the smaller array — call it A (size m), and the other B (size n). Binary search a partition index i in A, in the range [0, m]. Once i is chosen, the matching partition index j in B is forced — it’s whatever makes the left side hold exactly ⌈(m+n)/2⌉ elements total, computed in O(1) as j = half - i. Compare the boundary elements: maxLeft(A) <= minRight(B) and maxLeft(B) <= minRight(A). If both hold, the split is valid — read off the median. If maxLeft(A) is too big, i is too high, search lower; otherwise search higher. When i is at an array boundary (0 or m), there’s no real element on that side, so -Infinity / Infinity sentinels stand in — they can never be the reason a comparison fails, which is exactly the point: an empty side should never block a valid split.
Same nums1=[2, 4, 5], nums2=[1, 6, 7, 9] as Solution 1 — nums1 is already the smaller array, so no swap needed; the binary search takes 3 tries to converge, and the resulting median (5) matches Solution 1 exactly.
SMALLER ARRAY — binary searched (A)
LARGER ARRAY — partition derived in O(1) (B)
Press Play or Step to begin.
Step 0 / 7
The code
Section titled “The code”function medianOptimal(nums1, nums2) { let A = nums1, B = nums2; if (A.length > B.length) { const t = A; A = B; B = t; } const m = A.length, n = B.length;
let lo = 0, hi = m; const half = Math.floor((m + n + 1) / 2);
while (lo <= hi) { const i = Math.floor((lo + hi) / 2); const j = half - i;
const maxLeftA = i === 0 ? -Infinity : A[i - 1]; const minRightA = i === m ? Infinity : A[i]; const maxLeftB = j === 0 ? -Infinity : B[j - 1]; const minRightB = j === n ? Infinity : B[j];
if (maxLeftA <= minRightB && maxLeftB <= minRightA) { if ((m + n) % 2 === 1) return Math.max(maxLeftA, maxLeftB); return (Math.max(maxLeftA, maxLeftB) + Math.min(minRightA, minRightB)) / 2; } else if (maxLeftA > minRightB) { hi = i - 1; } else { lo = i + 1; } } throw new Error('input arrays must be sorted');}Line by line:
- The swap guarantees
Ais always the smaller-or-equal-size array, so the binary search range[0, m]below is always bounded bymin(m,n)— this one swap is the entire reason the complexity islog(min(m,n))and notlog(max(m,n)). half = Math.floor((m + n + 1) / 2)is the exact left-half size that works for both parities: it’s(total+1)/2for odd totals (the extra element lands on the left) andtotal/2for even totals — one formula, no branching.- Given a candidate
i,j = half - iis not searched — it’s the only value ofjthat makes the combined left side exactlyhalfelements, derived in O(1). - The four
maxLeft/minRightlines use-Infinity/Infinitysentinels exactly when a partition sits at an array’s edge (nothing real on that side) — this is essential: without it, an edge partition would either readundefinedor crash onA[-1], and every comparison below would break silently. - Both cross-conditions holding means the split is valid: read the median directly, no averaging needed for odd totals, averaging the two boundary values for even ones.
maxLeftA > minRightBmeans too many elements were taken fromA’s left — shrinkiby movinghidown; the opposite case growsiby movingloup. Every iteration halves the search range, givingO(log m)total iterations.
Complexity
Section titled “Complexity”| Time | Space | Approach |
|---|---|---|
| O(log(min(m,n))) | O(1) | Binary search a partition index in the smaller array only; the larger array’s matching partition is always an O(1) derivation, never itself searched |
Complexity comparison
Section titled “Complexity comparison”| Solution | Time | Space |
|---|---|---|
| Brute force (linear merge) | O(m+n) | O(m+n) |
| Optimal (partition binary search) | O(log(min(m,n))) | O(1) |
This is a genuine asymptotic improvement, not just a constant-factor speedup — going from linear in the combined size to logarithmic in the smaller size. The min(m,n) is specific and earned, not a loose bound: searching the smaller array bounds the number of binary-search iterations by that array’s size, and the larger array’s matching partition is always derivable from the smaller array’s current guess in O(1) — it never needs its own search. If the roles were reversed (searching the larger array), the algorithm would still be correct, just O(log(max(m,n))) — strictly worse whenever the arrays differ in size, which is exactly why the swap-to-smaller step at the top of Solution 2 isn’t optional.
Interview corner
Section titled “Interview corner”Why binary search specifically the SMALLER array, rather than either array or always the larger one?
- The binary search range is
[0, m]wheremis the length of whichever array is being searched — so the number of iterations is bounded by that array’s size, not the other one’s. - The matching partition index in the OTHER array is always derivable in O(1) from the chosen index (
j = half - i) — it never needs a search of its own, so its size doesn’t affect the iteration count at all. - Choosing to search the larger array instead would still be correct, just
O(log(max(m,n)))— strictly worse (or equal) whenever the two sizes differ, which is why the problem’s stated bound is specificallylog(min(m,n)), not just “logarithmic”.
Why does half = Math.floor((m + n + 1) / 2) correctly handle BOTH odd and even combined lengths with one formula, no branch?
- For an odd total, the true median is a single element that must sit as the last element of the left half — that requires the left half to hold
(total + 1) / 2elements, one more than a plain half-split. - For an even total,
(total + 1) / 2floors back down to exactlytotal / 2— an even split, which is what’s needed so the median can be averaged from the boundary of each side. - Because integer division floors automatically, one expression produces the “+1 for odd, exact half for even” behavior without an
if (total % 2)branch anywhere in the partition math.
What would break if the -Infinity / Infinity sentinels were removed and A[i - 1] / A[i] were read directly even at the array’s edges?
- At
i = 0,A[i - 1]isA[-1]— plain array indexing never throws in JS, it just silently returnsundefined, which then poisons every downstream comparison (any comparison againstundefinedevaluates tofalse). - At
i = m,A[i]is out of bounds the same way — alsoundefined, also silently wrong instead of throwing where the bug would be obvious. - The sentinels exist precisely so an empty side (nothing taken from this array’s left, or nothing left on its right) can never be the reason a valid split gets rejected — a real, common case whenever the two input arrays differ a lot in size, like
nums1=[1]against a 7-elementnums2.
Trick question — what does this log?
const partitionState = { maxLeftA: -Infinity, minRightA: 4 };console.log(JSON.stringify(partitionState));- Answer:
{"maxLeftA":null,"minRightA":4}— NOT{"maxLeftA":-Infinity,"minRightA":4}, and NOT a dropped key either. JSON.stringifyhas no way to representInfinity,-Infinity, orNaNin JSON (they aren’t valid JSON number literals) — its actual behavior is to silently convert any of them tonull, while keeping the key.- That’s different from
undefined: anundefined-valued property is dropped from the output entirely, not converted tonull— two different “missing-ish” values, two different serialization behaviors.
Trap: this is exactly why nobody should try to JSON.stringify the partition state above for debugging or logging — every boundary sentinel silently becomes null, which reads as “no data” rather than “this side of the partition is empty,” actively misleading whoever’s debugging. The sentinels are fine to use in live comparisons (-Infinity <= 4 works exactly as expected); they just can’t survive a round trip through JSON.
1. Why is the brute-force merge solution O(m+n), not O(log(min(m,n)))?
Show answer
It walks every element of both arrays once during the merge, so its cost scales linearly with the total size. The brute-force merge is a two-pointer walk that visits every element of both arrays exactly once before it can even look up the middle — that’s O(m+n) work no matter how few values are ultimately needed for the median.
2. The optimal solution binary searches a partition index. Why in the SMALLER of the two arrays specifically?
Show answer
Because the number of binary-search iterations is bounded by that array’s size, and the larger array’s partition is always derived in O(1) — never searched itself. Binary searching the smaller array bounds the number of iterations by min(m,n). The matching partition index in the OTHER array is always derived in O(1) from the current guess, so its size never factors into the iteration count at all — that’s the entire reason the bound is specifically log(min(m,n)).
3. What do the -Infinity / Infinity sentinels represent when the partition index i equals 0 or m?
Show answer
There’s no real element on that side of the partition (it’s empty) — the sentinel guarantees it can never wrongly fail a boundary comparison. At i = 0, A[i - 1] is A[-1] — plain array indexing never throws in JS, it just silently returns undefined, which then poisons every downstream comparison (any comparison against undefined evaluates to false). The sentinels exist precisely so an empty side can never be the reason a valid split gets rejected — a real, common case whenever the two input arrays differ a lot in size.
4. Why does half = Math.floor((m + n + 1) / 2) correctly size the left partition for BOTH odd and even combined lengths, with no explicit parity branch?
Show answer
Integer division makes it equal (total+1)/2 for odd totals and exactly total/2 for even totals — one formula covers both cases. Math.floor((m + n + 1) / 2) equals (total+1)/2 for odd totals (giving the left half the extra middle element) and floors back down to exactly total/2 for even totals — one expression handles both parities without an explicit odd/even branch.
5. For nums1=[2,4,5], nums2=[1,6,7,9] (7 total elements, odd), the algorithm finds a valid partition with maxLeftA=5 and maxLeftB=1. What’s the median?
Show answer
5 — max(maxLeftA, maxLeftB), since the total length is odd. For an odd combined length, once a valid partition is found the true median is whichever boundary value is larger — max(maxLeftA, maxLeftB) — since together the two maxLeft values are exactly the largest element(s) of the correctly-sized left half.