Algorithm

Set Matrix Zeroes

Arrays & Strings Pattern

Set Matrix Zeroes

Given an m x n integer matrix, if an element is 0, set its entire row and column to 0's. You must do it in place.

CONSTRAINTS
  • m == matrix.length
  • n == matrix[0].length
  • 1 <= m, n <= 200
  • -2³¹ <= matrix[i][j] <= 2³¹ - 1
EXAMPLE 1
Input: matrix = [[1,1,1],[1,0,1],[1,1,1]]
Output: [[1,0,1],[0,0,0],[1,0,1]]
The 0 at (1,1) marks row 1 and col 1 for zeroing. Pass 2 then fills them.
EXAMPLE 2
Input: matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
Output: [[0,0,0,0],[0,4,5,0],[0,3,1,0]]
Zeroes at (0,0) and (0,3) trigger multiple row and column zeroings.
What should I return if the matrix is already filled with zeros?
The matrix would remain unchanged since all rows and columns are already zero. Your algorithm should handle this efficiently by not performing redundant writes.
Is the matrix always square, or can it be rectangular?
The matrix can be rectangular (M x N). Your solution must handle cases where the number of rows is different from the number of columns.
Should I return a new matrix or modify the input directly?
The requirement is to modify the matrix strictly in-place. Do not return a new 2D array.

The task sounds mechanical — find the zeros, wipe their rows and columns — but it hides a genuine trap. Try the naive plan: scan the grid, and every time you meet a 0, immediately zero out its row and column. The zeros you write are now indistinguishable from the zeros that were originally there. The scan then treats your freshly written zeros as new triggers, wiping their rows and columns too, and the chain reaction can flatten the whole grid. The real problem is therefore: separate discovery from writing — first learn where the original zeros are, only then modify anything.

Remember with flag arrays

Notice we do not need to remember each zero's exact position — only which rows and which columns are condemned. One boolean per row, one per column:

python
rows_to_zero = [False] * m
cols_to_zero = [False] * n

for i in range(m):                      # pass 1: discovery only, no writes
    for j in range(n):
        if matrix[i][j] == 0:
            rows_to_zero[i] = True
            cols_to_zero[j] = True

for i in range(m):                      # pass 2: writes only, no discovery
    for j in range(n):
        if rows_to_zero[i] or cols_to_zero[j]:
            matrix[i][j] = 0

Correct, O(M×N) time, and O(M + N) extra space for the flags. The interview follow-up is predictable by now: do it with O(1) extra space.

The trick: the matrix contains its own notebook

The flags need one bit per row and one per column — and the matrix already has a column (the first cell of every row) and a row (the first cell of every column) we could write those bits into: mark "row i is condemned" by setting matrix[i][0] = 0, and "column j is condemned" by setting matrix[0][j] = 0.

But that destroys the original values of the first row and column — is that safe? Yes, and here is the argument: we only ever write a marker into matrix[i][0] when row i contains a zero — meaning every cell of row i, including matrix[i][0] itself, is destined to become 0 in the final answer anyway. The same holds for matrix[0][j]. We only overwrite information whose final value is already decided. Nothing the future needs is lost.

One corner needs care: cell (0, 0) is shared — a 0 there is ambiguous between "row 0 is condemned" and "column 0 is condemned". The fix costs one boolean variable (still O(1)): track the first column's fate separately.

Order of operations is the whole game

The first row and column are now the notebook. If pass 2 zeroes them before the interior is processed, the markers are wiped while still needed. So: zero the interior first, the notebook last — the code below walks rows bottom-up so row 0 (the marker row) is processed at the very end, and handles column 0 after each row's interior.

python
m, n = len(matrix), len(matrix[0])
first_col_zero = False

for i in range(m):                       # pass 1: record into the notebook
    if matrix[i][0] == 0: first_col_zero = True
    for j in range(1, n):
        if matrix[i][j] == 0:
            matrix[i][0] = 0             # condemn row i
            matrix[0][j] = 0             # condemn column j

for i in range(m - 1, -1, -1):           # pass 2: interior first, notebook last
    for j in range(n - 1, 0, -1):
        if matrix[i][0] == 0 or matrix[0][j] == 0:
            matrix[i][j] = 0
    if first_col_zero:
        matrix[i][0] = 0
Worked Example:[[1, 1, 1], [1, 0, 1], [1, 1, 1]]
- Pass 1 finds the 0 at (1, 1). It condemns row 1 by setting matrix[1][0] = 0, and column 1 by setting matrix[0][1] = 0.
- Pass 2, row 2: cell (2, 1) sees column-marker matrix[0][1] = 0, becomes 0.
- Row 1: row-marker matrix[1][0] = 0, so (1, 1) and (1, 2) become 0. Then matrix[1][0] itself — it doubles as the row marker and correctly ends up 0.
- Row 0 (last, notebook row): (0, 1) sees its own column marker, stays 0.
- Answer: [[1,0,1],[0,0,0],[1,0,1]] — a clean cross centered on the original zero.
The takeaway

Still O(M×N) time — every cell is read and possibly written — but the extra space fell from O(M+N) to O(1). Two lessons travel well beyond this problem. First, when an in-place transformation risks confusing its own writes with its input, split the work into a read-only discovery pass and a write-only execution pass. Second, when you need scratch memory you are not allowed to allocate, hunt for cells whose final value is already determined — they are free storage, because overwriting them destroys nothing the future needs.

Interactive Strategy Visualization

State Matrix Check-Sheet

In-place Indicator Mapping
1
1
1
1
0
1
1
1
1

Scanning matrix for zeroes. Found a 0 at (Row 1, Col 1).

O(M*N) Space
O(M+N) Space
O(1) Space (Markers)