Description
4. Median of Two Sorted Arrays
Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays.
The overall run time complexity should be .
Example 1:
Input: nums1 = [1,3], nums2 = [2]
Output: 2.00000
Explanation: merged array = [1,2,3] and median is 2.
Example 2:
Input: nums1 = [1,2], nums2 = [3,4]
Output: 2.50000
Explanation: merged array = [1,2,3,4] and median is (2 + 3) / 2 = 2.5.
Constraints:
nums1.length == mnums2.length == n0 <= m <= 10000 <= n <= 10001 <= m + n <= 2000-10^6 <= nums1[i], nums2[i] <= 10^6
Brute Force Approach: Merge and Find Median
Intuition
Merge both sorted arrays into a single sorted array of size using the two-pointer merging technique. Once merged, return the middle element if the total length is odd, or the average of the two middle elements if even.
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int[] merged = new int[m + n];
int i = 0, j = 0, k = 0;
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) {
merged[k++] = nums1[i++];
} else {
merged[k++] = nums2[j++];
}
}
while (i < m) merged[k++] = nums1[i++];
while (j < n) merged[k++] = nums2[j++];
int total = m + n;
if (total % 2 == 1) {
return merged[total / 2];
} else {
return (merged[total / 2 - 1] + merged[total / 2]) / 2.0;
}
}
}
Complexity
- Time Complexity: — Merges all elements into a new array.
- Space Complexity: — Requires extra memory for the merged array.
Better Approach: Two Pointers ( Space)
Intuition
We don’t need to store the entire merged array. We only need the element(s) at index (m + n) / 2 and (m + n) / 2 - 1. Simulate the two-pointer merge process using a counter and keep track of only the last two seen elements.
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int total = m + n;
int targetIdx2 = total / 2;
int i = 0, j = 0, count = 0;
int val1 = 0, val2 = 0;
while (count <= targetIdx2) {
val1 = val2;
if (i < m && (j >= n || nums1[i] <= nums2[j])) {
val2 = nums1[i++];
} else {
val2 = nums2[j++];
}
count++;
}
if (total % 2 == 1) {
return val2;
} else {
return (val1 + val2) / 2.0;
}
}
}
Complexity
- Time Complexity: — Traverses up to half of the combined elements.
- Space Complexity: — Uses constant auxiliary memory.
Most Optimized Solution: Binary Search on Partition
Intuition
Instead of merging, partition both arrays into two equal-sized left and right halves such that:
- Total elements in left half .
- Every element in the left half is every element in the right half.
Binary search on the smaller array (nums1) to find the partition cut point partition1:
partition1elements taken fromnums1partition2 = (m + n + 1) / 2 - partition1elements taken fromnums2
Check the boundary conditions:
maxLeft1 <= minRight2ANDmaxLeft2 <= minRight1- If
maxLeft1 > minRight2, we took too many elements fromnums1move left (high = partition1 - 1). - Otherwise, move right (
low = partition1 + 1).
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// Ensure nums1 is the smaller array to minimize binary search steps
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}
int m = nums1.length;
int n = nums2.length;
int low = 0, high = m;
while (low <= high) {
int partition1 = low + (high - low) / 2;
int partition2 = (m + n + 1) / 2 - partition1;
int maxLeft1 = (partition1 == 0) ? Integer.MIN_VALUE : nums1[partition1 - 1];
int minRight1 = (partition1 == m) ? Integer.MAX_VALUE : nums1[partition1];
int maxLeft2 = (partition2 == 0) ? Integer.MIN_VALUE : nums2[partition2 - 1];
int minRight2 = (partition2 == n) ? Integer.MAX_VALUE : nums2[partition2];
if (maxLeft1 <= minRight2 && maxLeft2 <= minRight1) {
if ((m + n) % 2 == 0) {
return (Math.max(maxLeft1, maxLeft2) + Math.min(minRight1, minRight2)) / 2.0;
} else {
return Math.max(maxLeft1, maxLeft2);
}
} else if (maxLeft1 > minRight2) {
high = partition1 - 1; // Move left
} else {
low = partition1 + 1; // Move right
}
}
return 0.0;
}
}
Complexity
- Time Complexity: — Binary search runs on the smaller array size.
- Space Complexity: — Constant memory.
Easy Memory Rule
“Partition smaller array into
leftandright. Valid partition requiresl1 <= r2andl2 <= r1. Binary search adjusts partition until valid.”
1. The Core Idea: Why Partitioning Works
By definition, the median splits a sorted array into two equal halves:
- Left Half: Contains the smaller half of all numbers.
- Right Half: Contains the larger half of all numbers.
- Every single number in the Left Half every single number in the Right Half.
Since we have two sorted arrays instead of one, we slice both arrays at specific cut points so that:
- The total count of elements in
Left1 + Left2equals half of the total combined elements. - Every number in
Left1 + Left2is smaller than or equal to every number inRight1 + Right2.
2. Why partition2 = (m + n + 1) / 2 - partition1?
-
Total size of left half: We need the left side to hold exactly half of the total combined elements, which is
(m + n + 1) / 2. -
Why the
+ 1?: It handles both even and odd total lengths using standard integer division: -
Even total (e.g., 6 elements):
(6 + 1) / 2 = 3elements in the left half. -
Odd total (e.g., 5 elements):
(5 + 1) / 2 = 3elements in the left half. (The left half gets 1 extra element, meaning the median is simply the largest value in the left half). -
The Formula: If binary search decides to pick
partition1elements fromnums1for the left half, thennums2must supply the remaining amount to reach the target size:
3. Purpose of the Boundary Variables
When we cut nums1 at partition1 and nums2 at partition2, each array gets split into two pieces around the cut:
nums1: [ ... maxLeft1 ] | [ minRight1 ... ]
nums2: [ ... maxLeft2 ] | [ minRight2 ... ]
maxLeft1: The largest element on the left side ofnums1(nums1[partition1 - 1]).minRight1: The smallest element on the right side ofnums1(nums1[partition1]).maxLeft2: The largest element on the left side ofnums2(nums2[partition2 - 1]).minRight2: The smallest element on the right side ofnums2(nums2[partition2]).
Why use Integer.MIN_VALUE and Integer.MAX_VALUE?
If a partition cut is at index 0 (taking 0 elements from an array), there is no left element, so we assign Integer.MIN_VALUE. If a cut is at the end (taking all elements), there is no right element, so we assign Integer.MAX_VALUE. This prevents out-of-bounds errors during comparisons.
4. Purpose of the if-else Conditions
Because nums1 and nums2 are already sorted individually, we already know maxLeft1 <= minRight1 and maxLeft2 <= minRight2.
To verify our combined left half is completely valid, we only need to cross-check the boundary diagonals:
Condition 1: Perfect Partition Found
if (maxLeft1 <= minRight2 && maxLeft2 <= minRight1)
- Meaning: All left elements are smaller than all right elements. The cut is correct!
- Calculate Median:
- If total length is odd: Median is
Math.max(maxLeft1, maxLeft2). - If total length is even: Median is the average of
Math.max(maxLeft1, maxLeft2)andMath.min(minRight1, minRight2).
Condition 2: Took Too Many Elements From nums1
else if (maxLeft1 > minRight2)
- Meaning:
maxLeft1is too big to belong in the left half. We need to take fewer elements fromnums1. - Action: Move binary search left (
high = partition1 - 1).
Condition 3: Took Too Few Elements From nums1
else // (maxLeft2 > minRight1)
- Meaning:
maxLeft2is too big, which meanspartition2was forced to take too many elements fromnums2. We need to take more elements fromnums1. - Action: Move binary search right (
low = partition1 + 1).