Problem overview
The method searchMatrix receives matrix, an array of rows where each row is an array of numbers, and a number target. It returns true if target appears anywhere in the matrix and false otherwise.
The matrix has two properties. Every row is sorted in non-decreasing order, and the first value of each row is greater than the last value of the row before it. Take this matrix:
[
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 60]
]
With target = 3 the answer is true (row 0, column 1). With target = 13 it is false: 13 would sit between 11 and 16, but it isn't there. With target = 30 it is true (row 2, column 1).
The statement asks for O(log(m · n)) time and O(1) extra space, where m is the number of rows and n the number of columns.
Approach: one sorted list in disguise
Checking every cell works, but it takes O(m · n) time. The two properties allow much more. Read the rows one after another and the values never go down: the last value of a row is smaller than the first value of the next row. So the whole matrix behaves like a single sorted list of m * n values. For the matrix above that list is 1, 3, 5, 7, 10, 11, 16, 20, 23, 30, 34, 60.
A sorted list can be searched with binary search: look at the middle, discard the half that cannot contain the target, repeat. The only obstacle is that the list doesn't exist as an array. We don't need to build it, since building it would cost O(m · n) time and space. Instead, we binary search over the positions 0 to m * n - 1 and convert a position into a row and a column whenever we need its value.
The conversion uses the row length. Every row holds n values, so the positions before position p fill Math.floor(p / n) complete rows, and p % n more cells of the next one. That makes the row Math.floor(p / n) and the column p % n. With 4 columns, position 9 is row 2, column 1.
Step by step
- Read the dimensions. The number of rows is
matrix.length. Every row has the same length, so the number of columns is the length of the first row. - Set the search range to the positions of the imagined list:
leftis 0 andrightism * n - 1. Both ends are inclusive, sorightis the last position itself. - Loop while
left <= right. With<=, a range holding a single position still gets checked. A strict<would skip it. - Find the middle position. Take
leftplus half the distance toright, rounded down withMath.floor. In JavaScript,/never truncates, so7 / 2is3.5and a position needs the explicit rounding. - Turn the position into a cell. The row is
Math.floor(middle / n)and the column ismiddle % n. Store the value found there. - If the value equals
target, returntrue. - If the value is smaller, the target can only be later, so move
lefttomiddle + 1. Usingmiddle + 1rather thanmiddlematters: the value atmiddleis already ruled out, and keeping it in the range could make the loop run forever. - Otherwise the value is bigger, so move
righttomiddle - 1. - After the loop, no position is left to check, so return
false.
The solution in JavaScript
class Solution {
searchMatrix(matrix, target) {
let m = matrix.length, n = matrix[0].length;
let left = 0, right = m * n - 1;
while (left <= right) {
let middle = Math.floor(left + (right - left) / 2);
let value = matrix[Math.floor(middle / n)][middle % n];
if (value == target) {
return true;
} else if (value < target) {
left = middle + 1;
} else {
right = middle - 1;
}
}
return false;
}
}
Some JavaScript details worth a closer look:
- Division gives a fraction.
9 / 4is2.25, and an array can't be indexed with2.25:matrix[2.25]isundefined, and reading[1]from it throws aTypeError. That's why the row isMath.floor(middle / n). The columnmiddle % nis already a whole number whenmiddleis. ==and===. The solution compares with==, which converts types when they differ. Both sides are numbers here, so it behaves like===, and===is the safer habit in general.- Two comma-separated declarations.
let m = ..., n = ...;declares both variables in one statement. It is the same as two separateletlines. - Numbers are doubles. The positions here stay far below 2^53 (at most a million for a 1000 by 1000 matrix), so every position is exact.
Tracing target = 30
The matrix above has m = 3 and n = 4, so the positions run from 0 to 11. Searching for 30 gives this trace:
| left | right | middle | row | column | value | decision |
|---|---|---|---|---|---|---|
| 0 | 11 | 5 | 1 | 1 | 11 | too small, left = 6 |
| 6 | 11 | 8 | 2 | 0 | 23 | too small, left = 9 |
| 9 | 11 | 10 | 2 | 2 | 34 | too big, right = 9 |
| 9 | 9 | 9 | 2 | 1 | 30 | found, return true |
Complexity
Time: O(log(m · n)). Each pass of the loop halves the number of positions still in play, so the loop runs about log2(m · n) times: around 20 passes for a million cells. Each pass does a fixed amount of work.
Space: O(1). The code keeps a handful of numbers. It finds a cell by arithmetic and never copies the matrix into a flat array.
Edge cases and common mistakes
- A single cell.
[[5]]with target 5 returnstrue, and with target 4 it returnsfalse. The range holds one position, which only the<=loop condition checks. - A single column or a single row.
[[1], [3], [5]]with target 3 returnstrue, and[[1, 3, 5]]with target 4 returnsfalse. The arithmetic works for any shape. - Targets outside the range. A target below the first value (such as 0) or above the last (such as 61) returns
falseafter the range shrinks to nothing. - Using the number of rows where the number of columns belongs. The row is
middle / nand the column ismiddle % n, withnthe row length. Swapping the roles points at the wrong cells. - Forgetting
Math.floor. The row index becomes a fraction and the lookup fails. - Moving to
middleinstead ofmiddle + 1ormiddle - 1. The range can stop shrinking, and the loop never ends. - Searching row by row with a full scan inside. That is correct but costs
O(m · n), which the statement rules out.
Testing
With the 3 by 4 matrix from the problem overview:
- target
3givestrue. - target
13givesfalse. - target
30givestrue. - target
1(the first value) givestrue. - target
60(the last value) givestrue. - target
0givesfalse. - target
61givesfalse.
