Menu
DSA interview questionsQuestion 108 of 249

DSA interview question · Question 108 of 249

Search a 2D Matrix II: Staircase Search from the Top-Right Corner

  • Medium
  • coding
  • ~15 min
  • High relevance
  • 6 min read
  • Updated Oct 2026

Short answer

Start at the top-right cell. It is the largest value in its row and the smallest in its column, so one comparison removes a whole row or column: if it is bigger than the target, move left; if smaller, move down; if equal, you are done. You take at most m + n steps, so it is O(m + n) time and O(1) space. A binary search on each row gives O(m log n), which is better only when there are far fewer rows than columns. Flattening the matrix into one sorted list does not work here, because a row does not continue the previous one.

On this page
  1. Problem
  2. Examples
  3. Approach 1: brute force
  4. Approach 2: binary search each row
  5. Approach 3: optimal (staircase search)
  6. Tests
  7. Edge cases and pitfalls
  8. Where this shows up in data engineering

Problem

You are given an m x n grid of integers in which every row is sorted in ascending order from left to right and every column is sorted in ascending order from top to bottom. Return whether a target value appears in the grid.

This is LeetCode 240, Search a 2D Matrix II. Unlike Search a 2D Matrix, the first value of a row can be smaller than the last value of the row above, so the grid is not one sorted list.

Assume up to 300 rows and 300 columns.

Examples

grid = [[ 2,  5,  9, 14],
        [ 3,  8, 12, 19],
        [ 7, 10, 16, 23],
        [11, 15, 20, 30]]

target = 12  ->  True
target = 13  ->  False
target = 2   ->  True    (top-left corner)
target = 31  ->  False   (larger than everything)

Approach 1: brute force

Check every cell.

def search_linear(grid, target):
    return any(target in row for row in grid)

This is O(m * n) time and uses neither ordering.

Approach 2: binary search each row

Each row is sorted, so binary search each one. You can also skip rows that cannot contain the target: if a row starts above the target, every later row does too, because columns are sorted.

from bisect import bisect_left

def search_rows(grid, target):
    for row in grid:
        if not row or row[0] > target:
            break                       # this and every lower row start too high
        i = bisect_left(row, target)
        if i < len(row) and row[i] == target:
            return True
    return False

This is O(m log n) time and O(1) space. It uses the row order fully but the column order only for the early exit.

Look at the top-right cell. Everything to its left in that row is smaller, and everything below it in that column is larger. So a single comparison with the target settles a whole line:

  • If the cell is greater than the target, the target cannot be anywhere in this column (everything below is even larger). Move one column left.
  • If the cell is less than the target, the target cannot be in this row (everything to the left is even smaller). Move one row down.
  • If it is equal, return True.

Walkthrough for target 12 in the grid above:

(0,3)=14 > 12 -> left
(0,2)=9  < 12 -> down
(1,2)=12      -> found
def search_staircase(grid, target):
    if not grid or not grid[0]:
        return False
    r, c = 0, len(grid[0]) - 1
    while r < len(grid) and c >= 0:
        v = grid[r][c]
        if v == target:
            return True
        if v > target:
            c -= 1                      # discard this column
        else:
            r += 1                      # discard this row
    return False

Why it is correct. The invariant is that if the target is present, it lies in rows r..m-1 and columns 0..c. Each move discards a row or column that cannot contain it, as argued above. When the region becomes empty, the target is not there. The bottom-left corner works just as well, with the directions mirrored. The top-left corner does not: it is the smallest in both its row and column, so a “too small” result does not tell you which way to go.

Complexity. Each step removes a row or a column, so at most m + n steps: O(m + n) time and O(1) space. This beats the per-row binary search unless one dimension is much smaller than the other.

Tests

import random

G = [[2, 5, 9, 14],
     [3, 8, 12, 19],
     [7, 10, 16, 23],
     [11, 15, 20, 30]]

def check(fn):
    assert fn(G, 12) is True
    assert fn(G, 13) is False
    assert fn(G, 2) is True
    assert fn(G, 30) is True
    assert fn(G, 31) is False
    assert fn(G, 1) is False
    assert fn([[5]], 5) is True
    assert fn([[1, 3, 5]], 4) is False
    assert fn([[1], [3], [5]], 3) is True
    assert fn([], 1) is False

def random_sorted_grid(rng, m, n):
    grid = [[0] * n for _ in range(m)]
    for i in range(m):
        for j in range(n):
            base = max(grid[i - 1][j] if i else 0, grid[i][j - 1] if j else 0)
            grid[i][j] = base + rng.randint(0, 3)
    return grid

for f in (search_linear, search_rows, search_staircase):
    check(f)

rng = random.Random(2)
for _ in range(300):
    g = random_sorted_grid(rng, rng.randint(1, 6), rng.randint(1, 6))
    t = rng.randint(-1, 30)
    assert search_staircase(g, t) == search_rows(g, t) == search_linear(g, t)
print("ok")

Edge cases and pitfalls

  • Treating it as one sorted list. The divmod trick from Search a 2D Matrix is wrong here, because row ends do not line up with the next row’s start.
  • Wrong corner. Top-left and bottom-right give no direction. Use top-right or bottom-left.
  • Empty grid or empty rows. Guard before reading grid[0].
  • Duplicates. They do not break the staircase; any equal cell ends the search.

Where this shows up in data engineering

A table sorted by two keys that rise together, such as a grid of time buckets against cumulative counts, has this shape, and the staircase walk is also the core of counting how many cells fall below a threshold in a sorted matrix. The same “discard a whole row or column per comparison” reasoning helps when merging or joining two sorted inputs.

By DataDank Editorial · Last reviewed Oct 2026 · Python 3 solutions verified with assert-based tests

Progress is saved in this browser only. No account needed.

Search
Filter by type