Coding Interview Patterns

Course Content

Coding Interview Patterns

20 sections · 146 lessons

Set Matrix Zeroes


This problem looks like a two-minute exercise and is asked because the obvious in-place version is wrong in a subtle way. The interviewer wants three answers in turn: a copy, two sets, and the constant-space version that stores its bookkeeping inside the matrix itself.

After pass 1: markers live in row 0 and column 010300078010110Column markers sit in row 0, row markers in column 0; the original zeros were at (1,1) and (2,3).
Applying the markers from the bottom-right keeps them readable until they are the last cells overwritten.

The problem

You get an m × n matrix of integers. If any cell is 0, set every cell in its row and its column to 0. Do it in place.

Example 1.

Text
input            output1  2  3  4       1  0  3  05  0  7  8       0  0  0  09 10 11  0       0  0  0  0

The 0 at (1, 1) clears row 1 and column 1. The 0 at (2, 3) clears row 2 and column 3.

Example 2. [[0, 1, 2, 0], [3, 4, 5, 2], [1, 3, 1, 5]] → [[0, 0, 0, 0], [0, 4, 5, 0], [0, 3, 1, 0]].

Constraints. 1 ≤ m, n ≤ 200. Values fit in 32-bit integers. The follow-up asks for O(1) extra space.

Clarifying questions

  • Only zeros that were in the original matrix count? Yes. A 0 we write must not trigger more clearing. This is the heart of the problem.
  • Can the matrix be a single row or column? Yes.
  • Is every value possibly 0? Yes, including the corner (0, 0).

Approach 1: the simple way

Keep a copy of the original. Read zeros from the copy, write zeros into the matrix.

Python
def set_zeroes_copy(matrix: list[list[int]]) -> None:    """Brute force: find zeros in a copy, clear rows and columns in the original."""    original = [row[:] for row in matrix]    rows, cols = len(matrix), len(matrix[0])    for r in range(rows):        for c in range(cols):            if original[r][c] == 0:                for k in range(cols):                    matrix[r][k] = 0                for k in range(rows):                    matrix[k][c] = 0

This is correct, but it costs O(m × n) extra space for the copy and O(m × n × (m + n)) time in the worst case, since each zero clears a whole row and column. Why the copy? Without it, clearing a row writes zeros that later look like original zeros. On [[1, 1, 1], [1, 0, 1], [1, 1, 1]], the immediate version cascades: the correct answer is [[1, 0, 1], [0, 0, 0], [1, 0, 1]], but writing zeros as you go produced [[1, 0, 0], [0, 0, 0], [0, 0, 0]].

A better middle step: record the indices of rows and columns that contain a 0 in two sets, then clear in a second pass. That is O(m × n) time and O(m + n) space.

The key insight

The sets only store one yes/no flag per row and one per column. We need somewhere to put m + n flags without extra memory — and the matrix has a row of n cells and a column of m cells sitting right there: row 0 and column 0.

So use matrix[r][0] = 0 to mean "row r must be cleared" and matrix[0][c] = 0 to mean "column c must be cleared". Writing a 0 there is safe, because that cell is in a row (or column) that is going to be zeroed anyway.

Two details make it correct:

  1. Cell (0, 0) is shared. It cannot mean both "clear row 0" and "clear column 0". Let it mean "clear row 0", and keep one extra boolean, first_col_zero, for column 0.
  2. Apply the markers last-in, first-out. Row 0 and column 0 hold the markers, so they must be the last cells overwritten. Process from the bottom-right corner back to the top-left, and do column 0 of each row after that row's other cells.

Approach 2: markers in row 0 and column 0

Python
def set_zeroes(matrix: list[list[int]]) -> None:    """O(1) space: row 0 and column 0 store which rows and columns to clear."""    rows, cols = len(matrix), len(matrix[0])    first_col_zero = any(matrix[r][0] == 0 for r in range(rows))    for r in range(rows):                     # pass 1: record markers        for c in range(1, cols):            if matrix[r][c] == 0:                matrix[r][0] = 0              # row r must be cleared                matrix[0][c] = 0              # column c must be cleared    for r in range(rows - 1, -1, -1):         # pass 2: apply, bottom-up        for c in range(cols - 1, 0, -1):            if matrix[r][0] == 0 or matrix[0][c] == 0:                matrix[r][c] = 0        if first_col_zero:            matrix[r][0] = 0

Step by step:

  1. Before anything is written, note whether column 0 has an original zero.
  2. Pass 1 scans every cell except column 0. For each zero, mark its row in column 0 and its column in row 0.
  3. Pass 2 walks rows from the bottom up and columns from right to left, zeroing any cell whose row or column is marked. Column 0 of each row is handled last, by the boolean.

Dry run on Example 1. first_col_zero is False (column 0 holds 1, 5, 9).

stagematrix (rows separated by /)what changed
start1 2 3 4 / 5 0 7 8 / 9 10 11 0—
pass 1, zero at (1,1)1 0 3 4 / 0 0 7 8 / 9 10 11 0marks: (1,0) for row 1, (0,1) for column 1
pass 1, zero at (2,3)1 0 3 0 / 0 0 7 8 / 0 10 11 0marks: (2,0) for row 2, (0,3) for column 3
pass 2, row 21 0 3 0 / 0 0 7 8 / 0 0 0 0row 2 marked
pass 2, row 11 0 3 0 / 0 0 0 0 / 0 0 0 0row 1 marked
pass 2, row 01 0 3 0 / 0 0 0 0 / 0 0 0 0row 0 not marked; columns 1 and 3 already 0

The result, 1 0 3 0 / 0 0 0 0 / 0 0 0 0, matches the expected output. Notice (0, 0) is still 1: no original zero sat in row 0 or column 0.

Complexity. O(m × n) time — two passes over the matrix. O(1) extra space — one boolean.

The marker version matched both the copy version and the two-sets version on 400 random matrices of every shape up to 6 × 6.

Edge cases

  • A zero in row 0 or column 0 originally. Example 2 has zeros at (0, 0) and (0, 3). first_col_zero is True, so column 0 is cleared, and the marker at (0, 0) clears row 0.
  • A single row or column. The same code works: with one column, pass 1 does nothing and the boolean does all the work.
  • No zeros. No markers are written; pass 2 changes nothing.
  • All zeros. Every marker is set; everything stays 0.

Follow-ups

  • "Why not just use a special value like -1 instead of 0 in pass 1?" Because the matrix may contain any integer, including -1. A sentinel only works if a value is guaranteed unused — ask before relying on one.
  • "Do the two-sets version in less code." zero_rows = {r for r, row in enumerate(matrix) if 0 in row} and similarly for columns via zip(*matrix). Fine in production; say it is O(m + n) space.
  • "The matrix is huge and stored on disk." Two passes are still enough: one streaming pass to collect the row and column flags (m + n bits), one to rewrite.

Check your understanding

0 of 2 answered

1.Why does the O(1) solution need a separate first_col_zero boolean?

2.For [[1, 1, 1], [1, 0, 1], [1, 1, 1]], what goes wrong if you zero the row and column immediately on finding each zero?