Problem overview
The method searchMatrix receives a matrix as an Array[Array[Int]], an array of rows, and an integer target. It returns true when the target appears somewhere in the matrix and false otherwise. Two properties hold: every row is sorted in non-decreasing order, and the first value of each row is greater than the last value of the row above it.
Take the matrix with rows [1, 3, 5, 7], [10, 11, 16, 20] and [23, 30, 34, 60]. The target 3 is in row 0, column 1, so the answer is true. The target 13 would sit between 11 and 16 in row 1, but it isn't there, so the answer is false.
Approach: treat the matrix as one sorted list
Checking every cell costs O(m · n). The statement asks for O(log(m · n)) time and O(1) extra space, which points to binary search.
The two properties together mean that reading the rows one after another gives a single sorted list of m * n values. For the example it is 1, 3, 5, 7, 10, 11, 16, 20, 23, 30, 34, 60. We don't build that list, because copying it would cost O(m · n) space. We only imagine it, and binary search the positions 0 to m * n - 1. The one thing we need is a way to turn a position into a cell.
Every row holds n values, so the positions before middle fill middle / n whole rows and middle % n cells of the next one. Row is middle / n, column is middle % n. With 4 columns, position 9 is row 2, column 1, which holds 30.
Each pass compares the value at the middle position with the target and throws away half of the positions, so the loop runs about log2(m · n) times: around 20 passes for a million cells.
Step by step
- Store the dimensions.
mismatrix.length, the number of rows, andnismatrix(0).length. Every row has the same length, so the first row tells the number of columns. - Set the search bounds.
leftis 0 andrightism * n - 1, the last position. Both arevars because the search moves them, and both are inclusive. - Loop while
left <= right. When the two are equal, one position is still unchecked, so a strict<would skip it. - Find the middle as
left + (right - left) / 2. Integer division rounds down, and adding half the distance toleftcan't overflow the wayleft + rightcan. - Convert the position to a cell and read its value: row
middle / n, columnmiddle % n. Scala indexes with parentheses, row first. - Return
trueif the value equals the target. - If the value is smaller, the target can only be later, so
leftbecomesmiddle + 1. - Otherwise the value is bigger, so
rightbecomesmiddle - 1. - Return
falseafter the loop: every position that could hold the target has been ruled out.
The solution in Scala
class Solution {
def searchMatrix(matrix: Array[Array[Int]], target: Int): Boolean = {
val m = matrix.length
val n = matrix(0).length
var left = 0
var right = m * n - 1
while (left <= right) {
val middle = left + (right - left) / 2
val value = matrix(middle / n)(middle % n)
if (value == target) {
return true
} else if (value < target) {
left = middle + 1
} else {
right = middle - 1
}
}
return false
}
}
The Scala details worth a closer look:
- Two sets of parentheses.
matrix(middle / n)is the row, anArray[Int], and the second pair of parentheses indexes into that row. There is nomatrix[i][j]syntax: in Scala,matrix(i)(j)is a call toapplyon the array, twice. returninside a loop. The last expression of a block is its value, so most Scala code leavesreturnout. Here the earlyreturn trueleaves thewhileloop at once, which is the clearest way to stop the search. Awhileloop itself has no value, so the method still needs its final line.valandvar.m,n,middleandvaluenever change after they are set, so they arevals. Onlyleftandrightmove.- Int division and remainder.
9 / 4is 2 and9 % 4is 1, which is how position 9 becomes row 2, column 1.
Complexity
Time: O(log(m · n)). The range of positions halves on every pass, and each pass does constant work: one division, one remainder and one comparison. This equals O(log m + log n).
Space: O(1). The method keeps a handful of integers. The cell is found by arithmetic, and no flattened copy of the matrix is made.
Edge cases and common mistakes
- A single cell. For
[[5]],leftandrightboth start at 0. Target 5 returnstrueand target 4 returnsfalse. This is where<=matters. - One column or one row. For rows
[1],[3],[5]and target 3,nis 1, so every position is its own row and the column is always 0. For the single row[1, 3, 5],middle / nis always 0. Both work with no special case. - Targets outside the range. 0 is smaller than everything and 61 is bigger than everything in the example. Both end with
falseafter the search runs out of positions. - Swapping the divisor. Dividing by
minstead ofngives wrong cells whenever the matrix isn't square, and may index out of bounds. Rows are found with the number of columns. - Keeping
middlein the range. Writingleft = middleorright = middlecan loop forever, because the position just checked stays in range. Usemiddle + 1andmiddle - 1. - Overflow in the midpoint.
Intis 32 bits and overflow wraps silently in Scala. Withleft = 2000000000andright = 2100000000,(left + right) / 2gives -97483648, whileleft + (right - left) / 2gives 2050000000. The matrix here has at most a million cells, so either form works, but the second form is the safe habit.
Testing
For the example matrix with rows [1, 3, 5, 7], [10, 11, 16, 20] and [23, 30, 34, 60]:
- target 3 gives
true; - target 13 gives
false; - target 30 gives
true: the search checks positions 5, 8, 10 and 9, which hold 11, 23, 34 and 30; - target 1 and target 60, the first and last cells, both give
true; - target 0 and target 61 both give
false.
The matrix [[5]] with target 5 gives true, and with target 4 gives false.
