Median of Two Sorted Arrays
A hard problem from Binary Search. Try it first, open a hint when you are stuck, and read the solution when you are done.
- Hard
- Binary Search
The problem
You are given two arrays of numbers, nums1 and nums2, each sorted from smallest to largest. Either one may be empty, but not both, and values may repeat. Return the median of all their values taken together: with the values in one sorted order, it is the middle one when their count is odd, and the average of the two middle ones when it is even. Aim for a method whose running time is O(log(m+n)), where m and n are the two lengths.
What to write
function solve(nums1: number[], nums2: number[]): numberExample 1
Input nums1 = [1,3] nums2 = [2] Output 2
Together the values are 1, 2 and 3 in sorted order. Their count is odd, so the median is the middle one, 2.
Example 2
Input nums1 = [1,2] nums2 = [3,4] Output 2.5
Together the values are 1, 2, 3 and 4. Their count is even, so the median is the average of the two middle ones: (2 + 3) / 2 = 2.5.
Constraints
Aim for O(log(m+n)) time.
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Use the constraints
The task asks for O(log(m+n)) time. Merging the two arrays, even only as far as the middle, handles about half of all the values, which is far more than that. So the median has to be found without building the merged order.
Hint 2: Ask a smaller question
Take [1, 3, 8] and [2, 4, 9, 10]. Write the seven values in order. The median is the fourth, so mark the four smallest values. Where do those four sit in their own arrays?
Hint 3: Name the pattern
Call the smaller half of all the values the left half. It is made of the first values of each array, so the only unknown is how many values
nums1gives it. If a count is too small, every smaller count is too. If it is too large, every larger count is too. That is binary search on the answer. Read the lesson on binary search on the answer.Hint 4: State the invariant
Cut each array where its part of the left half ends. The value before a cut is its left value, and the value after it is its right value, with minus or plus infinity where a cut is at an end. The cuts are correct exactly when each array's left value is at most the other array's right value.
Hint 5: Sketch the pseudocode
Make
nums1the shorter array. Binary search how many values it gives, i, from 0 to its length;nums2gives half the total, rounded up, minus i. Left value ofnums1above right value ofnums2: lower i. The reverse: raise i. Otherwise return the largest left value, averaged with the smallest right value if the total is even.
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Approach
Binary-search a partition of the shorter array so every value on the combined left is at most every value on the right.
The slow way
Merge the two arrays into one sorted list, then take its middle value, or the average of its two middle values when the count is even. It is correct, but merging handles every value, m + n of them, while the task asks for a number of steps that grows only like the logarithm of that.
The insight
Picture all the values in sorted order, split in the middle into a left half and a right half. The median sits at the split. Both arrays are sorted, so the left half is the first few values of nums1 plus the first few values of nums2. Its size is known, so choosing the count from nums1 fixes the count from nums2. Only that one count has to be found.
Step by step
- Search the shorter array for how many values it gives the left half. The code first makes
nums1the shorter array.nums1may then give the left half none of its values, all of them or anything between, andnums2always has enough values for the rest. - Try the middle count; nums2 gives the rest of the left half. The left half holds half of all the values, rounded up, so it has the extra value when the total is odd. Taking the first values of each array cuts both in two. That pair of cuts is the partition.
- Note the values next to each array's cut, or infinity past an end. The value before a cut is its left value, and the value after it is its right value. A cut at an end has no value on one side: minus or plus infinity stands in, so its comparison always passes.
- The cuts are correct when no left value is above a right value. Each array is sorted, so its left value is never above its own right value. Only the two comparisons across the arrays can fail. When both hold, no left-half value is above any right-half value.
- Read the median off the largest left value and the smallest right one. With an odd total the left half has one value more, so its largest value is the middle one. With an even total the two middle values are that one and the smallest right value, so average them.
- Left value of nums1 above right value of nums2: take fewer from nums1. That left value belongs in the right half, so
nums1gave too many. Giving even more only moves its left value up and the right value ofnums2down, so every larger count fails as well. - Otherwise the left value of nums2 is too big: take more from nums1. Only one comparison is left to fail: the left value of
nums2is above the right value ofnums1. Sonums2gave too many values andnums1too few, and every smaller count fails the same way.
The same numbered steps are the comments in each solution.
JavaScript
function solve(nums1, nums2) {
// 1. Search the shorter array for how many values it gives the left half
if (nums1.length > nums2.length) return solve(nums2, nums1);
const m = nums1.length,
n = nums2.length;
let l = 0,
r = m;
while (l <= r) {
// 2. Try the middle count; nums2 gives the rest of the left half
const i = (l + r) >>> 1,
j = Math.floor((m + n + 1) / 2) - i;
// 3. Note the values next to each array's cut, or infinity past an end
const a = nums1[i - 1] ?? -Infinity,
b = nums1[i] ?? Infinity,
c = nums2[j - 1] ?? -Infinity,
d = nums2[j] ?? Infinity;
// 4. The cuts are correct when no left value is above a right value
if (a <= d && c <= b) {
// 5. Read the median off the largest left value and the smallest right one
return (m + n) % 2
? Math.max(a, c)
: (Math.max(a, c) + Math.min(b, d)) / 2;
}
// 6. Left value of nums1 above right value of nums2: take fewer from nums1
if (a > d) {
r = i - 1;
} else {
// 7. Otherwise the left value of nums2 is too big: take more from nums1
l = i + 1;
}
}
// Sorted arrays always have correct cuts, so this line is never reached.
return 0;
}TypeScript
function solve(nums1: number[], nums2: number[]): number {
// 1. Search the shorter array for how many values it gives the left half
if (nums1.length > nums2.length) return solve(nums2, nums1);
const m = nums1.length,
n = nums2.length;
let l = 0,
r = m;
while (l <= r) {
// 2. Try the middle count; nums2 gives the rest of the left half
const i = (l + r) >>> 1,
j = Math.floor((m + n + 1) / 2) - i;
// 3. Note the values next to each array's cut, or infinity past an end
const a = nums1[i - 1] ?? -Infinity,
b = nums1[i] ?? Infinity,
c = nums2[j - 1] ?? -Infinity,
d = nums2[j] ?? Infinity;
// 4. The cuts are correct when no left value is above a right value
if (a <= d && c <= b) {
// 5. Read the median off the largest left value and the smallest right one
return (m + n) % 2
? Math.max(a, c)
: (Math.max(a, c) + Math.min(b, d)) / 2;
}
// 6. Left value of nums1 above right value of nums2: take fewer from nums1
if (a > d) {
r = i - 1;
} else {
// 7. Otherwise the left value of nums2 is too big: take more from nums1
l = i + 1;
}
}
// Sorted arrays always have correct cuts, so this line is never reached.
return 0;
}
Python
def solve(nums1, nums2):
# 1. Search the shorter array for how many values it gives the left half
if len(nums1) > len(nums2):
return solve(nums2, nums1)
m, n = len(nums1), len(nums2)
low, high = 0, m
while low <= high:
# 2. Try the middle count; nums2 gives the rest of the left half
i = (low + high) // 2
j = (m + n + 1) // 2 - i
# 3. Note the values next to each array's cut, or infinity past an end
left1 = nums1[i - 1] if i > 0 else float("-inf")
right1 = nums1[i] if i < m else float("inf")
left2 = nums2[j - 1] if j > 0 else float("-inf")
right2 = nums2[j] if j < n else float("inf")
# 4. The cuts are correct when no left value is above a right value
if left1 <= right2 and left2 <= right1:
# 5. Read the median off the largest left value and the smallest right one
if (m + n) % 2:
return max(left1, left2)
return (max(left1, left2) + min(right1, right2)) / 2
# 6. Left value of nums1 above right value of nums2: take fewer from nums1
if left1 > right2:
high = i - 1
else:
# 7. Otherwise the left value of nums2 is too big: take more from nums1
low = i + 1
# Sorted arrays always have correct cuts, so this line is never reached.
return 0
Example 2, traced
Input nums1 = [1,2] nums2 = [3,4] Output 2.5
| Step | Count tried from nums1 | The arrays, cut | Left values | right values | What happens |
|---|---|---|---|---|
| Set-up | Any count from 0 to 2 | [1, 2] and [3, 4] | - | 4 values in all: the left half takes 2 |
| First try | 1 (middle of 0 to 2) | [1 | 2] and [3 | 4] | 1, 3 | 2, 4 | Left value 3 is above right value 2: take more from nums1 |
| Second try | 2 (middle of 2 to 2) | [1, 2 |] and [| 3, 4] | 2, minus infinity | plus infinity, 3 | No left value is above a right value: the cuts are correct |
| Median | - | - | Largest left 2 | smallest right 3 | Even total: (2 + 3) / 2 = 2.5 |
Why it works
As nums1 gives more values, its left value can only rise and the right value of nums2 can only fall. So the counts form a run of "too few", then the correct ones, then a run of "too many", and each test shows on which side the correct counts are. With correct cuts the left half holds exactly the smallest values, and the median is read at its edge.
Time and space
Time O(log min(m,n)), extra space O(1). The search covers the counts from 0 to the shorter array's length, min(m,n), and drops at least half of them each round. That is about log min(m,n) rounds of at most four reads each: O(log min(m,n)) time, which meets the task's aim. Only a few numbers are stored: O(1) space.
n and m are input sizes, h is a tree's height, V and E are a graph's vertices and edges. The bounds describe this reference solution.
Watch out
- The left half takes half of the total, rounded up. If you round down, example 1 returns 1 instead of 2: the left half then holds one value of three, and the median ends up in the right half.
- Search the shorter array. If you search the longer one, [-2, 0, 1, 2, 4] with an empty second array asks that empty array for a value: JavaScript returns 0 instead of 1, and Python stops with an error.
- Compare with "at most", not "below", because equal values may sit on both sides of a cut. With "below", [-1, 1] and [-3, 1, 1] has no passing cuts: the loop ends and the code returns 0 instead of 1.
Pattern
Binary search on the answer. Builds on Binary Search and Koko Eating Bananas.
Keep going
2 more problems solved with binary search on the answer
- Koko Eating BananasMedium
- Swim In Rising WaterHard