Intuition
Mutating as you scan would corrupt later checks, so first record which rows and columns must be zeroed, then apply the changes in a second pass.
The trap is that zeroing as you scan corrupts the very data you're still reading. So the fix is two phases: record which rows and columns are doomed, then apply. Separating detection from mutation is the general lesson for any in-place grid transform where a write can be mistaken for input.
Approach
Zeroing as you scan corrupts the scan
The tempting move — when you find a 0, immediately zero out its whole row and column — is a trap. Those freshly written zeros are indistinguishable from original zeros, so the rest of the scan mistakes them for triggers and cascades, zeroing far more than it should. The fix is to never let detection and mutation happen at the same time.
Separate detection from mutation
Make a first pass that only records which rows and which columns contain at least one original zero, storing their indices in two sets. This pass changes nothing, so there's no cascade — every zero it sees is a real one from the input.
Apply the flags in a second pass
Now sweep the grid again and set a cell to 0 if its row index or its column index was flagged. Because the flags were all computed from the original matrix, the result is exactly right regardless of overlap (set membership is idempotent, so a cell at a flagged row and column is just zeroed once). Two passes: O(m·n) time, O(m + n) space for the two sets.
Solution & live demo
Common pitfalls
Zeroing during the scan
for i, row in enumerate(matrix):
for j, v in enumerate(row):
if v == 0:
for k in range(len(row)): matrix[i][k] = 0# phase 1: collect rows/cols # phase 2: apply
The zeros you write are indistinguishable from original zeros, so they trigger further clearing and the whole matrix cascades to zero. Detection must finish before any mutation starts.
Storing coordinates instead of row and column indices
zeros = [(i, j) for ...] for i, j in zeros: matrix[i][j] = 0
rows, cols = set(), set() ... if i in rows or j in cols: row[j] = 0
Recording the cell only re-zeroes that one cell. What must be cleared is the entire row and the entire column it sits in, so the indices are what matter.
Assuming the O(1)-space version is required
# elaborate first-row/first-column marker scheme
rows, cols = set(), set()
The marker trick reduces space to O(1) but is fiddly and easy to get wrong. Two sets are O(m + n) — usually acceptable — and the interviewer generally wants to see this working first before you optimise.
Edge cases
Both sets stay empty, so the second pass changes nothing.
Every row and column is flagged; the whole matrix stays zero after the second pass.
Set membership is idempotent, so overlapping flags simply zero the cell once.