Matrix Search — Sorted Matrix
When every row and column of a matrix is sorted, a single corner gives you a powerful elimination strategy: each comparison permanently removes an entire row or column. Find any value in O(m + n) — no binary search needed.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Watch it happen, frame by frame
- Now, the code — line by line
- Why does it never miss the target?
- When should I reach for this? (the trigger list)
- The same trick in three disguises
- Disguise 1 — Count elements ≤ target in each row
- Disguise 2 — Kth Smallest in a Sorted Matrix (LC #378)
- Traps that catch beginners
- Say it like a pro (interview one-liner)
- Check yourself
- Practice problems
Before we start
By the end of this page you will be able to:
- See why starting from the top-right corner of a sorted matrix gives you two elimination choices at every step.
- Explain out loud why each comparison eliminates an entire row or column — not just a single cell.
- Recognise this staircase pattern whenever a problem gives you a 2D matrix sorted by both row and column.
Stop at every Pause & Think box — the insight here is a direct cousin of the opposite-end two pointers you already know.
Picture this first (no code yet)
A real-life story
You are in a giant library arranged as a grid of shelves. The rule of the library: prices of books increase as you walk right along any shelf, and increase as you walk down any aisle.
You are looking for a book that costs exactly ₹50.
You start at the top-right corner — the most expensive book on the top shelf. It costs ₹70.
"Too expensive," you think. "And every book below me on this aisle costs even more — so this entire right column is useless for me." You step left.
The book now costs ₹40. "Too cheap. And every book to my left on this shelf is even cheaper — so this entire row is useless." You step down.
Now it costs ₹55. Too expensive — step left. ₹50. Found it.
You never backtracked. Every step eliminated an entire row or column. You searched a grid of thousands of books in a handful of steps.
That librarian's walk — start at the top-right corner, eliminate a column if too expensive, eliminate a row if too cheap — is the Sorted Matrix Search pattern.
The actual problem
You are given an
m × nmatrix where every row is sorted left-to-right and every column is sorted top-to-bottom. Given a target value, returntrueif it exists in the matrix,falseotherwise.
matrix:
[ [1, 4, 7, 11],
[2, 5, 8, 12],
[3, 6, 9, 16],
[10, 13, 14, 17] ]
target = 5 → true
target = 20 → false
First, the slow way (so you feel the pain)
Brute force: check every cell.
for row in matrix:
for val in row:
if val == target:
return True
return False
O(m × n) — for a 1000×1000 matrix, that's a million checks.
Binary search on each row: since each row is sorted, binary-search it.
import bisect
for row in matrix:
idx = bisect.bisect_left(row, target)
if idx < len(row) and row[idx] == target:
return True
return False
O(m log n) — better, but still visits every row. For a square n×n matrix: O(n log n).
The staircase search does it in O(m + n) — for a 1000×1000 matrix, that's 2000 checks instead of 1,000,000.
The turning point
The key question: which corner should you start from?
Pause & think
Consider these four corners of the matrix above:
- Top-left (1): smallest in both its row and column.
- Top-right (11): largest in its row, smallest in its column.
- Bottom-left (10): smallest in its row, largest in its column.
- Bottom-right (17): largest in both its row and column.
Which corner lets you make a useful decision when you compare it to a target? Which corners give you no useful information?
Top-left (1): If target > 1, should you go right or down? Both directions increase — you can't choose. Stuck.
Bottom-right (17): If target < 17, should you go left or up? Both directions decrease — again stuck.
Top-right (11): If target < 11, you know the entire right column from this cell downward is too large (they're all ≥ 11). Step left. If target > 11, you know the entire top row from this cell leftward is too small (they're all ≤ 11). Step down. One useful decision every time.
Bottom-left works the same way but mirrored.
Start from a corner that has one direction larger and one direction smaller — that's top-right or bottom-left.
The one idea to remember
The entire pattern in one sentence
Start at the top-right corner; if the value is too big, step left (eliminating this column); if it's too small, step down (eliminating this row) — each step permanently removes a full row or column until you find the target or run out of matrix.
Watch it happen, frame by frame
Matrix (same as above), target = 5. Starting position: row=0, col=3 (top-right, value=11).
(0,3)=11 > 5 → too big, step LEFT. col=2
(0,2)= 7 > 5 → too big, step LEFT. col=1
(0,1)= 4 < 5 → too small, step DOWN. row=1
(1,1)= 5 = 5 → FOUND ✓
Now try target = 20 (not in the matrix):
(0,3)=11 < 20 → too small, step DOWN. row=1
(1,3)=12 < 20 → too small, step DOWN. row=2
(2,3)=16 < 20 → too small, step DOWN. row=3
(3,3)=17 < 20 → too small, step DOWN. row=4
row=4 ≥ m=4 → out of bounds, STOP. return False ✓
Pause & think
Cover the trace below. Search for 14 in the same matrix. Write each position and step.
Check your trace
(0,3)=11 < 14 → too small, step DOWN. row=1
(1,3)=12 < 14 → too small, step DOWN. row=2
(2,3)=16 > 14 → too big, step LEFT. col=2
(2,2)= 9 < 14 → too small, step DOWN. row=3
(3,2)=14 = 14 → FOUND ✓
4 steps to find a value in a 4×4 matrix (16 cells). Brute force: up to 16 checks.
Now, the code — line by line
def searchMatrix(matrix, target):
if not matrix or not matrix[0]:
return False
row = 0 # start at the top row
col = len(matrix[0]) - 1 # start at the rightmost column (top-right corner)
while row < len(matrix) and col >= 0: # stay inside the matrix
val = matrix[row][col]
if val == target:
return True # found it
elif val > target:
col -= 1 # too big — eliminate this column, step left
else:
row += 1 # too small — eliminate this row, step down
return False # pointer walked off the edge — not found
Mapping every line to the library story:
row = 0, col = len(matrix[0]) - 1— walk to the top-right shelf.while row < len(matrix) and col >= 0:— stay inside the library. Walk off the bottom (row ≥ m) or off the left (col < 0) and the book isn't here.val > target: col -= 1— this shelf's price is too high. Every book below on this aisle is higher still — discard the entire rightmost remaining aisle.val < target: row += 1— this book is too cheap. Every book to the left is cheaper still — discard the entire top remaining shelf.
Why does it never miss the target?
At every step we eliminate a row or column only after proving every remaining cell in it cannot be the target:
- We eliminate column
colwhenmatrix[row][col] > target. Since columns are sorted top-to-bottom, every cell below(row, col)in this column is ≥matrix[row][col]> target. So none of them can be the target. - We eliminate row
rowwhenmatrix[row][col] < target. Since rows are sorted left-to-right, every cell to the left of(row, col)in this row is ≤matrix[row][col]< target. So none of them can be the target.
Every elimination is guaranteed junk. The target, if it exists, is always still inside the remaining sub-matrix.
Time: O(m + n) — row can increase at most m times; col can decrease at most n times. Total steps ≤ m + n.
Space: O(1) — just two integer pointers.
| Approach | Time | Space |
|---|---|---|
| Brute force | O(m × n) | O(1) |
| Binary search per row | O(m log n) | O(1) |
| Staircase search | O(m + n) | O(1) |
When should I reach for this? (the trigger list)
Reach for the top-right (or bottom-left) staircase search when:
- The matrix is sorted both row-wise and column-wise.
- You're asked to find, count, or decide existence of a value in such a matrix.
- Brute force is O(m × n) and the interviewer asks for better.
- The problem resembles "Search a 2D Matrix" or "Count elements less than X in a sorted matrix."
A tell-tale sign: the matrix guarantees that every row is sorted AND every column is sorted. That combination is the gift that allows the staircase.
Keep this in mind
This is different from LeetCode #74 ("Search a 2D Matrix I"), where the matrix is fully sorted as a single flat list (last element of row i < first element of row i+1). That variant allows a single binary search. This pattern (LC #240, "Search a 2D Matrix II") has independent row and column sorting without that global guarantee — it requires the staircase.
The same trick in three disguises
Disguise 1 — Count elements ≤ target in each row
For each row, binary-search for the insertion point of target. Since rows are sorted, bisect_right(row, target) gives the count in O(log n) per row. But using the staircase idea you can also count globally in O(m + n):
def countLessEqual(matrix, target):
# Start at bottom-left: largest in row, smallest in column
row, col = len(matrix) - 1, 0
count = 0
while row >= 0 and col < len(matrix[0]):
if matrix[row][col] <= target:
count += row + 1 # all cells above (row,col) in this column qualify
col += 1
else:
row -= 1
return count
Notice: we start from bottom-left here (smallest in row, largest in column) — the mirrored variant.
Disguise 2 — Kth Smallest in a Sorted Matrix (LC #378)
Binary search on value range, use staircase to count elements ≤ mid in O(m + n) per binary-search step.
def kthSmallest(matrix, k):
n = len(matrix)
lo, hi = matrix[0][0], matrix[-1][-1]
def count_le(mid):
row, col = 0, n - 1
cnt = 0
while row < n and col >= 0:
if matrix[row][col] <= mid:
cnt += col + 1 # all elements in this row up to col qualify
row += 1
else:
col -= 1
return cnt
while lo < hi:
mid = (lo + hi) // 2
if count_le(mid) < k:
lo = mid + 1
else:
hi = mid
return lo
Level up — Median in a Row-Wise Sorted Matrix
Binary search on values [1, max_val], use the staircase count to find how many elements are ≤ mid across all rows. The median is the smallest value where this count ≥ (total elements + 1) / 2.
Same staircase count_le as above, but applied row by row (since columns aren't globally sorted in the row-wise-only variant).
Traps that catch beginners
Watch out for these
- Starting from the wrong corner (top-left or bottom-right). Top-left gives two increasing directions — you can't decide which way to go. Top-right gives one increasing direction (down) and one decreasing (left) — exactly one useful choice per step.
- Using this on a matrix sorted only row-wise (LC #74 variant). If the matrix has the global-flat-sorted property, binary search is enough. This staircase is for the harder variant (both row and column sorted independently, LC #240).
- Wrong while condition. Must be
row < m AND col >= 0— both conditions together. If either pointer goes out of range, the target isn't in the matrix. - Moving the wrong pointer. Too big →
col -= 1(go left). Too small →row += 1(go down). Swapping these makes the search walk toward a corner with no elimination power.
| Bug | Fix |
|---|---|
| Starting at top-left | Start at top-right: row=0, col=n-1 |
while row < m or col >= 0 | Use and — both must be satisfied |
col -= 1 when too small | Too small → row += 1; too big → col -= 1 |
Say it like a pro (interview one-liner)
"Since both rows and columns are sorted, I'll use the staircase search — start at the top-right corner where I have one direction that gets larger and one that gets smaller. If the value is too big I step left, eliminating this column; if too small I step down, eliminating this row. Each step eliminates a full row or column, giving O(m + n) time and O(1) space."
Remember this forever
Sorted Matrix Search (Staircase)
Start at top-right corner. val > target → step left (kill column). val < target → step down (kill row).
Why top-right? One direction increases (down), one decreases (left) — always one useful choice.
Cost: O(m + n) time, O(1) space
Trigger: matrix sorted both row-wise AND column-wise + find/count a value
Skeleton:
row, col = 0, n-1
while row < m and col >= 0:
if val == target: found
elif val > target: col -= 1
else: row += 1
Check yourself
Why does starting at the top-left corner fail?
At the top-left corner, both "go right" and "go down" lead to larger values. When the target is larger than the current cell, you have no way to decide which direction to explore — both could work. Without a single decisive direction, you're back to brute force.
When we step left (col -= 1), what are we proving about the eliminated column?
We're proving that every remaining cell in column col is ≥ matrix[row][col] > target (because the column is sorted top-to-bottom, and row is the topmost uneliminated row). So no cell in that column can equal the target. It's safe to discard the entire column permanently.
What changes if you start from the bottom-left corner instead of top-right?
Bottom-left has the smallest value in its row and the largest value in its column. If val > target → step up (row -= 1, kills this row's larger cells). If val < target → step right (col += 1, kills this column's smaller cells). Exactly the same elimination logic, mirrored. Both work — top-right and bottom-left are the two valid starting corners.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Search a 2D Matrix II | Medium | The classic staircase — start top-right | LC #240 |
| Search a 2D Matrix | Medium | Fully flat-sorted — single binary search suffices | LC #74 |
| Kth Smallest Element in a Sorted Matrix | Medium | Binary search on value + staircase count | LC #378 |
| Count Negatives in a Sorted Matrix | Easy | Staircase from bottom-left to count negatives | LC #1351 |
When you can write the staircase loop from memory and explain why you start at top-right and not top-left, you've learned this pattern.
Next up: In-Place Array Manipulation — where the array itself becomes your notebook, using negation to mark visited cells without extra memory.