title: Search a 2D Matrix
slug: search-a-2d-matrix
difficulty: medium
leetcode_id: 74
leetcode_url: https://leetcode.com/problems/search-a-2d-matrix/
categories:
  - arrays
  - binary-search
patterns:
  - binary-search
  - matrix-traversal

function_signature: "def search_matrix(matrix: list[list[int]], target: int) -> bool:"

test_cases:
  visible:
    - input: { matrix: [[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], target: 3 }
      expected: true
    - input: { matrix: [[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], target: 13 }
      expected: false
  hidden:
    - input: { matrix: [[1]], target: 1 }
      expected: true
    - input: { matrix: [[1]], target: 0 }
      expected: false
    - input: { matrix: [[1, 2, 3]], target: 2 }
      expected: true
    - input: { matrix: [[1], [2], [3]], target: 3 }
      expected: true
    - input: { matrix: [[1, 2], [3, 4]], target: 5 }
      expected: false

description: |
  You are given an `m x n` integer matrix `matrix` with the following two properties:

  - Each row is sorted in non-decreasing order.
  - The first integer of each row is greater than the last integer of the previous row.

  Given an integer `target`, return `true` *if* `target` *is in* `matrix` *or* `false` *otherwise*.

  You must write a solution in `O(log(m * n))` time complexity.

constraints: |
  - `m == matrix.length`
  - `n == matrix[i].length`
  - `1 <= m, n <= 100`
  - `-10^4 <= matrix[i][j], target <= 10^4`

examples:
  - input: "matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3"
    output: "true"
    explanation: "The target 3 is found in the first row at index 1."
  - input: "matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13"
    output: "false"
    explanation: "The target 13 is not present in the matrix."

explanation:
  intuition: |
    Imagine flattening this 2D matrix into a single sorted 1D array. Because each row is sorted *and* the first element of each row is greater than the last element of the previous row, the entire matrix is essentially one long sorted sequence arranged in rows.

    For example, the matrix:
    ```
    [[1,  3,  5,  7],
     [10, 11, 16, 20],
     [23, 30, 34, 60]]
    ```

    Is equivalent to the sorted array: `[1, 3, 5, 7, 10, 11, 16, 20, 23, 30, 34, 60]`

    This insight is the key! Instead of treating it as a 2D search problem, we can treat it as a standard **binary search** on a virtual 1D array. The only trick is converting between a 1D index and 2D row/column coordinates.

    Think of it like this: if you have a 1D index `mid` in a matrix with `n` columns, you can find the row with `mid // n` (integer division) and the column with `mid % n` (remainder). This mapping lets us perform binary search without actually flattening the array.

  approach: |
    We solve this using **Binary Search on Virtual 1D Array**:

    **Step 1: Initialise search boundaries**

    - `rows`: Number of rows in the matrix (`m`)
    - `cols`: Number of columns in the matrix (`n`)
    - `left`: Set to `0` (start of the virtual 1D array)
    - `right`: Set to `rows * cols - 1` (end of the virtual 1D array)

    &nbsp;

    **Step 2: Perform binary search**

    - While `left <= right`:
      - Calculate `mid = left + (right - left) // 2` to avoid integer overflow
      - Convert `mid` to 2D coordinates: `row = mid // cols`, `col = mid % cols`
      - Get the value at `matrix[row][col]`
      - If the value equals `target`, return `true`
      - If the value is less than `target`, search the right half: `left = mid + 1`
      - If the value is greater than `target`, search the left half: `right = mid - 1`

    &nbsp;

    **Step 3: Return result**

    - If the loop exits without finding the target, return `false`

    &nbsp;

    The key insight is that we never actually flatten the matrix. We perform binary search on indices `0` to `m*n - 1` and convert each index to row/column coordinates on the fly.

  common_pitfalls:
    - title: Linear Search Through Rows
      description: |
        A naive approach might iterate through each row and perform a linear search, resulting in **O(m * n)** time complexity.

        With the constraint that `m, n <= 100`, this gives up to 10,000 operations, which is acceptable but far from optimal. The problem explicitly requires `O(log(m * n))`, so this approach would be incorrect.
      wrong_approach: "Nested loops checking each element"
      correct_approach: "Binary search treating matrix as virtual 1D array"

    - title: Two Separate Binary Searches
      description: |
        Some solutions first binary search to find the correct row, then binary search within that row. While this achieves `O(log m + log n) = O(log(m * n))` complexity, it's more complex to implement.

        The single binary search approach is cleaner and directly treats the matrix as a sorted 1D array.
      wrong_approach: "Binary search for row, then binary search within row"
      correct_approach: "Single binary search with index-to-coordinate conversion"

    - title: Integer Overflow in Mid Calculation
      description: |
        When calculating `mid`, using `(left + right) / 2` can cause integer overflow in languages with fixed-size integers if `left` and `right` are both large.

        Always use `left + (right - left) // 2` to avoid this issue. In Python this isn't strictly necessary due to arbitrary precision integers, but it's a good habit for interviews.
      wrong_approach: "(left + right) / 2"
      correct_approach: "left + (right - left) // 2"

    - title: Off-by-One Errors in Index Conversion
      description: |
        A common mistake is confusing rows and columns when converting from 1D to 2D indices. Remember:

        - `row = mid // cols` (divide by number of columns)
        - `col = mid % cols` (remainder after dividing by columns)

        Using `rows` instead of `cols` will produce incorrect coordinates.

  key_takeaways:
    - "**Virtual flattening**: A sorted 2D matrix with strictly increasing rows can be treated as a sorted 1D array without actually creating one"
    - "**Index conversion**: For a matrix with `n` columns, index `i` maps to row `i // n` and column `i % n`"
    - "**Binary search pattern**: This problem demonstrates how binary search applies to any sorted data structure, not just arrays"
    - "**Related problems**: Search a 2D Matrix II (LeetCode 240) has different constraints requiring a different approach (staircase search)"

  time_complexity: "O(log(m * n)). We perform binary search over `m * n` elements, halving the search space with each iteration."
  space_complexity: "O(1). We only use a constant number of variables (`left`, `right`, `mid`, `row`, `col`) regardless of input size."

solutions:
  - approach_name: Binary Search on Virtual 1D Array
    is_optimal: true
    code: |
      def search_matrix(matrix: list[list[int]], target: int) -> bool:
          if not matrix or not matrix[0]:
              return False

          rows, cols = len(matrix), len(matrix[0])
          # Treat the matrix as a sorted 1D array of length rows * cols
          left, right = 0, rows * cols - 1

          while left <= right:
              # Calculate mid index (avoids overflow in other languages)
              mid = left + (right - left) // 2

              # Convert 1D index to 2D coordinates
              row = mid // cols
              col = mid % cols
              value = matrix[row][col]

              if value == target:
                  return True
              elif value < target:
                  # Target is in the right half
                  left = mid + 1
              else:
                  # Target is in the left half
                  right = mid - 1

          return False
    explanation: |
      **Time Complexity:** O(log(m * n)) — Standard binary search over m * n elements.

      **Space Complexity:** O(1) — Only uses constant extra space.

      We treat the 2D matrix as a virtual 1D sorted array. By converting indices on the fly (`row = mid // cols`, `col = mid % cols`), we can perform standard binary search without creating an actual flattened array.

  - approach_name: Two Binary Searches
    is_optimal: false
    code: |
      def search_matrix(matrix: list[list[int]], target: int) -> bool:
          if not matrix or not matrix[0]:
              return False

          rows, cols = len(matrix), len(matrix[0])

          # First binary search: find the row where target could be
          top, bottom = 0, rows - 1
          while top <= bottom:
              mid_row = top + (bottom - top) // 2
              # Check if target is in this row's range
              if matrix[mid_row][0] <= target <= matrix[mid_row][cols - 1]:
                  # Target could be in this row, search within it
                  left, right = 0, cols - 1
                  while left <= right:
                      mid_col = left + (right - left) // 2
                      if matrix[mid_row][mid_col] == target:
                          return True
                      elif matrix[mid_row][mid_col] < target:
                          left = mid_col + 1
                      else:
                          right = mid_col - 1
                  return False
              elif matrix[mid_row][0] > target:
                  bottom = mid_row - 1
              else:
                  top = mid_row + 1

          return False
    explanation: |
      **Time Complexity:** O(log m + log n) = O(log(m * n)) — Binary search for row, then within row.

      **Space Complexity:** O(1) — Only uses constant extra space.

      This approach first finds the correct row using binary search on the first elements of each row, then searches within that row. While it has the same complexity, it's more verbose than the single binary search approach.

  - approach_name: Linear Search
    is_optimal: false
    code: |
      def search_matrix(matrix: list[list[int]], target: int) -> bool:
          # Simple but inefficient - O(m * n) time
          for row in matrix:
              for val in row:
                  if val == target:
                      return True
          return False
    explanation: |
      **Time Complexity:** O(m * n) — Checks every element in the worst case.

      **Space Complexity:** O(1) — Only uses constant extra space.

      This brute force approach checks every element. While simple, it doesn't meet the problem's requirement for O(log(m * n)) complexity. Included to illustrate why binary search is necessary.
