240. Search a 2D Matrix II
Problem Description
You are given a 2D integer matrix with special sorting properties and need to search for a target value in it.
The matrix has these characteristics:
- Each row is sorted in ascending order from left to right
- Each column is sorted in ascending order from top to bottom
Your task is to write an efficient algorithm that determines whether a given target
value exists in the matrix. The function should return true
if the target is found, and false
otherwise.
For example, consider this matrix:
[1, 4, 7, 11, 15] [2, 5, 8, 12, 19] [3, 6, 9, 16, 22] [10, 13, 14, 17, 24] [18, 21, 23, 26, 30]
If target = 5
, the function should return true
since 5 exists in the matrix.
If target = 20
, the function should return false
since 20 does not exist in the matrix.
The challenge is to leverage the sorted properties of the matrix to create an efficient search algorithm rather than checking every element one by one.
Intuition
Since each row is sorted in ascending order, we can take advantage of this property to search more efficiently than checking every element. When we look at any single row, it's just like searching in a regular sorted array - we can use binary search!
The key insight is that we don't need to worry about the column-wise sorting property for this particular approach. We can treat the problem as searching through multiple sorted arrays (each row being one sorted array).
For each row, we can use binary search to quickly determine if the target exists in that row. Binary search allows us to find an element in a sorted array in O(log n)
time instead of O(n)
time for linear search.
The algorithm becomes straightforward:
- Go through each row one by one
- For each row, use binary search to look for the target
- If we find the target in any row, we immediately return
true
- If we've checked all rows and haven't found the target, return
false
The bisect_left
function in Python performs binary search and returns the leftmost position where target
should be inserted to keep the array sorted. If the element at that position equals our target, we've found it. If not, the target doesn't exist in that row.
This approach is more efficient than a naive search because instead of checking all m × n
elements, we're doing m
binary searches, each taking O(log n)
time, giving us O(m × log n)
time complexity.
Solution Approach
The solution implements a binary search approach for each row of the matrix. Here's how the implementation works:
Algorithm Steps:
-
Iterate through each row: We loop through every row in the matrix using a simple for loop.
-
Apply binary search on each row: For each row, we use Python's
bisect_left
function, which performs binary search. The functionbisect_left(row, target)
returns the leftmost indexj
wheretarget
should be inserted in the sorted arrayrow
to maintain sorted order. -
Check if target is found: After getting index
j
frombisect_left
:- First, we verify that
j < len(matrix[0])
to ensure we're within bounds (sincebisect_left
could return an index equal to the array length if the target is larger than all elements) - Then we check if
row[j] == target
to confirm we've found the target value - If both conditions are true, we immediately return
True
- First, we verify that
-
Return result: If we've searched all rows without finding the target, we return
False
.
Key Implementation Details:
bisect_left(row, target)
performs binary search inO(log n)
time for each row- The boundary check
j < len(matrix[0])
prevents index out of bounds errors - Early return optimization: As soon as we find the target in any row, we return
True
without checking remaining rows
Time Complexity: O(m × log n)
where m
is the number of rows and n
is the number of columns. We perform binary search (O(log n)
) for each of the m
rows.
Space Complexity: O(1)
as we only use a constant amount of extra space for variables j
and the loop iterator.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider this 3×4 matrix and search for target = 7
:
[1, 3, 5, 7] [2, 4, 6, 8] [10, 11, 12, 13]
Step-by-step execution:
Row 0: [1, 3, 5, 7]
- Apply
bisect_left([1, 3, 5, 7], 7)
- Binary search process:
- Middle element at index 1 is 3 (3 < 7), search right half
- Middle element at index 2 is 5 (5 < 7), search right half
- Middle element at index 3 is 7 (7 == 7), but bisect_left returns leftmost position
bisect_left
returns index 3- Check: Is 3 < 4 (length of row)? Yes ✓
- Check: Is row[3] == 7? Yes, 7 == 7 ✓
- Target found! Return True
Let's see another example where target = 9
(not in matrix):
Row 0: [1, 3, 5, 7]
bisect_left([1, 3, 5, 7], 9)
returns index 4- Check: Is 4 < 4? No ✗
- Target not in this row, continue to next row
Row 1: [2, 4, 6, 8]
bisect_left([2, 4, 6, 8], 9)
returns index 4- Check: Is 4 < 4? No ✗
- Target not in this row, continue to next row
Row 2: [10, 11, 12, 13]
bisect_left([10, 11, 12, 13], 9)
returns index 0- Check: Is 0 < 4? Yes ✓
- Check: Is row[0] == 9? No, 10 ≠ 9 ✗
- Target not in this row
All rows checked, return False
This example demonstrates how binary search efficiently narrows down the search space in each row, and how the boundary checks ensure we don't access invalid indices.
Solution Implementation
1from typing import List
2from bisect import bisect_left
3
4
5class Solution:
6 def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
7 """
8 Search for a target value in a 2D matrix.
9 Each row is searched using binary search.
10
11 Args:
12 matrix: 2D list of integers
13 target: Integer value to search for
14
15 Returns:
16 True if target is found, False otherwise
17 """
18 # Iterate through each row in the matrix
19 for row in matrix:
20 # Use binary search to find the leftmost position where target could be inserted
21 position = bisect_left(row, target)
22
23 # Check if the position is valid and contains the target value
24 if position < len(row) and row[position] == target:
25 return True
26
27 # Target not found in any row
28 return False
29
1class Solution {
2 /**
3 * Searches for a target value in a 2D matrix.
4 * Each row in the matrix is searched using binary search.
5 *
6 * @param matrix The 2D integer matrix to search in
7 * @param target The target value to find
8 * @return true if the target is found, false otherwise
9 */
10 public boolean searchMatrix(int[][] matrix, int target) {
11 // Iterate through each row in the matrix
12 for (int[] currentRow : matrix) {
13 // Perform binary search on the current row
14 int searchResult = Arrays.binarySearch(currentRow, target);
15
16 // If the result is non-negative, the target was found
17 if (searchResult >= 0) {
18 return true;
19 }
20 }
21
22 // Target was not found in any row
23 return false;
24 }
25}
26
1class Solution {
2public:
3 bool searchMatrix(vector<vector<int>>& matrix, int target) {
4 // Iterate through each row of the matrix
5 for (auto& row : matrix) {
6 // Use binary search to find the lower bound position of target in current row
7 // lower_bound returns an iterator to the first element >= target
8 int columnIndex = lower_bound(row.begin(), row.end(), target) - row.begin();
9
10 // Check if the found position is within bounds and contains the target value
11 if (columnIndex < matrix[0].size() && row[columnIndex] == target) {
12 return true; // Target found
13 }
14 }
15
16 // Target not found in any row
17 return false;
18 }
19};
20
1/**
2 * Searches for a target value in a 2D matrix
3 * Each row is sorted in ascending order
4 * @param matrix - 2D array of numbers to search in
5 * @param target - The value to search for
6 * @returns true if target is found, false otherwise
7 */
8function searchMatrix(matrix: number[][], target: number): boolean {
9 // Get the number of columns in the matrix
10 const columnCount = matrix[0].length;
11
12 // Iterate through each row in the matrix
13 for (const row of matrix) {
14 // Find the insertion index for target in the sorted row using binary search
15 const insertionIndex = binarySearchIndex(row, target);
16
17 // Check if the target exists at the found index
18 if (insertionIndex < columnCount && row[insertionIndex] === target) {
19 return true;
20 }
21 }
22
23 // Target not found in any row
24 return false;
25}
26
27/**
28 * Finds the index where a value should be inserted in a sorted array
29 * Uses binary search to maintain O(log n) complexity
30 * @param sortedArray - Array sorted in ascending order
31 * @param value - Value to find insertion index for
32 * @returns The index where value should be inserted to maintain sorted order
33 */
34function binarySearchIndex(sortedArray: number[], value: number): number {
35 let left = 0;
36 let right = sortedArray.length;
37
38 // Binary search to find insertion position
39 while (left < right) {
40 const mid = Math.floor((left + right) / 2);
41
42 if (sortedArray[mid] < value) {
43 left = mid + 1;
44 } else {
45 right = mid;
46 }
47 }
48
49 return left;
50}
51
Time and Space Complexity
The time complexity is O(m × log n)
, where m
is the number of rows and n
is the number of columns in the matrix. This is because:
- The outer loop iterates through all
m
rows of the matrix - For each row,
bisect_left
performs a binary search which takesO(log n)
time (since each row hasn
elements) - Therefore, the total time complexity is
O(m) × O(log n) = O(m × log n)
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space:
- The variable
j
stores the index returned bybisect_left
- No additional data structures are created that scale with the input size
- The iteration through the matrix doesn't require extra space proportional to the input
Common Pitfalls
Pitfall 1: Suboptimal Time Complexity
The current solution uses binary search on each row independently, resulting in O(m × log n) time complexity. However, this approach doesn't fully leverage the fact that columns are also sorted. We're essentially treating this as m separate sorted arrays rather than exploiting the 2D sorted structure.
Why it's problematic: For large matrices, we might unnecessarily search through many rows even when the column sorting could help us eliminate entire regions of the matrix.
Pitfall 2: Missing Early Termination Opportunities
The current approach doesn't skip rows that clearly cannot contain the target based on their range. For instance, if a row's first element is greater than the target, we still perform binary search on it.
Better Solution: Staircase Search Algorithm
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
if not matrix or not matrix[0]:
return False
# Start from top-right corner
row = 0
col = len(matrix[0]) - 1
while row < len(matrix) and col >= 0:
current = matrix[row][col]
if current == target:
return True
elif current > target:
# Current element is too large, move left
col -= 1
else:
# Current element is too small, move down
row += 1
return False
Why this is better:
- Time Complexity: O(m + n) instead of O(m × log n)
- Fully utilizes both row and column sorting properties
- Eliminates impossible regions: At each step, we eliminate either an entire row or column
- Simpler implementation: No need for binary search library
Alternative starting position: You could also start from the bottom-left corner with similar logic (moving up when current > target, right when current < target).
Pitfall 3: Not Considering Edge Cases
The original solution doesn't explicitly handle edge cases like:
- Empty matrix
[]
- Matrix with empty rows
[[]]
- Single element matrix
[[5]]
While Python's for
loop naturally handles empty iterations, explicit checks make the code more robust and clear about intent.
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!