Lesson 13 of 70 6 min

MANG Problem #2: Median of Two Sorted Arrays (Expert)

1. Problem Statement......

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

1. Problem Statement

Mental Model

Imposing order to reduce the search space complexity.

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 $O(\log (m+n))$.

2. The Mental Model: The "Balanced Partition"

Most developers think the median is a value. A Staff Engineer thinks of the median as a Divider. We want to find a point to "cut" both arrays such that:

  1. The number of elements on the combined left side equals the number on the right.
  2. The largest element on the left side is smaller than the smallest element on the right side.

Since the arrays are sorted, we don't need to look at all elements. We only need to find the correct i and j where:

  • nums1[i-1] <= nums2[j]
  • nums2[j-1] <= nums1[i]

3. Visual Execution (The Binary Cross-Check)

graph LR
    subgraph "Array 1 (Size M)"
        L1[Left 1: nums1[i-1]] | R1[Right 1: nums1[i]]
    end
    subgraph "Array 2 (Size N)"
        L2[Left 2: nums2[j-1]] | R2[Right 2: nums2[j]]
    end
    
    Condition{Check: L1 <= R2 && L2 <= R1}
    Condition -- Yes --> Success[Found Median!]
    Condition -- No (L1 > R2) --> MoveLeft[Reduce i]
    Condition -- No (L2 > R1) --> MoveRight[Increase i]

4. Java Implementation (Optimal O(log(min(M, N))))

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    // 1. Ensure binary search is done on the smaller array
    // This reduces iterations to O(log(min(M,N)))
    if (nums1.length > nums2.length) {
        return findMedianSortedArrays(nums2, nums1);
    }
    
    int m = nums1.length;
    int n = nums2.length;
    int low = 0;
    int high = m;
    
    while (low <= high) {
        int partitionX = (low + high) / 2;
        int partitionY = (m + n + 1) / 2 - partitionX;
        
        // Edge cases for partitions at the start or end
        int maxLeftX = (partitionX == 0) ? Integer.MIN_VALUE : nums1[partitionX - 1];
        int minRightX = (partitionX == m) ? Integer.MAX_VALUE : nums1[partitionX];
        
        int maxLeftY = (partitionY == 0) ? Integer.MIN_VALUE : nums2[partitionY - 1];
        int minRightY = (partitionY == n) ? Integer.MAX_VALUE : nums2[partitionY];
        
        if (maxLeftX <= minRightY && maxLeftY <= minRightX) {
            // Correct split found
            if ((m + n) % 2 == 0) {
                return (Math.max(maxLeftX, maxLeftY) + Math.min(minRightX, minRightY)) / 2.0;
            } else {
                return Math.max(maxLeftX, maxLeftY);
            }
        } else if (maxLeftX > minRightY) {
            // We are too far to the right in nums1
            high = partitionX - 1;
        } else {
            // We are too far to the left in nums1
            low = partitionX + 1;
        }
    }
    throw new IllegalArgumentException("Input arrays are not sorted.");
}

5. Verbal Interview Script (Staff Tier)

Interviewer: "Why did you swap the arrays at the beginning?"

You: "Swapping the arrays is the most critical optimization in this problem. By ensuring we perform binary search on the shorter array, we guarantee two things. First, the time complexity becomes $O(\log(\min(M, N)))$, which is strictly better than the required $O(\log(M+N))$. Second, and more importantly, it prevents the index of the second array (partitionY) from becoming negative during calculation. This 'Defensive Programming' pattern makes the logic robust against all input size combinations, which is a hallmark of Staff-level implementation."

6. Staff-Level Follow-Ups

Follow-up 1: "How do you handle empty arrays?"

  • The Answer: "The use of Integer.MIN_VALUE and Integer.MAX_VALUE as boundary sentinels gracefully handles the empty case. If an array is empty, the partition will always be at index 0 or $M$, and the logic will correctly fall back to using the values from the non-empty array."

Follow-up 2: "Can you solve this by finding the K-th smallest element?"

  • The Answer: "Yes. Median is just a special case of finding the $K$-th smallest element where $K = (M+N)/2$. A more generalized solution for $K$-th smallest would also run in $O(\log(K))$ time and is often used in interview variations where the median is not the target."

7. Performance Nuances (The Java Perspective)

  1. Primitive Specialization: We use double for the final return value to handle precise averages. In high-performance Java, we avoid using Double (boxed) objects to minimize object header overhead in millions of calculations.
  2. Arithmetic Safety: Note the calculation (m + n + 1) / 2. The + 1 is a common trick to handle both even and odd total lengths, ensuring the left side always contains the median in an odd-length combined set.

6. Staff-Level Verbal Masterclass (Communication)

Interviewer: "How would you defend this specific implementation in a production review?"

You: "In a mission-critical environment, I prioritize the Big-O efficiency of the primary data path, but I also focus on the Predictability of the system. In this implementation, I chose a state-based dynamic programming approach. While a recursive solution is more readable, I would strictly monitor the stack depth. If this were to handle skewed inputs, I would immediately transition to an explicit stack on the heap to avoid a StackOverflowError. From a memory perspective, I leverage primitive arrays to ensure that we minimize the garbage collection pauses (Stop-the-world) that typically plague high-throughput Java applications."

7. Global Scale & Distributed Pivot

When a problem like this is moved from a single machine to a global distributed architecture, the constraints change fundamentally.

  1. Data Partitioning: We would shard the input space using Consistent Hashing. This ensures that even if our dataset grows to petabytes, any single query only hits a small subset of our cluster, maintaining logarithmic lookup times.
  2. State Consistency: For problems involving state updates (like DP or Caching), we would use a Distributed Consensus protocol like Raft or Paxos to ensure that all replicas agree on the final state, even in the event of a network partition (The P in CAP theorem).

8. Performance Nuances (The Staff Perspective)

  1. Cache Locality: Accessing a 2D matrix in row-major order (reading [i][j] then [i][j+1]) is significantly faster than column-major order in modern CPUs due to L1/L2 cache pre-fetching. I always structure my loops to align with how the memory is physically laid out.
  2. Autoboxing and Generics: In Java, using List<Integer> instead of int[] can be 3x slower due to the overhead of object headers and constant wrapping. For the most performance-sensitive sections of this algorithm, I advocate for primitive specialized structures.

Key Takeaways

  • nums1[i-1] <= nums2[j]
  • nums2[j-1] <= nums1[i]
  • The Answer: "The use of Integer.MIN_VALUE and Integer.MAX_VALUE as boundary sentinels gracefully handles the empty case. If an array is empty, the partition will always be at index 0 or $M$, and the logic will correctly fall back to using the values from the non-empty array."

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.