Problem overview
The function searchMatrix receives matrix, a slice of rows where each row is a []int, and an integer target. It returns true if target appears anywhere in the matrix and false otherwise.
The matrix is special. Every row is sorted in non-decreasing order, and each row starts with a value 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 in row 1, and it isn't there. With target = 30 it is true (row 2, column 1).
The lesson 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. Checking every cell would cost O(m · n).
Approach: one sorted list in disguise
The two properties together mean something simple: if you read the matrix row by row, left to right, the values never go down. The example reads as 1, 3, 5, 7, 10, 11, 16, 20, 23, 30, 34, 60, one sorted list of 12 values. Binary search works on any sorted sequence, so it works here.
The catch is that the list doesn't exist in memory; the data is a slice of slices. Building a flat copy would take O(m · n) time and space and defeat the purpose. Instead, the search works on positions from 0 to m * n - 1 and converts a position into a row and a column only when it needs to read a value.
The conversion is plain arithmetic. Every row holds n values, so the positions before p fill p / n whole rows, and p % n more cells of the next row. That makes the row p / n and the column p % n. With 4 columns, position 9 is row 2, column 1, which is where the 30 lives. Both operations use integer division, which rounds down for non-negative numbers, so no floating point is involved.
Step by step
- Measure the matrix.
mis the number of rows,len(matrix), andnis the length of the first row. Every row has the same length, so the first one speaks for all of them. - Set the search range to every position:
leftstarts at 0 andrightatm*n-1. Both ends are inclusive, sorightis the last position itself. - Loop while the range is non-empty. Go has no
while; aforwith only a condition does that job. The condition isleft <= right, because when the two are equal one position is still unchecked. - Pick the middle position as
left + (right-left)/2. It rounds down, and unlike(left+right)/2the sum can't overflow in languages with fixed-width integers. Go'sintis 64 bits so the lesson's sizes are safe either way, but the habit costs nothing. - Turn the position into a cell with
middle/nandmiddle%n, and read the value there. - Found it? If the value equals
target, returntrue. - Value too small? Then
targetcan only be at a later position, so movelefttomiddle + 1. The value atmiddlewas already checked, so keeping it in the range would risk an endless loop. - Value too big? Move
righttomiddle - 1. - Range empty? When the loop ends, every position has been ruled out, so return
false.
The solution in Go
func searchMatrix(matrix [][]int, target int) bool {
m, n := len(matrix), len(matrix[0])
left, right := 0, m*n-1
for left <= right {
middle := left + (right-left)/2
value := matrix[middle/n][middle%n]
if value == target {
return true
} else if value < target {
left = middle + 1
} else {
right = middle - 1
}
}
return false
}
A few Go details:
- Multiple assignment with
:=.m, n := len(matrix), len(matrix[0])declares both variables in one statement, and so doesleft, right := 0, m*n-1. All the right-hand expressions are evaluated first. - A
[][]intis a slice of slices.matrix[row][col]indexes the outer slice and then the row. Go checks both indexes at run time and panics when one is out of range. - Integer
/and%. Between twoints,/drops the fraction and%gives the remainder. For the non-negative positions used here,p / nandp % nare exactly the row and column. - No parentheses around conditions. Go's
forandiftake bare conditions, and the braces are required.
Here is the search for 30 in the example matrix, printing what each pass reads:
left=0 right=11 middle=5 row=1 col=1 value=11
left=6 right=11 middle=8 row=2 col=0 value=23
left=9 right=11 middle=10 row=2 col=2 value=34
left=9 right=9 middle=9 row=2 col=1 value=30
It takes four reads out of 12 cells. The last pass has left == right, which shows why the loop condition needs <=. For 13 the search reads 11, 23 and 16, then left (6) passes right (5) and the function returns false.
Complexity
Time: O(log(m · n)). Each pass discards at least half of the remaining positions, so the loop runs about log2(m · n) times: around 20 passes for a million cells. Each pass does a constant amount of work, because reading a cell is just two index operations.
Space: O(1). The function keeps a handful of integers. It never builds the flat list; the cell is found by arithmetic.
Edge cases and common mistakes
- A single cell. For
[[5]], the range is just position 0. Target 5 returnstrueand target 4 returnsfalse. - One row or one column.
[[1, 2, 3]](m = 1) and[[1], [3], [5]](n = 1) both work with the same code: with one column,middle/nismiddleandmiddle%nis 0. - Using
left < right. When one position is left, a strict comparison skips it, so a target at that position, like the 30 above, would be reported missing. - Setting
left = middleorright = middle. The value atmiddlehas been ruled out. Keeping it can make the range stop shrinking, and the loop never ends. - Swapping
mandn. The row ismiddle / n, withnthe number of columns, not rows. Dividing bymreads the wrong cells or goes out of range. - Targets outside the range. A target below the first value (0 in the example) or above the last one (61) simply runs the range down to empty and returns
false. - An empty matrix.
matrix[0]panics for a slice with no rows. The lesson guarantees at least one row and one column, so the solution doesn't guard against it; in code of your own, checklen(matrix) == 0first.
Testing
With the example matrix, the function returns these results:
- target
3givestrue. - target
13givesfalse. - target
30givestrue. - target
1(the first value) givestrue, and target60(the last) givestrue. - target
0and target61both givefalse.
Other shapes: [[5]] with 5 gives true, with 4 gives false; [[1, 2, 3]] with 3 gives true; [[1], [3], [5]] with 3 gives true.
