Facebook Pixel

2387. Median of a Row Wise Sorted Matrix

Problem Description

You are given an m x n matrix grid containing an odd number of integers where each row is sorted in non-decreasing order. Your task is to find and return the median of all elements in the matrix.

The median is the middle value when all elements are arranged in sorted order. Since the matrix contains an odd number of elements, the median will be the element at position ⌈(m × n) / 2⌉ when all elements are sorted.

Key constraints:

  • Each row in the matrix is already sorted in non-decreasing order
  • The total number of elements (m × n) is odd
  • You must solve this problem in less than O(m × n) time complexity

The challenge is to find the median without actually sorting all elements of the matrix, which would take O(m × n × log(m × n)) time. Instead, you need to leverage the fact that each row is already sorted to achieve a more efficient solution.

For example, if you have a 3×3 matrix with 9 elements total, the median would be the 5th smallest element across the entire matrix.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Since we need to find the median in less than O(m × n) time, we can't afford to merge or sort all elements. However, we can think about this problem differently: instead of finding the exact position of the median, we can search for the median value itself.

The key insight is that the median has a special property: it's the smallest number x such that at least ⌈(m × n) / 2⌉ elements in the matrix are less than or equal to x. In other words, if we count how many elements are ≤ some value, the median is the smallest value where this count reaches our target position.

This transforms our problem into a search problem: we need to find the minimum value that satisfies a certain condition. This naturally leads us to binary search on the answer space.

For any candidate value x, we can efficiently count how many elements in the matrix are ≤ x by utilizing the fact that each row is sorted. For each row, we can use binary search to find the rightmost position where elements are ≤ x. The sum of these positions across all rows gives us the total count.

The beauty of this approach is that:

  1. We perform binary search on the possible values (the outer binary search)
  2. For each candidate value, we count elements using binary search on each sorted row (the inner binary search)
  3. This gives us O(m × log(n) × log(range)) time complexity, which is better than O(m × n)

The solution searches for the first value where the count of elements ≤ that value is at least target = ⌈(m × n) / 2⌉. This value must be the median because it's the point where we've seen exactly half (or just over half) of all elements.

Learn more about Binary Search patterns.

Solution Implementation

1from typing import List
2from bisect import bisect_right
3
4
5class Solution:
6    def matrixMedian(self, grid: List[List[int]]) -> int:
7        """
8        Find the median of a row-wise sorted matrix.
9
10        The algorithm uses binary search on the value range to find the median.
11        For each candidate value, it counts how many elements are less than or equal to it.
12        The median is the smallest value where at least (m*n+1)/2 elements are <= to it.
13        """
14
15        def count_elements_less_than_or_equal(target_value):
16            """Count elements in the matrix <= target_value."""
17            total_count = 0
18            for row in grid:
19                total_count += bisect_right(row, target_value)
20            return total_count
21
22        # Get matrix dimensions
23        num_rows = len(grid)
24        num_cols = len(grid[0])
25
26        # Calculate the position of the median element
27        median_position = (num_rows * num_cols + 1) // 2
28
29        # Binary search using the template to find smallest value with count >= median_position
30        left, right = 0, 10**6
31        first_true_index = -1
32
33        while left <= right:
34            mid = (left + right) // 2
35            if count_elements_less_than_or_equal(mid) >= median_position:
36                # Feasible - found a value where count >= target
37                first_true_index = mid
38                right = mid - 1  # Try to find smaller value
39            else:
40                left = mid + 1
41
42        return first_true_index
43
1class Solution {
2    private int[][] grid;
3
4    /**
5     * Find the median of all elements in a row-wise sorted matrix.
6     * Uses binary search on the value range to find the median.
7     */
8    public int matrixMedian(int[][] grid) {
9        this.grid = grid;
10        int rows = grid.length;
11        int cols = grid[0].length;
12
13        // Calculate the position of the median (1-indexed)
14        int medianPosition = (rows * cols + 1) >> 1;
15
16        // Binary search using the template to find smallest value with count >= medianPosition
17        int left = 0;
18        int right = 1000000;
19        int firstTrueIndex = -1;
20
21        while (left <= right) {
22            int mid = left + (right - left) / 2;
23
24            // Count how many elements are less than or equal to mid
25            if (count(mid) >= medianPosition) {
26                // Feasible - found a value where count >= target
27                firstTrueIndex = mid;
28                right = mid - 1;  // Try to find smaller value
29            } else {
30                left = mid + 1;
31            }
32        }
33
34        return firstTrueIndex;
35    }
36
37    /**
38     * Count the number of elements in the matrix that are less than or equal to x.
39     * Uses binary search on each sorted row to efficiently count elements.
40     */
41    private int count(int x) {
42        int totalCount = 0;
43
44        // For each row in the matrix
45        for (int[] row : grid) {
46            int left = 0;
47            int right = row.length;
48
49            // Binary search to find the first position where row[position] > x
50            while (left < right) {
51                int mid = (left + right) >> 1;
52
53                if (row[mid] > x) {
54                    right = mid;
55                } else {
56                    left = mid + 1;
57                }
58            }
59
60            totalCount += left;
61        }
62
63        return totalCount;
64    }
65}
66
1class Solution {
2public:
3    int matrixMedian(vector<vector<int>>& grid) {
4        // Get matrix dimensions
5        int numRows = grid.size();
6        int numCols = grid[0].size();
7
8        // The median position in a sorted array of m*n elements
9        int medianPosition = (numRows * numCols + 1) / 2;
10
11        // Lambda function to count elements less than or equal to a given value
12        auto countSmallerOrEqual = [&](int value) {
13            int count = 0;
14            for (const auto& row : grid) {
15                count += (upper_bound(row.begin(), row.end(), value) - row.begin());
16            }
17            return count;
18        };
19
20        // Binary search using the template to find smallest value with count >= medianPosition
21        int left = 0;
22        int right = 1000000;
23        int firstTrueIndex = -1;
24
25        while (left <= right) {
26            int mid = left + (right - left) / 2;
27
28            if (countSmallerOrEqual(mid) >= medianPosition) {
29                // Feasible - found a value where count >= target
30                firstTrueIndex = mid;
31                right = mid - 1;  // Try to find smaller value
32            } else {
33                left = mid + 1;
34            }
35        }
36
37        return firstTrueIndex;
38    }
39};
40
1function matrixMedian(grid: number[][]): number {
2    // Get matrix dimensions
3    const numRows: number = grid.length;
4    const numCols: number = grid[0].length;
5
6    // The median position in a sorted array of m*n elements
7    const medianPosition: number = Math.floor((numRows * numCols + 1) / 2);
8
9    // Helper function to find upper bound index (first element > target)
10    const upperBound = (arr: number[], target: number): number => {
11        let left: number = 0;
12        let right: number = arr.length;
13
14        while (left < right) {
15            const mid: number = Math.floor((left + right) / 2);
16            if (arr[mid] <= target) {
17                left = mid + 1;
18            } else {
19                right = mid;
20            }
21        }
22        return left;
23    };
24
25    // Helper function to count elements less than or equal to a given value
26    const countSmallerOrEqual = (value: number): number => {
27        let count: number = 0;
28        for (const row of grid) {
29            count += upperBound(row, value);
30        }
31        return count;
32    };
33
34    // Binary search using the template to find smallest value with count >= medianPosition
35    let left: number = 0;
36    let right: number = 1000000;
37    let firstTrueIndex: number = -1;
38
39    while (left <= right) {
40        const mid: number = Math.floor((left + right) / 2);
41
42        if (countSmallerOrEqual(mid) >= medianPosition) {
43            // Feasible - found a value where count >= target
44            firstTrueIndex = mid;
45            right = mid - 1;  // Try to find smaller value
46        } else {
47            left = mid + 1;
48        }
49    }
50
51    return firstTrueIndex;
52}
53

Solution Approach

The solution implements the two binary search approach mentioned in the reference. Let's break down the implementation step by step:

1. Define the counting function:

def count(x):
    return sum(bisect_right(row, x) for row in grid)

This helper function counts how many elements in the entire matrix are less than or equal to x. For each row, bisect_right(row, x) returns the rightmost position where we can insert x to keep the row sorted, which effectively gives us the count of elements ≤ x in that row. We sum these counts across all rows.

2. Calculate the target position:

m, n = len(grid), len(grid[0])
target = (m * n + 1) >> 1

Since we have an odd number of elements, the median is at position ⌈(m × n) / 2⌉. The expression (m * n + 1) >> 1 is equivalent to (m * n + 1) // 2, which gives us the ceiling of (m × n) / 2 for odd numbers.

3. Perform binary search using the template:

We define our feasible function: feasible(x) = count(x) >= target. This creates a pattern like [false, false, ..., true, true, ...] as x increases. We want to find the first true value (the smallest x where count >= target).

Using the standard binary search template:

left, right = 0, 10**6
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if count(mid) >= target:  # Feasible
        first_true_index = mid
        right = mid - 1  # Try to find smaller value
    else:
        left = mid + 1

return first_true_index

The template finds the smallest value x such that count(x) >= target, which is exactly our median value.

Time Complexity:

  • Outer binary search: O(log(range)) where range is the value range (10^6 in this case)
  • For each binary search iteration, we call count(x) which performs m binary searches, each taking O(log n) time
  • Total: O(m × log(n) × log(range))

This is significantly better than the naive O(m × n × log(m × n)) approach of merging and sorting all elements.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through finding the median of this 3×3 matrix:

grid = [[1, 1, 2],
        [2, 3, 3],
        [1, 3, 4]]

Step 1: Setup

  • Matrix dimensions: m = 3, n = 3
  • Total elements: 3 × 3 = 9 (odd number)
  • Target position: (9 + 1) >> 1 = 5 (we need the 5th smallest element)
  • Feasible function: count(x) >= 5

Step 2: Binary Search using Template

Initialize: left = 0, right = 10^6, first_true_index = -1

(After many iterations narrowing down, we focus on the key iterations)

Key Iteration: left = 1, right = 4

  • mid = (1 + 4) // 2 = 2
  • Count elements ≤ 2:
    • Row 1: [1, 1, 2] → bisect_right gives 3 (all elements ≤ 2)
    • Row 2: [2, 3, 3] → bisect_right gives 1 (only first element ≤ 2)
    • Row 3: [1, 3, 4] → bisect_right gives 1 (only first element ≤ 2)
    • Total count = 3 + 1 + 1 = 5 >= 5 → feasible!
  • Update first_true_index = 2, right = mid - 1 = 1

Next Iteration: left = 1, right = 1

  • mid = (1 + 1) // 2 = 1
  • Count elements ≤ 1:
    • Row 1: [1, 1, 2] → bisect_right gives 2
    • Row 2: [2, 3, 3] → bisect_right gives 0
    • Row 3: [1, 3, 4] → bisect_right gives 1
    • Total count = 2 + 0 + 1 = 3 < 5 → not feasible
  • Update left = mid + 1 = 2

Final: left = 2, right = 1

  • left > right, loop terminates
  • Result: first_true_index = 2

Step 3: Result

The binary search finds that 2 is the smallest value where at least 5 elements are ≤ to it. Therefore, the median is 2.

To verify: Sorting all elements gives [1, 1, 1, 2, 2, 3, 3, 3, 4], and the 5th element is indeed 2.

The key insight is that we never actually sorted all elements. Instead, we efficiently searched for the value that would be at the median position by counting how many elements are less than or equal to candidate values.

Time and Space Complexity

The time complexity is O(m × log n × log M), where m is the number of rows, n is the number of columns of the grid, and M is the maximum element in the grid (here M = 10^6).

Breaking down the complexity:

  • The outer binary search using bisect_left on the range [0, 10^6] performs O(log M) iterations
  • For each iteration, the count function is called
  • Inside count, we iterate through m rows
  • For each row, bisect_right performs binary search on a sorted row of length n, taking O(log n) time
  • Therefore, each count call takes O(m × log n) time
  • Total time complexity: O(log M × m × log n) = O(m × log n × log M)

The space complexity is O(1) as the algorithm only uses a constant amount of extra space for variables like m, n, and target. The bisect_left function with range(10^6 + 1) doesn't actually create the full range in memory due to Python's lazy evaluation of range objects.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Using Wrong Binary Search Template

The Pitfall: Using while left < right with right = mid instead of the standard template with while left <= right, first_true_index, and right = mid - 1.

Why It's Wrong: Different binary search variants behave differently at boundaries. Using inconsistent patterns makes code harder to reason about and more prone to off-by-one errors.

The Fix: Use the standard template consistently:

left, right = 0, 10**6
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if count(mid) >= target:
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

return first_true_index

2. Incorrect Value Range Assumption

The code assumes all matrix values are between 0 and 10^6, which may not always be true. If the matrix contains negative numbers or values larger than 10^6, the binary search will fail to find the correct median.

Solution: Find the actual min and max values in the matrix:

min_val = min(row[0] for row in grid)
max_val = max(row[-1] for row in grid)
left, right = min_val, max_val

3. Confusion Between Element Count and Element Value

A common mistake is confusing what the binary search returns. We're searching through a range of values with a counting function. The result is the actual median value, not an index into the matrix.

4. Off-by-One Error in Median Position Calculation

When calculating the median position, using (m * n + 1) // 2 vs (m * n) // 2 can lead to different results. The current formula works for odd-sized matrices but would need adjustment for even sizes.

Solution for odd matrices (current problem):

median_position = (m * n + 1) // 2
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does quick sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More