Problem overview
The method SearchMatrix receives a matrix of integers, written as an int[][] (an array of rows, each row an array of int), and a target. It returns true when the target is somewhere 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 above. In the example, the matrix is {1,3,5,7}, {10,11,16,20}, {23,30,34,60}. Target 3 gives true, target 13 gives false (it would sit between 11 and 16, but isn't there) and target 30 gives true.
The lesson asks for O(log(m · n)) time and O(1) extra space, so looking at every cell is too slow.
Approach: one sorted list in disguise
Read the rows one after another and the matrix becomes a single sorted list of m * n values: 1, 3, 5, 7, 10, 11, 16, 20, 23, 30, 34, 60. The second property is what makes this true, because each row starts above where the previous one ends. Binary search works on any sorted sequence, so it works here.
The catch is that the list doesn't exist in memory. The values live in rows, and building a flat copy would cost O(m · n) space. The fix is to binary search over positions 0 to m * n - 1 and translate a position into a cell only when you need its value. With n columns, position p is in row p / n (the number of whole rows before it) and column p % n (the cells left over in its own row). With 4 columns, position 9 is row 2, column 1, which holds 30.
Searching for 13, the loop looks at position 5 (value 11, too small, so move right), then position 8 (value 23, too big, so move left), then position 6 (value 16, too big). The range is now empty, so the answer is false.
Step by step
- Read the sizes.
mismatrix.Length, the number of rows, andnismatrix[0].Length, the length of the first row. Every row has the same length, so the first row speaks for all of them. - Set the search range to the positions of the flat list:
left = 0andright = m * n - 1. Both ends are inclusive, sorightis the last position itself. - Loop while
left <= right. When the two are equal, one position is still unchecked, so the condition needs<=. - Pick the middle:
left + (right - left) / 2. Integer division rounds down, and adding half the distance toleftgives the same position as(left + right) / 2without the risk of the sum overflowing. - Convert the position to a cell with
middle / nandmiddle % n, and read the value there. - Compare. If the value equals the target, return
true. If it is smaller, the target can only be at a later position, soleft = middle + 1. Otherwise it is bigger, soright = middle - 1. The+ 1and- 1matter:middlewas already checked, and keeping it in the range could loop forever. - Return
falseafter the loop. Reaching that line means every position that could hold the target was ruled out.
The solution in C#
class Solution {
public bool SearchMatrix(int[][] matrix, int target) {
int m = matrix.Length, n = matrix[0].Length;
int left = 0, right = m * n - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
int 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 C# details worth knowing:
- Jagged, not rectangular.
int[][]is an array of arrays, indexedmatrix[row][column]. A rectangular arrayint[,]is indexedmatrix[row, column]and reports its sizes withGetLength(0)andGetLength(1). Herematrix.Lengthcounts rows andmatrix[0].Lengthcounts columns. - Integer division and remainder.
/on twoints drops the fraction, which is what turns a position into a row number.%gives the remainder, which is the column. - Overflow wraps silently. Without a
checkedcontext,intarithmetic that goes past 2147483647 wraps around. Withleft = 2000000000andright = 2100000000,(left + right) / 2gives -97483648, whileleft + (right - left) / 2gives 2050000000. For this lessonm * nis at most a million, so either form is safe, but the habit costs nothing. - The method is a pure lookup. It reads the matrix and never changes it.
Complexity
Time: O(log(m · n)). Each pass of the loop halves the number of positions still possible, so the loop runs about log2(m · n) times: around 20 for a million cells. Converting a position to a cell is a division and a remainder, which take constant time.
Space: O(1). The method keeps a handful of int variables. It never builds the flat list, because the cell is found by arithmetic.
Searching row by row would take O(m · n). Binary searching first for the row and then inside it is also O(log m + log n), which is the same as O(log(m · n)). The single search over positions needs only one loop.
Edge cases and common mistakes
- One cell.
{{5}}with target 5 givestrue, and with target 4 givesfalse. The loop runs once, which is why the condition is<=. - One column.
{{1},{3},{5}}with target 3 givestrue. Herenis 1, somiddle / nis the position itself andmiddle % nis always 0. - Targets outside the range. The smallest value (1) and the largest (60) are found, while 0 and 61 give
falsewithout any out-of-range access. - Swapping the row and column formulas. Using
middle % nfor the row andmiddle / nfor the column reads the wrong cell, or goes past the end of an array and throwsIndexOutOfRangeException. - Dividing by
minstead ofn. The divisor is the row length, the number of columns, not the number of rows. - Using
middleinstead ofmiddle + 1ormiddle - 1. The range then never shrinks in some cases, and the loop runs forever.
Testing
With the matrix {1,3,5,7}, {10,11,16,20}, {23,30,34,60}:
- target 3 gives
true, target 13 givesfalse, target 30 givestrue. - target 1 gives
true, target 60 givestrue, target 0 givesfalse, target 61 givesfalse. {{5}}with 5 givestrue; with 4 givesfalse.{{1},{3},{5}}with 3 givestrue.
