Builds on Converging Pointers, which you haven’t finished yet.
Saddleback Search (Staircase Search in a Sorted Matrix)
matrix has m rows and n columns. Every row is sorted in ascending order from left to right, and every column is sorted in ascending order from top to bottom (values may repeat). Return true if target appears anywhere in matrix, otherwise false.
Staircase Search
row- and column-sorted matrix ➔ start at the top-right corner ➔ drop a row or a column per step| 1 | def search_matrix(matrix: list[list[int]], target: int) -> bool: |
| 2 | """Search a 2D Matrix II: walk from the top-right corner, dropping a row or column per step. |
| 3 | Time: O(M + N) | Space: O(1) |
| 4 | """ |
| 5 | if not matrix or not matrix[0]: |
| 6 | return False |
| 7 | |
| 8 | # row and col are the two pointers: row only moves down and col only moves left, so together they walk a staircase of cells |
| 9 | row, col = 0, len(matrix[0]) - 1 # Top-right corner: the largest in its row, the smallest in its column |
| 10 | |
| 11 | # 1. Drop One Line Per Step: |
| 12 | # Among the cells that are left, the current value is the largest in its row and the smallest in its column, so one comparison with target rules out a whole column or a whole row. A dropped line never comes back. |
| 13 | while row < len(matrix) and col >= 0: |
| 14 | val = matrix[row][col] |
| 15 | if val == target: |
| 16 | return True |
| 17 | if val > target: |
| 18 | col -= 1 # Everything below in this column is even bigger: drop the column |
| 19 | else: |
| 20 | row += 1 # Everything left in this row is even smaller: drop the row |
| 21 | |
| 22 | # 2. Report That The Target Is Missing: |
| 23 | # The walk left the matrix, so every row and every column that could hold target was dropped. |
| 24 | return False |
target is in matrix, it is in a row at or below row and a column at or left of col.The top-right value is the largest in its row and the smallest in its column, so start there: one comparison with target rules out a whole line. If val > target, everything below it in that column is even bigger, so col -= 1. If val < target, everything to its left in that row is even smaller, so row += 1. This is Converging Pointers on a grid: row only moves down and col only moves left, and a dropped line never comes back. The walk ends when it finds target or steps off the matrix, after at most M + N steps. Nothing is halved, so this is an elimination walk, not a binary search. The trap is the starting corner: start only at the top-right or the bottom-left corner. At the top-left corner both moves make the value bigger, so no comparison can rule anything out.
See it run
Search a 2D Matrix II: search_matrix(matrix, 23)
row 0col 4val 15
Each row and each column increases. A dot marks a cell in a row or column that earlier steps already dropped.
valthe cell compared with target in this steptop-lefta start corner that cannot rule out a linefoundthe cell equal to target
Start: row = 0 and col = 4, the top-right corner. The target is 23.
Gotcha 1 of 3
- The trap
Gotcha 1: Starting the walk at the top-left corner
Mistakerow, col = 0, 0Fixrow, col = 0, len(matrix[0]) - 1What happens: At
(0, 0), moving down and moving right both lead to larger values. So when the value is too small, one comparison cannot rule out a whole line. The top-right value is the largest in its row and the smallest in its column. There, each comparison rules out one row or one column. The bottom-left corner works too.Example: Search a 2D Matrix II (LC 240), Example 1, target = 5Mistake: returns FalseFix: returns True
- Each row starts after the previous row ends, as in Search a 2D Matrix (LC 74): read row by row the grid is one sorted list, so a binary search can drop half of the cells per comparison, not one row or column. See Global Matrix Bisection
- You must find the k-th smallest value, not whether one given value is present, as in Kth Smallest Number in Multiplication Table (LC 668). See Kth Smallest
Deep Dive
Why the top-right corner rules out a whole line
Converging Pointers already teaches that two pointers can start at two ends and discard one end per comparison. This card adds only the grid version: the two pointers are row and col, and each comparison discards a whole row or a whole column.
In Search a 2D Matrix II (LC 240), every row increases from left to right, and every column increases from top to bottom. The top-right value is the largest in its row, and it is the smallest in its column.
If val > target, the values below val in the same column are larger than val. None of them can equal target, so col -= 1 drops the column. If val < target, the values to the left of val in the same row are smaller than val. None of them can equal target, so row += 1 drops the row.
After a drop, the cells that are left form a smaller rectangle. Its top-right cell has the same property, so the next comparison works in the same way.
Tracing the 5 by 5 matrix with target = 23
The board shows the matrix of Example 1 of Search a 2D Matrix II (LC 240). A dot marks a cell in a row or column that an earlier step dropped. The tag val marks the cell that is compared.
The walk starts at 15. It is smaller than 23, so row += 1. The values 19 and 22 are also smaller, so row becomes 3. Then 24 is larger than 23, so col becomes 3.
The value 17 is smaller, so row becomes 4. The value 26 is larger, so col becomes 2. The value at row 4 and column 2 is 23, which equals target, so the function returns True.
The walk read 7 cells of a 25-cell matrix. If target is 20, which is not in the matrix, the walk reads 9 cells. It ends when row passes the last row. For the same matrix, target = 5 returns True and target = 20 returns False.
Why the corner matters, and why this is not binary search
The start corner must be the top-right or the bottom-left corner. At the top-left corner, moving right and moving down both lead to larger values. In the trace, 1 < 23 does not say whether 23 is to the right or below. A top-left walk that moves right when val < target and down otherwise returns False for target = 5 on the traced matrix, although 5 is in it.
At the top-right corner, the two moves lead to different directions of change: left is smaller and down is larger. This is why each comparison has exactly one safe drop.
The loop condition is row < len(matrix) and col >= 0. Column 0 is a real column, so col > 0 would end the walk before it reads column 0. A target that is only in column 0, such as 18 in the trace, would be reported missing.
No comparison removes half of the cells, so this is not a binary search. Each comparison removes one line. Search a 2D Matrix (LC 74) is different: each row starts after the previous row ends, so the whole grid reads as one sorted list, and a binary search on that list is faster.
The cost, and where the walk appears
Each step moves row down or col left, and neither moves back. A matrix with M rows and N columns allows at most M + N - 1 reads, so the time is O(M + N). The extra space is O(1).
The same walk follows the border between small and large values in other sorted grids. Count Negative Numbers in a Sorted Matrix (LC 1351) walks the same way on a matrix sorted in decreasing order, and counts a whole column at once when a value is negative. Leftmost Column with at Least a One (LC 1428) walks along the border of the zeros and the ones.
Find Positive Integer Solution for a Given Equation (LC 1237) does not give a matrix. It gives a function that increases in both arguments. The two arguments play the roles of row and col.
60-Second Pitch
"Rows and columns are both sorted, so I start at the top-right corner. That value is the largest in its row and the smallest in its column. If it equals target, I return True. If it is larger, everything below it in its column is larger, so I move col left. If it is smaller, everything left of it in its row is smaller, so I move row down. Each step drops a whole row or a whole column, and neither pointer moves back. When a pointer leaves the matrix, I return False. The time is O(M + N), and the space is O(1). I cannot use the top-left corner, because both moves lead to larger values there."
Common Questions
- What is the time and space complexity?
The time is O(M + N) for M rows and N columns, because every step moves
rowdown orcolleft, and the walk makes at most M + N - 1 reads. The space isO(1).- Can you start at the bottom-left corner?
Yes. The bottom-left value is the largest in its column and the smallest in its row. If
val > target, move up. Ifval < target, move right. The walk is the mirror image of the top-right walk.- Can binary search do better?
A binary search on each row costs O(M log N). The walk costs O(M + N), which is smaller when the matrix is close to square.
- How is this different from Search a 2D Matrix (LC 74)?
In Search a 2D Matrix (LC 74), each row starts after the previous row ends. Read row by row, the grid is one sorted list, and a binary search on the flat index takes O(log(M * N)) time. In this problem, a row can start below the end of the row above, so there is no flat order.
- How do you count the values below a limit with the same walk?
In Count Negative Numbers in a Sorted Matrix (LC 1351), the rows and columns are sorted in decreasing order. Start at the top-right corner. If the value is negative, this cell and every cell below it in its column are negative, so add the number of those cells and move
colleft. If the value is not negative, moverowdown. The walk is still O(M + N).
SWE Reasoning
Spot it
- The input is a matrix whose rows and columns are both sorted, and the rows do not continue each other.
- The question is whether a value is present, or how many values are smaller or larger than a limit.
- A function increases in two arguments, and you must find the border of a condition.
- Binary search on a flat list does not apply, because the grid is not one sorted list.
The invariant, step by step
row = 0andcol = len(matrix[0]) - 1put the walk at the top-right corner.- If
targetis inmatrix, it is in a row at or belowrowand in a column at or left ofcol. val = matrix[row][col]is the largest in its row and the smallest in its column, among the cells that are left.- If
val == target, returnTrue. - If
val > target, the cells belowvalin its column are larger, socol -= 1. Ifval < target, the cells to the left in its row are smaller, sorow += 1. - Each drop keeps the invariant. When
rowpasses the last row orcoldrops below 0, no cell is left, and the function returnsFalse.
Traps
- Starting at the top-left corner. Both moves lead to larger values, so one comparison cannot rule out a line.
col > 0in the loop condition. Column 0 is never read, and atargetthat is only there is missed.- Moving
rowandcolafter one comparison. The other line is dropped without a check, and it may holdtarget. - Calling it binary search. Each step drops one line, not half of the cells, and the time is O(M + N).
Practice
Builds on Converging Pointers, which you haven’t finished yet.
Saddleback Search (Staircase Search in a Sorted Matrix)
matrix has m rows and n columns. Every row is sorted in ascending order from left to right, and every column is sorted in ascending order from top to bottom (values may repeat). Return true if target appears anywhere in matrix, otherwise false.
Staircase Search
row- and column-sorted matrix ➔ start at the top-right corner ➔ drop a row or a column per step| 1 | def search_matrix(matrix: list[list[int]], target: int) -> bool: |
| 2 | """Search a 2D Matrix II: walk from the top-right corner, dropping a row or column per step. |
| 3 | Time: O(M + N) | Space: O(1) |
| 4 | """ |
| 5 | if not matrix or not matrix[0]: |
| 6 | return False |
| 7 | |
| 8 | # row and col are the two pointers: row only moves down and col only moves left, so together they walk a staircase of cells |
| 9 | row, col = 0, len(matrix[0]) - 1 # Top-right corner: the largest in its row, the smallest in its column |
| 10 | |
| 11 | # 1. Drop One Line Per Step: |
| 12 | # Among the cells that are left, the current value is the largest in its row and the smallest in its column, so one comparison with target rules out a whole column or a whole row. A dropped line never comes back. |
| 13 | while row < len(matrix) and col >= 0: |
| 14 | val = matrix[row][col] |
| 15 | if val == target: |
| 16 | return True |
| 17 | if val > target: |
| 18 | col -= 1 # Everything below in this column is even bigger: drop the column |
| 19 | else: |
| 20 | row += 1 # Everything left in this row is even smaller: drop the row |
| 21 | |
| 22 | # 2. Report That The Target Is Missing: |
| 23 | # The walk left the matrix, so every row and every column that could hold target was dropped. |
| 24 | return False |
target is in matrix, it is in a row at or below row and a column at or left of col.The top-right value is the largest in its row and the smallest in its column, so start there: one comparison with target rules out a whole line. If val > target, everything below it in that column is even bigger, so col -= 1. If val < target, everything to its left in that row is even smaller, so row += 1. This is Converging Pointers on a grid: row only moves down and col only moves left, and a dropped line never comes back. The walk ends when it finds target or steps off the matrix, after at most M + N steps. Nothing is halved, so this is an elimination walk, not a binary search. The trap is the starting corner: start only at the top-right or the bottom-left corner. At the top-left corner both moves make the value bigger, so no comparison can rule anything out.
See it run
Search a 2D Matrix II: search_matrix(matrix, 23)
row 0col 4val 15
Each row and each column increases. A dot marks a cell in a row or column that earlier steps already dropped.
valthe cell compared with target in this steptop-lefta start corner that cannot rule out a linefoundthe cell equal to target
Start: row = 0 and col = 4, the top-right corner. The target is 23.
Gotcha 1 of 3
- The trap
Gotcha 1: Starting the walk at the top-left corner
Mistakerow, col = 0, 0Fixrow, col = 0, len(matrix[0]) - 1What happens: At
(0, 0), moving down and moving right both lead to larger values. So when the value is too small, one comparison cannot rule out a whole line. The top-right value is the largest in its row and the smallest in its column. There, each comparison rules out one row or one column. The bottom-left corner works too.Example: Search a 2D Matrix II (LC 240), Example 1, target = 5Mistake: returns FalseFix: returns True
- Each row starts after the previous row ends, as in Search a 2D Matrix (LC 74): read row by row the grid is one sorted list, so a binary search can drop half of the cells per comparison, not one row or column. See Global Matrix Bisection
- You must find the k-th smallest value, not whether one given value is present, as in Kth Smallest Number in Multiplication Table (LC 668). See Kth Smallest
Deep Dive
Why the top-right corner rules out a whole line
Converging Pointers already teaches that two pointers can start at two ends and discard one end per comparison. This card adds only the grid version: the two pointers are row and col, and each comparison discards a whole row or a whole column.
In Search a 2D Matrix II (LC 240), every row increases from left to right, and every column increases from top to bottom. The top-right value is the largest in its row, and it is the smallest in its column.
If val > target, the values below val in the same column are larger than val. None of them can equal target, so col -= 1 drops the column. If val < target, the values to the left of val in the same row are smaller than val. None of them can equal target, so row += 1 drops the row.
After a drop, the cells that are left form a smaller rectangle. Its top-right cell has the same property, so the next comparison works in the same way.
Tracing the 5 by 5 matrix with target = 23
The board shows the matrix of Example 1 of Search a 2D Matrix II (LC 240). A dot marks a cell in a row or column that an earlier step dropped. The tag val marks the cell that is compared.
The walk starts at 15. It is smaller than 23, so row += 1. The values 19 and 22 are also smaller, so row becomes 3. Then 24 is larger than 23, so col becomes 3.
The value 17 is smaller, so row becomes 4. The value 26 is larger, so col becomes 2. The value at row 4 and column 2 is 23, which equals target, so the function returns True.
The walk read 7 cells of a 25-cell matrix. If target is 20, which is not in the matrix, the walk reads 9 cells. It ends when row passes the last row. For the same matrix, target = 5 returns True and target = 20 returns False.
Why the corner matters, and why this is not binary search
The start corner must be the top-right or the bottom-left corner. At the top-left corner, moving right and moving down both lead to larger values. In the trace, 1 < 23 does not say whether 23 is to the right or below. A top-left walk that moves right when val < target and down otherwise returns False for target = 5 on the traced matrix, although 5 is in it.
At the top-right corner, the two moves lead to different directions of change: left is smaller and down is larger. This is why each comparison has exactly one safe drop.
The loop condition is row < len(matrix) and col >= 0. Column 0 is a real column, so col > 0 would end the walk before it reads column 0. A target that is only in column 0, such as 18 in the trace, would be reported missing.
No comparison removes half of the cells, so this is not a binary search. Each comparison removes one line. Search a 2D Matrix (LC 74) is different: each row starts after the previous row ends, so the whole grid reads as one sorted list, and a binary search on that list is faster.
The cost, and where the walk appears
Each step moves row down or col left, and neither moves back. A matrix with M rows and N columns allows at most M + N - 1 reads, so the time is O(M + N). The extra space is O(1).
The same walk follows the border between small and large values in other sorted grids. Count Negative Numbers in a Sorted Matrix (LC 1351) walks the same way on a matrix sorted in decreasing order, and counts a whole column at once when a value is negative. Leftmost Column with at Least a One (LC 1428) walks along the border of the zeros and the ones.
Find Positive Integer Solution for a Given Equation (LC 1237) does not give a matrix. It gives a function that increases in both arguments. The two arguments play the roles of row and col.
60-Second Pitch
"Rows and columns are both sorted, so I start at the top-right corner. That value is the largest in its row and the smallest in its column. If it equals target, I return True. If it is larger, everything below it in its column is larger, so I move col left. If it is smaller, everything left of it in its row is smaller, so I move row down. Each step drops a whole row or a whole column, and neither pointer moves back. When a pointer leaves the matrix, I return False. The time is O(M + N), and the space is O(1). I cannot use the top-left corner, because both moves lead to larger values there."
Common Questions
- What is the time and space complexity?
The time is O(M + N) for M rows and N columns, because every step moves
rowdown orcolleft, and the walk makes at most M + N - 1 reads. The space isO(1).- Can you start at the bottom-left corner?
Yes. The bottom-left value is the largest in its column and the smallest in its row. If
val > target, move up. Ifval < target, move right. The walk is the mirror image of the top-right walk.- Can binary search do better?
A binary search on each row costs O(M log N). The walk costs O(M + N), which is smaller when the matrix is close to square.
- How is this different from Search a 2D Matrix (LC 74)?
In Search a 2D Matrix (LC 74), each row starts after the previous row ends. Read row by row, the grid is one sorted list, and a binary search on the flat index takes O(log(M * N)) time. In this problem, a row can start below the end of the row above, so there is no flat order.
- How do you count the values below a limit with the same walk?
In Count Negative Numbers in a Sorted Matrix (LC 1351), the rows and columns are sorted in decreasing order. Start at the top-right corner. If the value is negative, this cell and every cell below it in its column are negative, so add the number of those cells and move
colleft. If the value is not negative, moverowdown. The walk is still O(M + N).
SWE Reasoning
Spot it
- The input is a matrix whose rows and columns are both sorted, and the rows do not continue each other.
- The question is whether a value is present, or how many values are smaller or larger than a limit.
- A function increases in two arguments, and you must find the border of a condition.
- Binary search on a flat list does not apply, because the grid is not one sorted list.
The invariant, step by step
row = 0andcol = len(matrix[0]) - 1put the walk at the top-right corner.- If
targetis inmatrix, it is in a row at or belowrowand in a column at or left ofcol. val = matrix[row][col]is the largest in its row and the smallest in its column, among the cells that are left.- If
val == target, returnTrue. - If
val > target, the cells belowvalin its column are larger, socol -= 1. Ifval < target, the cells to the left in its row are smaller, sorow += 1. - Each drop keeps the invariant. When
rowpasses the last row orcoldrops below 0, no cell is left, and the function returnsFalse.
Traps
- Starting at the top-left corner. Both moves lead to larger values, so one comparison cannot rule out a line.
col > 0in the loop condition. Column 0 is never read, and atargetthat is only there is missed.- Moving
rowandcolafter one comparison. The other line is dropped without a check, and it may holdtarget. - Calling it binary search. Each step drops one line, not half of the cells, and the time is O(M + N).