Files
2026-08-24 04:35:13 +00:00

356 lines
15 KiB
HTML
Raw Permalink 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>021. 搜索二维矩阵 II – 图解</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; }
.search-current { background:#fef3c7 !important; border-color:#f59e0b !important; color:#92400e; font-weight:700; box-shadow:0 0 0 3px rgba(245,158,11,.3); }
.search-visited { background:#dbeafe !important; border-color:#3b82f6 !important; color:#1e40af; }
.search-excluded { background:#f1f5f9 !important; color:#94a3b8 !important; border-color:#e2e8f0 !important; }
.search-found { background:#dcfce7 !important; border-color:#16a34a !important; color:#166534; font-weight:700; box-shadow:0 0 0 4px rgba(22,163,74,.25); }
.search-start { background:#ede9fe !important; border-color:#8b5cf6 !important; color:#5b21b6; font-weight:700; }
.comparison-box {
display:flex; align-items:center; gap:8px; padding:10px 16px;
border-radius:10px; margin-top:10px; font-size:14px; font-weight:600;
}
.comparison-box.greater { background:#fee2e2; color:#991b1b; }
.comparison-box.less { background:#dbeafe; color:#1e40af; }
.comparison-box.equal { background:#dcfce7; color:#166534; }
.move-arrow { font-size:20px; font-weight:800; }
.path-chip { font-size:12px; }
.excluded-info {
display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:8px;
}
.excluded-item {
padding:6px 12px; border-radius:8px; font-size:12px; text-align:center;
}
.excluded-item.rows-ex { background:#fef3c7; color:#92400e; }
.excluded-item.cols-ex { background:#cffafe; color:#155e75; }
.excluded-item.active-row { background:#fef3c7; color:#92400e; font-weight:700; }
.excluded-item.active-col { background:#cffafe; color:#155e75; font-weight:700; }
</style>
</head>
<body>
<div class="container">
<h1>🔍 021. 搜索二维矩阵 II <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:矩阵 | LeetCode Hot 100</p>
<div class="controls" id="controls">
<label>矩阵:</label>
<input type="text" id="matrixInput" placeholder="默认示例" style="width:280px;">
<label style="margin-left:8px;">target:</label>
<input type="number" id="targetInput" value="5" style="width:80px;">
<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,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target: 5, label: 'target=5 (存在)'},
{input: [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target: 20, label: 'target=20 (不存在)'},
{input: [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target: 1, label: 'target=1 (左上角)'},
{input: [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target: 30, label: 'target=30 (右下角)'},
];
let origMatrix, targetVal, steps, stepCtrl;
function buildSteps(mat, target) {
origMatrix = mat.map(r => [...r]);
targetVal = target;
steps = [];
const m = mat.length, n = mat[0].length;
const visited = []; // [{r, c, val, comparison}]
const excludedRows = new Set();
const excludedCols = new Set();
let found = false;
let foundPos = null;
// Build excluded cells: rows above visited that went down, columns right of visited that went left
function getExcludedCells() {
const cells = {};
// Exclude rows that were fully ruled out (rows above current that we moved down from)
for (const r of excludedRows) {
for (let c = 0; c < n; c++) {
cells[`${r},${c}`] = true;
}
}
// Exclude columns that were fully ruled out
for (const c of excludedCols) {
for (let r = 0; r < m; r++) {
cells[`${r},${c}`] = true;
}
}
return cells;
}
// Init
steps.push({
stage: 'init', msg: `从右上角 (0, ${n-1}) 开始搜索 target = ${target}。性质:每行左→右递增,每列上→下递增`,
pos: [0, n-1], visited: [], found: false, foundPos: null,
excludedCells: {}, excludedRows: new Set(), excludedCols: new Set(),
comparison: null, currentRow: 0, currentCol: n-1
});
let i = 0, j = n - 1;
while (i < m && j >= 0) {
const val = mat[i][j];
visited.push({r: i, c: j, val, comparison: val === target ? '=' : val > target ? '>' : '<'});
if (val === target) {
found = true;
foundPos = [i, j];
steps.push({
stage: 'found', msg: `找到!matrix[${i}][${j}] = ${val} === target = ${target}`,
pos: [i, j], visited: [...visited], found: true, foundPos: [i, j],
excludedCells: getExcludedCells(),
excludedRows: new Set(excludedRows), excludedCols: new Set(excludedCols),
comparison: '=', currentRow: i, currentCol: j
});
break;
} else if (val > target) {
// Value too large → move left (exclude this column)
excludedCols.add(j);
steps.push({
stage: 'move_left', msg: `matrix[${i}][${j}] = ${val} > ${target},偏大 ← 向左移动,排除第${j}列`,
pos: [i, j], visited: [...visited], found: false, foundPos: null,
excludedCells: getExcludedCells(),
excludedRows: new Set(excludedRows), excludedCols: new Set(excludedCols),
comparison: '>', currentRow: i, currentCol: j, nextCol: j-1
});
j--;
} else {
// Value too small → move down (exclude this row)
excludedRows.add(i);
steps.push({
stage: 'move_down', msg: `matrix[${i}][${j}] = ${val} < ${target},偏小 ↓ 向下移动,排除第${i}行`,
pos: [i, j], visited: [...visited], found: false, foundPos: null,
excludedCells: getExcludedCells(),
excludedRows: new Set(excludedRows), excludedCols: new Set(excludedCols),
comparison: '<', currentRow: i, currentCol: j, nextRow: i+1
});
i++;
}
// Step after moving
if (i < m && j >= 0) {
steps.push({
stage: 'arrived', msg: `到达 matrix[${i}][${j}] = ${mat[i][j]}`,
pos: [i, j], visited: [...visited], found: false, foundPos: null,
excludedCells: getExcludedCells(),
excludedRows: new Set(excludedRows), excludedCols: new Set(excludedCols),
comparison: null, currentRow: i, currentCol: j
});
}
}
if (!found) {
steps.push({
stage: 'not_found', msg: `越界,target = ${target} 不在矩阵中`,
pos: null, visited: [...visited], found: false, foundPos: null,
excludedCells: getExcludedCells(),
excludedRows: new Set(excludedRows), excludedCols: new Set(excludedCols),
comparison: null, currentRow: -1, currentCol: -1
});
}
}
function render(step) {
const s = steps[step];
const m = origMatrix.length, n = origMatrix[0].length;
// Build visited set
const visitedSet = {};
for (const v of s.visited) {
visitedSet[`${v.r},${v.c}`] = v;
}
function cellClass(val, r, c) {
const key = `${r},${c}`;
// Found
if (s.found && s.foundPos && r === s.foundPos[0] && c === s.foundPos[1]) return 'search-found';
// Current position
if (s.pos && r === s.pos[0] && c === s.pos[1]) return 'search-current';
// Visited
if (visitedSet[key]) return 'search-visited';
// Start position (top-right)
if (r === 0 && c === n-1 && !visitedSet[key] && s.stage === 'init') return 'search-start';
// Excluded
if (s.excludedCells[key]) return 'search-excluded';
return '';
}
// Legend
let viz = '<div class="legend">';
viz += '<span><span class="dot" style="background:#fef3c7;border:1px solid #f59e0b;"></span> 当前位置</span>';
viz += '<span><span class="dot" style="background:#dbeafe;border:1px solid #3b82f6;"></span> 搜索路径</span>';
viz += '<span><span class="dot" style="background:#f1f5f9;border:1px solid #e2e8f0;"></span> 已排除区域</span>';
viz += '<span><span class="dot" style="background:#dcfce7;border:1px solid #16a34a;"></span> 找到目标</span>';
viz += '</div>';
// Grid
viz += renderGrid(origMatrix, { cellClass, cellSize: 48 });
// Comparison box
if (s.comparison) {
const compClass = s.comparison === '>' ? 'greater' : s.comparison === '<' ? 'less' : 'equal';
const arrow = s.comparison === '>' ? '⬅ 向左' : s.comparison === '<' ? '⬇ 向下' : '✅ 匹配';
const val = s.pos ? origMatrix[s.pos[0]][s.pos[1]] : '?';
viz += `<div class="comparison-box ${compClass}">`;
viz += `matrix[${s.pos?s.pos[0]:'?'}][${s.pos?s.pos[1]:'?'}] = ${val} ${s.comparison} target=${targetVal}`;
viz += `<span class="move-arrow">${arrow}</span>`;
viz += '</div>';
}
// Excluded info
const exRows = s.excludedRows ? [...s.excludedRows] : [];
const exCols = s.excludedCols ? [...s.excludedCols] : [];
if (exRows.length > 0 || exCols.length > 0) {
viz += '<div class="excluded-info">';
viz += `<div class="excluded-item rows-ex">已排除行:[${exRows.join(', ')}]</div>`;
viz += `<div class="excluded-item cols-ex">已排除列:[${exCols.join(', ')}]</div>`;
const totalExcluded = exRows.length * n + exCols.length * m - exRows.length * exCols.length;
viz += `<div class="excluded-item rows-ex">已排除格子数:约 ${totalExcluded}</div>`;
viz += `<div class="excluded-item cols-ex">剩余搜索空间:${m*n - totalExcluded}</div>`;
viz += '</div>';
}
// Path chips
if (s.visited.length > 0) {
viz += '<div style="margin-top:12px;"><b>搜索路径:</b></div>';
viz += '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;">';
s.visited.forEach((v, idx) => {
const isCurrent = s.pos && v.r === s.pos[0] && v.c === s.pos[1];
const isFound = s.found && idx === s.visited.length - 1;
const chipCls = isFound ? 'green' : isCurrent ? 'orange' : 'blue';
const compSymbol = v.comparison === '>' ? ' >' : v.comparison === '<' ? ' <' : v.comparison === '=' ? ' =' : '';
viz += `<span class="chip ${chipCls} path-chip" style="min-width:auto;padding:4px 10px;font-size:12px;">[${v.r}][${v.c}]=${v.val}${compSymbol}</span>`;
if (idx < s.visited.length - 1) viz += '<span style="color:var(--text-muted);font-size:16px;">→</span>';
});
viz += '</div>';
}
$('vizArea').innerHTML = viz;
// Detail
let detail = '<div class="calc-block">' + s.msg + '</div>';
detail += `<div style="margin-top:6px;font-size:13px;">target = <b>${targetVal}</b> | 已搜索 ${s.visited.length} 个位置</div>`;
if (s.pos) {
detail += `<div style="margin-top:4px;font-size:13px;">当前位置:(${s.pos[0]}, ${s.pos[1]}) = ${origMatrix[s.pos[0]][s.pos[1]]}</div>`;
}
$('detailContent').innerHTML = detail;
// Result
if (s.stage === 'found') {
$('resultContent').innerHTML = `<div class="final-answer">找到 target=<b>${targetVal}</b>,位置 matrix[${s.foundPos[0]}][${s.foundPos[1]}]<div class="complexity">时间 O(m+n) | 空间 O(1)</div></div>`;
} else if (s.stage === 'not_found') {
$('resultContent').innerHTML = `<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">target=<b>${targetVal}</b> 不在矩阵中<div class="complexity">时间 O(m+n) | 空间 O(1)</div></div>`;
}
$('hintText').textContent = s.msg;
const pipe = [['init','初始化'],['move_left','⬅ 向左'],['move_down','⬇ 向下'],['arrived','到达'],['found','✅ 找到'],['not_found','❌ 未找到']];
$('pipeline').innerHTML = pipe.map(([k,l]) =>
`<span class="pipe-step ${s.stage===k?'active':''}">${l}</span>`
).join('<i>→</i>');
}
function rebuild() {
try {
const mat = JSON.parse($('matrixInput').value);
const tgt = parseInt($('targetInput').value);
if (!Array.isArray(mat) || isNaN(tgt)) throw 0;
buildSteps(mat, tgt);
stepCtrl = new StepController({ onStep: render });
stepCtrl.setSteps(steps.map((_, i) => i));
render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
stepCtrl.onStep = (idx) => { render(idx); $('stepInfo').textContent = `步骤 ${idx + 1} / ${steps.length}`; };
} catch (e) { alert('请输入合法 JSON 二维数组和 target 数字'); }
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e, i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
$('matrixInput').value = JSON.stringify(examples[0].input);
$('targetInput').value = examples[0].target;
rebuild();
$('applyBtn').onclick = () => rebuild();
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('matrixInput').value = JSON.stringify(e.input);
$('targetInput').value = e.target;
rebuild();
};
$('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 searchMatrix(matrix, target):
m, n = len(matrix), len(matrix[0])
i, j = 0, n - 1 # 从右上角出发
while i < m and j >= 0:
if matrix[i][j] == target:
return True
elif matrix[i][j] > target:
j -= 1 # 偏大 → 向左
else:
i += 1 # 偏小 → 向下
return False`, { lang: 'Python' });
})();
</script>
</body>
</html>