Files
illustrated-algorithm/search-a-2d-matrix/index.html
T
2026-08-24 04:35:13 +00:00

250 lines
11 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>064. 搜索二维矩阵 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 120px; padding: 16px 0; }
.code-section { margin-top: 16px; }
.phase-tag { display:inline-block; padding:2px 10px; border-radius:999px; font-size:12px; font-weight:600; margin-bottom:8px; }
.phase-tag.row { background:#dbeafe; color:#1e40af; }
.phase-tag.col { background:#dcfce7; color:#166534; }
</style>
</head>
<body>
<div class="container">
<h1>🟡 064. 搜索二维矩阵 <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="matrix=[[1,3,5,7],[10,11,16,20],[23,30,34,60]], target=3">
<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 = [
{matrix:[[1,3,5,7],[10,11,16,20],[23,30,34,60]], target:3, label:'示例1: target=3'},
{matrix:[[1,3,5,7],[10,11,16,20],[23,30,34,60]], target:13, label:'示例2: target=13'},
{matrix:[[1]], target:1, label:'示例3: 1×1矩阵'},
];
let matrix, target, steps, stepCtrl;
function buildSteps(mat, tgt) {
matrix = mat; target = tgt;
steps = [];
const m = mat.length, n = mat[0].length;
steps.push({phase:'init', stage:'start', msg:`初始化:${m}行×${n}列矩阵,target=${tgt}`, rowL:0, rowR:m-1, rowMid:-1, colL:-1, colR:-1, colMid:-1, found:false, hlRow:-1, hlCell:''});
// Phase 1: binary search on rows
let rowL = 0, rowR = m - 1;
steps.push({phase:'row', stage:'rowStart', msg:`第一步:在行上二分,找到 target 可能所在的行`, rowL, rowR, rowMid:-1, colL:-1, colR:-1, colMid:-1, found:false, hlRow:-1, hlCell:''});
let targetRow = -1;
while (rowL <= rowR) {
const rowMid = Math.floor((rowL + rowR) / 2);
steps.push({phase:'row', stage:'rowCalc', msg:`行二分:rowMid = ⌊(${rowL}+${rowR})/2⌋ = ${rowMid},第${rowMid}行首元素=${mat[rowMid][0]},末元素=${mat[rowMid][n-1]}`, rowL, rowR, rowMid, colL:-1, colR:-1, colMid:-1, found:false, hlRow:rowMid, hlCell:''});
if (mat[rowMid][0] > tgt) {
steps.push({phase:'row', stage:'rowUp', msg:`第${rowMid}行首元素 ${mat[rowMid][0]} > ${tgt},target 不会在当前行及以下,rowR = ${rowMid}-1 = ${rowMid-1}`, rowL, rowR:rowMid-1, rowMid, colL:-1, colR:-1, colMid:-1, found:false, hlRow:rowMid, hlCell:''});
rowR = rowMid - 1;
} else if (mat[rowMid][n-1] < tgt) {
steps.push({phase:'row', stage:'rowDown', msg:`第${rowMid}行末元素 ${mat[rowMid][n-1]} < ${tgt},target 不在当前行及以上,rowL = ${rowMid}+1 = ${rowMid+1}`, rowL:rowMid+1, rowR, rowMid, colL:-1, colR:-1, colMid:-1, found:false, hlRow:rowMid, hlCell:''});
rowL = rowMid + 1;
} else {
steps.push({phase:'row', stage:'rowFound', msg:`${tgt} ∈ [${mat[rowMid][0]}, ${mat[rowMid][n-1]}],target 可能在第 ${rowMid} 行`, rowL, rowR, rowMid, colL:-1, colR:-1, colMid:-1, found:false, hlRow:rowMid, hlCell:''});
targetRow = rowMid;
break;
}
}
if (targetRow === -1) {
steps.push({phase:'done', stage:'notFound', msg:`未找到包含 ${tgt} 的行,返回 false`, rowL, rowR, rowMid:-1, colL:-1, colR:-1, colMid:-1, found:false, hlRow:-1, hlCell:''});
return;
}
// Phase 2: binary search on columns
let colL = 0, colR = n - 1;
steps.push({phase:'col', stage:'colStart', msg:`第二步:在第 ${targetRow} 行内二分查找 ${tgt}`, rowL, rowR, rowMid:targetRow, colL, colR, colMid:-1, found:false, hlRow:targetRow, hlCell:''});
while (colL <= colR) {
const colMid = Math.floor((colL + colR) / 2);
steps.push({phase:'col', stage:'colCalc', msg:`列二分:colMid = ⌊(${colL}+${colR})/2⌋ = ${colMid},matrix[${targetRow}][${colMid}] = ${mat[targetRow][colMid]}`, rowL, rowR, rowMid:targetRow, colL, colR, colMid, found:false, hlRow:targetRow, hlCell:`${targetRow},${colMid}`});
if (mat[targetRow][colMid] === tgt) {
steps.push({phase:'col', stage:'found', msg:`matrix[${targetRow}][${colMid}] = ${mat[targetRow][colMid]} == ${tgt},找到!`, rowL, rowR, rowMid:targetRow, colL, colR, colMid, found:true, hlRow:targetRow, hlCell:`${targetRow},${colMid}`});
return;
} else if (mat[targetRow][colMid] < tgt) {
steps.push({phase:'col', stage:'colRight', msg:`${mat[targetRow][colMid]} < ${tgt},右半区查找,colL = ${colMid}+1 = ${colMid+1}`, rowL, rowR, rowMid:targetRow, colL:colMid+1, colR, colMid, found:false, hlRow:targetRow, hlCell:`${targetRow},${colMid}`});
colL = colMid + 1;
} else {
steps.push({phase:'col', stage:'colLeft', msg:`${mat[targetRow][colMid]} > ${tgt},左半区查找,colR = ${colMid}-1 = ${colMid-1}`, rowL, rowR, rowMid:targetRow, colL, colR:colMid-1, colMid, found:false, hlRow:targetRow, hlCell:`${targetRow},${colMid}`});
colR = colMid - 1;
}
}
steps.push({phase:'done', stage:'notFound', msg:`在第 ${targetRow} 行中未找到 ${tgt},返回 false`, rowL, rowR, rowMid:targetRow, colL, colR, colMid:-1, found:false, hlRow:targetRow, hlCell:''});
}
function render(step) {
const s = steps[step];
const mat = matrix;
const m = mat.length, n = mat[0].length;
// Matrix visualization
const gridHl = {};
if (s.hlCell) gridHl[s.hlCell] = 'current';
const rowHlH = {};
if (s.hlRow >= 0) {
for (let c = 0; c < n; c++) {
if (s.hlCell !== `${s.hlRow},${c}`) gridHl[`${s.hlRow},${c}`] = 'visited';
}
}
let viz = `<div class="phase-tag ${s.phase==='row'||s.phase==='init'?'row':'col'}">${s.phase==='row'||s.phase==='init'?'阶段一:行二分':'阶段二:列二分'}</div>`;
viz += renderGrid(mat, {highlights: gridHl, cellSize: 44});
// Row binary search info
if (s.rowMid >= 0) {
viz += `<div style="margin-top:10px;font-size:13px;">行搜索:left=${s.rowL}, right=${s.rowR}, mid=${s.rowMid}`;
if (s.rowMid < m) viz += ` → 行首=${mat[s.rowMid]?.[0]}, 行末=${mat[s.rowMid]?.[n-1]}`;
viz += '</div>';
}
if (s.colMid >= 0) {
viz += `<div style="font-size:13px;">列搜索:left=${s.colL}, right=${s.colR}, mid=${s.colMid} → 值=${mat[s.hlRow][s.colMid]}</div>`;
}
viz += `<div style="font-size:13px;color:var(--purple);font-weight:600;">target = ${target}</div>`;
if (s.found) {
viz += `<div class="current-answer">✅ 找到!matrix[${s.hlRow}][${s.colMid}] = ${target}</div>`;
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = `<div class="calc-block">${s.msg}</div>`;
if (s.stage === 'found') {
$('resultContent').innerHTML = `<div class="final-answer">返回 <b>true</b><br>位置:matrix[${s.hlRow}][${s.colMid}] = ${target}<br>时间复杂度 O(log m + log n)</div>`;
} else if (s.stage === 'notFound') {
$('resultContent').innerHTML = `<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">返回 <b>false</b><br>矩阵中不存在 ${target}</div>`;
}
$('hintText').textContent = s.msg;
const stages = ['start→开始','rowStart→行搜索','rowCalc→行计算','rowFound→定位行','colStart→列搜索','colCalc→列计算','found→找到','notFound→未找到'];
$('pipeline').innerHTML = stages.map(st => {
const [k,l] = st.split('→');
return `<span class="pipe-step ${s.stage===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 = 'matrix=[[1,3,5,7],[10,11,16,20],[23,30,34,60]], target=3';
buildSteps(examples[0].matrix, examples[0].target);
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 m = $('inputArea').value.match(/matrix=(\[\[.*?\]\]).*target=(-?\d+)/s);
if (!m) { alert('格式: matrix=[[1,3,5,7],[10,11,16,20]], target=3'); return; }
const mat = JSON.parse(m[1]);
const tgt = parseInt(m[2]);
buildSteps(mat, tgt);
stepCtrl.setSteps(steps.map((_,i)=>i)); render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
} catch(e) { alert('输入格式错误'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = `matrix=${JSON.stringify(e.matrix)}, target=${e.target}`;
buildSteps(e.matrix, e.target);
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 searchMatrix(matrix, target):
m, n = len(matrix), len(matrix[0])
# 第一步:二分找行
top, bottom = 0, m - 1
while top <= bottom:
row = (top + bottom) // 2
if matrix[row][0] > target:
bottom = row - 1
elif matrix[row][-1] < target:
top = row + 1
else:
break
if top > bottom:
return False
row = (top + bottom) // 2
# 第二步:在行内二分
l, r = 0, n - 1
while l <= r:
mid = (l + r) // 2
if matrix[row][mid] == target:
return True
elif matrix[row][mid] < target:
l = mid + 1
else:
r = mid - 1
return False`, {lang:'Python'});
})();
</script>
</body>
</html>