Files
illustrated-algorithm/set-matrix-zeroes/index.html
T
2026-08-24 04:35:13 +00:00

455 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>018. 矩阵置零 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 120px; padding: 16px 0; }
.code-section { margin-top: 16px; }
.legend { display:flex; flex-wrap:wrap; gap:12px; margin:10px 0; font-size:13px; }
.legend span { display:inline-flex; align-items:center; gap:4px; }
.legend .dot { width:14px; height:14px; border-radius:4px; display:inline-block; }
.marker-row-cell { background:#fef3c7 !important; border-color:#f59e0b !important; color:#92400e; font-weight:700; }
.zeroed-cell { background:#fee2e2 !important; border-color:#ef4444 !important; color:#991b1b; }
.first-row-marker { background:#ede9fe !important; border-color:#8b5cf6 !important; color:#5b21b6; }
.first-col-marker { background:#cffafe !important; border-color:#06b6d4 !important; color:#155e75; }
.origin-zero { background:#fef3c7 !important; border-color:#f59e0b !important; color:#92400e; }
.phase-tag { display:inline-block; padding:2px 8px; border-radius:999px; font-size:11px; font-weight:700; margin-right:6px; }
.phase-tag.mark { background:#fef3c7; color:#92400e; }
.phase-tag.zero { background:#fee2e2; color:#991b1b; }
.phase-tag.first { background:#ede9fe; color:#5b21b6; }
.phase-tag.done { background:#dcfce7; color:#166534; }
.info-row { display:flex; gap:16px; flex-wrap:wrap; margin-top:8px; font-size:13px; }
.info-row .tag { padding:2px 8px; border-radius:6px; font-weight:600; }
.tag.green { background:#dcfce7; color:#166534; }
.tag.red { background:#fee2e2; color:#991b1b; }
.tag.blue { background:#dbeafe; color:#1e40af; }
.tag.purple { background:#ede9fe; color:#5b21b6; }
.tag.cyan { background:#cffafe; color:#155e75; }
.border-label { font-size:11px; color:var(--text-muted); text-align:center; padding:2px 0; }
</style>
</head>
<body>
<div class="container">
<h1>🔲 018. 矩阵置零 <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:矩阵 | LeetCode Hot 100</p>
<div class="controls" id="controls">
<label for="inputArea">输入:</label>
<input type="text" id="inputArea" placeholder="默认示例,可自定义">
<button id="applyBtn" class="primary">生成图解</button>
<select id="exampleSelect"></select>
<span style="flex:1"></span>
<button id="prevBtn">◀ 上一步</button>
<button id="nextBtn">下一步 ▶</button>
<button id="jumpBtn">⏭ 跳到结果</button>
<button id="autoBtn">自动播放</button>
<button id="resetBtn">重置</button>
</div>
<div class="pipeline" id="pipeline"></div>
<div class="hint info" id="hintBox">
<span id="stepInfo"></span><br>
<span id="hintText"></span>
</div>
<div class="panels">
<div class="panel" id="mainPanel">
<h3>📊 可视化</h3>
<div class="vis-area" id="vizArea">点击「生成图解」开始</div>
</div>
<div class="panel-grid">
<div class="panel" id="detailPanel">
<h3>📝 当前步骤详情</h3>
<div id="detailContent">等待开始...</div>
</div>
<div class="panel" id="resultPanel">
<h3>✅ 结果</h3>
<div id="resultContent">等待完成...</div>
</div>
</div>
</div>
<div class="panel code-section">
<h3>💻 参考代码(Python)</h3>
<div id="codeArea"></div>
</div>
<footer>Powered by QwenPaw · 图解算法 · LeetCode Hot 100</footer>
</div>
<script src="../shared/algo-viz.js"></script>
<script>
"use strict";
(function() {
const examples = [
{input: [[1,1,1],[1,0,1],[1,1,1]], label: '3×3 含1个0'},
{input: [[0,1,2,0],[3,4,5,2],[1,3,1,5]], label: '3×4 含2个0'},
{input: [[1,2,3,4],[5,0,7,8],[9,10,11,12],[13,14,15,0]], label: '4×4 含2个0'},
];
let origMatrix, steps, stepCtrl;
function buildSteps(mat) {
origMatrix = mat.map(r => [...r]);
const m = mat.length, n = mat[0].length;
const work = mat.map(r => [...r]);
steps = [];
let firstRowZero = false, firstColZero = false;
// --- Step 0: Init ---
steps.push({
stage: 'init', msg: '用第一行和第一列做标记空间,实现 O(1) 额外空间的矩阵置零',
phase: 'init', firstRowZero: null, firstColZero: null,
currentCell: null, markerCells: {}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: 1
});
// --- Step 1: Scan first row for zeros ---
for (let j = 0; j < n; j++) {
if (work[0][j] === 0) firstRowZero = true;
steps.push({
stage: 'scan_first_row', msg: `检查第一行:matrix[0][${j}] = ${work[0][j]}${work[0][j]===0 ? ' → 发现0!' : ''}`,
phase: 'scan', firstRowZero: firstRowZero || null, firstColZero: null,
currentCell: `0,${j}`, markerCells: {}, zeroedCells: {},
scanPos: `0,${j}`, scanRow: 0, scanCol: j,
codeLine: 2
});
}
steps.push({
stage: 'first_row_result', msg: `第一行扫描完毕:${firstRowZero ? '✅ 含有0,首行需最终置零' : '❌ 无0,首行无需置零'}`,
phase: 'scan', firstRowZero, firstColZero: null,
currentCell: null, markerCells: {}, zeroedCells: {},
scanPos: null, scanRow: 0, scanCol: -1,
codeLine: 2
});
// --- Step 2: Scan first col for zeros ---
for (let i = 0; i < m; i++) {
if (work[i][0] === 0) firstColZero = true;
steps.push({
stage: 'scan_first_col', msg: `检查第一列:matrix[${i}][0] = ${work[i][0]}${work[i][0]===0 ? ' → 发现0!' : ''}`,
phase: 'scan', firstRowZero, firstColZero: firstColZero || null,
currentCell: `${i},0`, markerCells: {}, zeroedCells: {},
scanPos: `${i},0`, scanRow: i, scanCol: 0,
codeLine: 3
});
}
steps.push({
stage: 'first_col_result', msg: `第一列扫描完毕:${firstColZero ? '✅ 含有0,首列需最终置零' : '❌ 无0,首列无需置零'}`,
phase: 'scan', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: 0,
codeLine: 3
});
// --- Step 3: Mark inner cells ---
steps.push({
stage: 'mark_start', msg: '扫描内部区域 (i≥1, j≥1):发现0则在对应的首行/首列位置做标记',
phase: 'mark', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: 5
});
const markerCells = {};
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
if (work[i][j] === 0) {
markerCells[`${i},0`] = true;
markerCells[`0,${j}`] = true;
const snapshot = {...markerCells};
steps.push({
stage: 'mark', msg: `发现 matrix[${i}][${j}] = 0!→ 标记 row_marker[${i}][0] = 0,col_marker[0][${j}] = 0`,
phase: 'mark', firstRowZero, firstColZero,
currentCell: `${i},${j}`, markerCells: {...snapshot}, zeroedCells: {},
scanPos: `${i},${j}`, scanRow: i, scanCol: j,
codeLine: 7
});
}
}
}
steps.push({
stage: 'mark_done', msg: `标记阶段完毕,共在首行/首列做了 ${Object.keys(markerCells).length} 处标记`,
phase: 'mark', firstRowZero, firstColZero,
currentCell: null, markerCells: {...markerCells}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: 5
});
// --- Step 4: Zero rows based on first column markers ---
const zeroedCells = {};
steps.push({
stage: 'zero_start', msg: '根据标记,对内部区域置零:先按行标记置零,再按列标记置零',
phase: 'zero', firstRowZero, firstColZero,
currentCell: null, markerCells: {...markerCells}, zeroedCells: {},
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: 9
});
for (let i = 1; i < m; i++) {
if (work[i][0] === 0) {
steps.push({
stage: 'zero_row', msg: `第一列标记 matrix[${i}][0] = 0 → 第${i}行全部置零`,
phase: 'zero', firstRowZero, firstColZero,
currentCell: `${i},0`, markerCells: {...markerCells}, zeroedCells: {...zeroedCells},
scanPos: `${i},0`, scanRow: i, scanCol: -1,
codeLine: 10
});
for (let j = 1; j < n; j++) {
zeroedCells[`${i},${j}`] = true;
}
steps.push({
stage: 'zero_row_done', msg: `第${i}行已置零(${n-1}个元素)`,
phase: 'zero', firstRowZero, firstColZero,
currentCell: null, markerCells: {...markerCells}, zeroedCells: {...zeroedCells},
scanPos: null, scanRow: i, scanCol: -1,
codeLine: 11
});
}
}
for (let j = 1; j < n; j++) {
if (work[0][j] === 0) {
steps.push({
stage: 'zero_col', msg: `第一行标记 matrix[0][${j}] = 0 → 第${j}列全部置零`,
phase: 'zero', firstRowZero, firstColZero,
currentCell: `0,${j}`, markerCells: {...markerCells}, zeroedCells: {...zeroedCells},
scanPos: `0,${j}`, scanRow: -1, scanCol: j,
codeLine: 13
});
for (let i = 1; i < m; i++) {
zeroedCells[`${i},${j}`] = true;
}
steps.push({
stage: 'zero_col_done', msg: `第${j}列已置零(${m-1}个元素)`,
phase: 'zero', firstRowZero, firstColZero,
currentCell: null, markerCells: {...markerCells}, zeroedCells: {...zeroedCells},
scanPos: null, scanRow: -1, scanCol: j,
codeLine: 14
});
}
}
// --- Step 5: First row ---
if (firstRowZero) {
for (let j = 0; j < n; j++) zeroedCells[`0,${j}`] = true;
steps.push({
stage: 'zero_first_row', msg: `首行有0标记,首行${n}个元素全部置零`,
phase: 'first', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {...zeroedCells},
scanPos: null, scanRow: 0, scanCol: -1,
codeLine: 16
});
}
if (firstColZero) {
for (let i = 0; i < m; i++) zeroedCells[`${i},0`] = true;
steps.push({
stage: 'zero_first_col', msg: `首列有0标记,首列${m}个元素全部置零`,
phase: 'first', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {...zeroedCells},
scanPos: null, scanRow: -1, scanCol: 0,
codeLine: 18
});
}
// --- Build result ---
const result = mat.map(r => [...r]);
for (const key of Object.keys(zeroedCells)) {
const [r, c] = key.split(',').map(Number);
result[r][c] = 0;
}
steps.push({
stage: 'done', msg: '完成!矩阵已原地修改,空间复杂度 O(1)',
phase: 'done', firstRowZero, firstColZero,
currentCell: null, markerCells: {}, zeroedCells: {...zeroedCells}, result,
scanPos: null, scanRow: -1, scanCol: -1,
codeLine: -1
});
}
function render(step) {
const s = steps[step];
const m = origMatrix.length, n = origMatrix[0].length;
// Build cell highlights
function cellClass(val, r, c) {
const key = `${r},${c}`;
// Zeroed cells (red)
if (s.zeroedCells[key]) return 'zeroed-cell';
// Current cell
if (s.currentCell === key) return 'current';
// Marker cells in first row (purple)
if (s.markerCells[key] && r === 0) return 'first-row-marker';
// Marker cells in first col (cyan)
if (s.markerCells[key] && c === 0) return 'first-col-marker';
// Original zeros being scanned
if (s.scanPos === key && val === 0) return 'origin-zero';
return '';
}
// Legend
let viz = '<div class="legend">';
viz += '<span><span class="dot" style="background:#fef3c7;border:1px solid #f59e0b;"></span> 原始0</span>';
viz += '<span><span class="dot" style="background:#ede9fe;border:1px solid #8b5cf6;"></span> 首行标记</span>';
viz += '<span><span class="dot" style="background:#cffafe;border:1px solid #06b6d4;"></span> 首列标记</span>';
viz += '<span><span class="dot" style="background:#fee2e2;border:1px solid #ef4444;"></span> 已置零</span>';
viz += '<span><span class="dot" style="background:#fef3c7;border:2px solid #f59e0b;box-shadow:0 0 0 2px rgba(245,158,11,.3);"></span> 当前位置</span>';
viz += '</div>';
// Grid with row/col labels
viz += '<div style="display:flex;gap:12px;flex-wrap:wrap;">';
viz += '<div>';
viz += '<div style="margin-bottom:6px;font-weight:600;font-size:14px;">原始矩阵</div>';
viz += renderGrid(origMatrix, { cellClass, cellSize: 44 });
viz += '</div>';
// Show result when available
if (s.result) {
viz += '<div>';
viz += '<div style="margin-bottom:6px;font-weight:600;font-size:14px;">置零结果</div>';
viz += renderGrid(s.result, { cellSize: 44 });
viz += '</div>';
}
viz += '</div>';
// Phase info box
if (s.phase === 'mark') {
const markers = Object.keys(s.markerCells);
const rowsMarked = [...new Set(markers.filter(k => k.split(',')[1]==='0').map(k=>k.split(',')[0]))];
const colsMarked = [...new Set(markers.filter(k => k.split(',')[0]==='0').map(k=>k.split(',')[1]))];
viz += '<div style="margin-top:12px;padding:10px 14px;background:#fef3c7;border-radius:8px;font-size:13px;line-height:1.7;">';
viz += '<b>标记状态:</b>需置零的行 = [' + rowsMarked.join(', ') + '],需置零的列 = [' + colsMarked.join(', ') + ']';
viz += '</div>';
}
if (s.phase === 'zero' || s.phase === 'first') {
const count = Object.keys(s.zeroedCells).length;
viz += '<div style="margin-top:12px;padding:10px 14px;background:#fee2e2;border-radius:8px;font-size:13px;line-height:1.7;">';
viz += `<b>置零进度:</b>已置零 ${count} 个格子`;
viz += '</div>';
}
$('vizArea').innerHTML = viz;
// Detail panel
const phaseLabels = {init:'初始化', scan:'扫描首行首列', mark:'标记', zero:'根据标记置零', first:'首行首列置零', done:'完成'};
const phaseClass = {init:'blue', scan:'blue', mark:'mark', zero:'zero', first:'first', done:'done'};
let detail = `<div style="margin-bottom:8px;"><span class="phase-tag ${phaseClass[s.phase]||''}">${phaseLabels[s.phase]||s.phase}</span></div>`;
detail += '<div class="calc-block">' + s.msg + '</div>';
detail += '<div class="info-row">';
if (s.firstRowZero !== null) detail += `<span class="tag ${s.firstRowZero?'red':'green'}">首行含0: ${s.firstRowZero?'是':'否'}</span>`;
if (s.firstColZero !== null) detail += `<span class="tag ${s.firstColZero?'red':'green'}">首列含0: ${s.firstColZero?'是':'否'}</span>`;
const mCount = Object.keys(s.markerCells).length;
if (mCount > 0) detail += `<span class="tag purple">标记数: ${mCount}</span>`;
const zCount = Object.keys(s.zeroedCells).length;
if (zCount > 0) detail += `<span class="tag red">已置零: ${zCount}</span>`;
detail += '</div>';
$('detailContent').innerHTML = detail;
// Result panel
if (s.stage === 'done') {
const rows = s.result.map(r => '[' + r.join(',') + ']');
$('resultContent').innerHTML = `<div class="final-answer">置零结果:<b>[${rows.join(', ')}]</b><div class="complexity">时间 O(mn) | 空间 O(1)</div></div>`;
}
$('hintText').textContent = s.msg;
const pipe = [
['init','初始化'],['scan_first_row','扫描首行'],['scan_first_col','扫描首列'],
['mark','标记0位置'],['zero_row','行置零'],['zero_col','列置零'],
['zero_first_row','首行置零'],['zero_first_col','首列置零'],['done','完成']
];
const stageToPhase = s => {
if (s.startsWith('scan_first_row')) return 'scan_first_row';
if (s.startsWith('scan_first_col')) return 'scan_first_col';
if (s.startsWith('mark')) return 'mark';
if (s.startsWith('zero_row')) return 'zero_row';
if (s.startsWith('zero_col')) return 'zero_col';
if (s.startsWith('zero_first_row')) return 'zero_first_row';
if (s.startsWith('zero_first_col')) return 'zero_first_col';
return s;
};
const activePhase = stageToPhase(s.stage);
$('pipeline').innerHTML = pipe.map(([k,l]) =>
`<span class="pipe-step ${activePhase===k?'active':''}">${l}</span>`
).join('<i>→</i>');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e, i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
$('inputArea').value = JSON.stringify(examples[0].input);
buildSteps(examples[0].input);
stepCtrl = new StepController({ onStep: render });
stepCtrl.setSteps(steps.map((_, i) => i));
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
stepCtrl.onStep = (idx) => { render(idx); $('stepInfo').textContent = `步骤 ${idx + 1} / ${steps.length}`; };
$('applyBtn').onclick = () => {
try {
const mat = JSON.parse($('inputArea').value);
if (!Array.isArray(mat) || !Array.isArray(mat[0])) throw 0;
buildSteps(mat);
stepCtrl.setSteps(steps.map((_, i) => i));
render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
} catch (e) { alert('请输入合法 JSON 二维数组,如 [[1,1,1],[1,0,1],[1,1,1]]'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = JSON.stringify(e.input);
buildSteps(e.input);
stepCtrl.setSteps(steps.map((_, i) => i));
render(0);
};
$('prevBtn').onclick = () => stepCtrl.prev();
$('nextBtn').onclick = () => stepCtrl.next();
$('jumpBtn').onclick = () => stepCtrl.jumpToEnd();
$('autoBtn').onclick = () => { const on = stepCtrl.toggleAuto(); $('autoBtn').textContent = on ? '暂停' : '自动播放'; };
$('resetBtn').onclick = () => { stepCtrl.reset(); $('autoBtn').textContent = '自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode(`def setZeroes(matrix):
m, n = len(matrix), len(matrix[0])
# 1. 记录首行/首列是否本身含0
first_row_zero = any(matrix[0][j] == 0 for j in range(n))
first_col_zero = any(matrix[i][0] == 0 for i in range(m))
# 2. 用首行/首列作为标记:内部0对应位置置0
for i in range(1, m):
for j in range(1, n):
if matrix[i][j] == 0:
matrix[i][0] = 0 # 行标记
matrix[0][j] = 0 # 列标记
# 3. 根据标记对内部区域置零
for i in range(1, m):
if matrix[i][0] == 0:
for j in range(1, n):
matrix[i][j] = 0
for j in range(1, n):
if matrix[0][j] == 0:
for i in range(1, m):
matrix[i][j] = 0
# 4. 处理首行/首列
if first_row_zero:
for j in range(n):
matrix[0][j] = 0
if first_col_zero:
for i in range(m):
matrix[i][0] = 0`, { lang: 'Python' });
})();
</script>
</body>
</html>