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

364 lines
14 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>019. 螺旋矩阵 – 图解</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; }
.spiral-visited { background:#dcfce7 !important; border-color:#16a34a !important; color:#166534; position:relative; }
.spiral-current { background:#fef3c7 !important; border-color:#f59e0b !important; color:#92400e; box-shadow:0 0 0 3px rgba(245,158,11,.3); }
.spiral-boundary-top { border-top:3px solid #3b82f6 !important; }
.spiral-boundary-bottom { border-bottom:3px solid #ef4444 !important; }
.spiral-boundary-left { border-left:3px solid #8b5cf6 !important; }
.spiral-boundary-right { border-right:3px solid #f59e0b !important; }
.direction-badge {
display:inline-flex; align-items:center; gap:4px; padding:4px 12px;
border-radius:999px; font-size:14px; font-weight:700; margin:4px;
}
.direction-badge.top { background:#dbeafe; color:#1e40af; }
.direction-badge.right { background:#fef3c7; color:#92400e; }
.direction-badge.bottom { background:#fee2e2; color:#991b1b; }
.direction-badge.left { background:#ede9fe; color:#5b21b6; }
.boundary-info {
display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:10px;
}
.boundary-item {
padding:6px 12px; border-radius:8px; font-size:13px; font-weight:600; text-align:center;
}
.boundary-item.top-b { background:#dbeafe; color:#1e40af; }
.boundary-item.bottom-b { background:#fee2e2; color:#991b1b; }
.boundary-item.left-b { background:#ede9fe; color:#5b21b6; }
.boundary-item.right-b { background:#fef3c7; color:#92400e; }
.order-overlay {
position:absolute; top:-6px; right:-6px; min-width:16px; height:16px;
border-radius:50%; background:#16a34a; color:#fff; font-size:9px;
display:flex; align-items:center; justify-content:center; font-weight:700;
line-height:1; padding:0 2px; z-index:2;
}
</style>
</head>
<body>
<div class="container">
<h1>🌀 019. 螺旋矩阵 <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,2,3],[4,5,6],[7,8,9]], label: '3×3'},
{input: [[1,2,3,4],[5,6,7,8],[9,10,11,12]], label: '3×4'},
{input: [[1,2,3],[4,5,6],[7,8,9],[10,11,12]], label: '4×3'},
{input: [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], label: '4×4'},
];
let origMatrix, steps, stepCtrl, visitOrder;
// Pre-compute spiral visit order with positions
function computeVisitOrder(mat) {
const m = mat.length, n = mat[0].length;
const order = []; // [{r, c, val, direction, top, bottom, left, right}]
let top = 0, bottom = m - 1, left = 0, right = n - 1;
while (top <= bottom && left <= right) {
for (let j = left; j <= right; j++) order.push({r:top, c:j, val:mat[top][j], dir:'top', top, bottom, left, right});
top++;
if (top <= bottom) {
for (let i = top; i <= bottom; i++) order.push({r:i, c:right, val:mat[i][right], dir:'right', top:top-1, bottom, left, right});
}
right--;
if (top <= bottom && left <= right) {
for (let j = right; j >= left; j--) order.push({r:bottom, c:j, val:mat[bottom][j], dir:'bottom', top:top-1, bottom, left, right:right+1});
bottom--;
}
if (top <= bottom && left <= right) {
for (let i = bottom; i >= top; i--) order.push({r:i, c:left, val:mat[i][left], dir:'left', top, bottom:bottom+1, left, right:right+1});
left++;
}
}
return order;
}
function buildSteps(mat) {
origMatrix = mat.map(r => [...r]);
steps = [];
visitOrder = computeVisitOrder(mat);
const m = mat.length, n = mat[0].length;
// Step 0: Init
steps.push({
stage: 'init', msg: `按层螺旋遍历 ${m}×${n} 矩阵,维护 top/bottom/left/right 四个边界`,
visitedCount: 0, currentPos: null, direction: null,
top: 0, bottom: m-1, left: 0, right: n-1,
visitedSet: {}
});
// New layer step for each layer
let prevLayer = -1;
for (let idx = 0; idx < visitOrder.length; idx++) {
const v = visitOrder[idx];
const layerId = Math.min(v.r, v.c, m-1-v.r, n-1-v.c);
if (layerId > prevLayer) {
prevLayer = layerId;
steps.push({
stage: 'new_layer', msg: `进入第 ${layerId+1} 层:top=${v.top} bottom=${v.bottom} left=${v.left} right=${v.right}`,
visitedCount: idx, currentPos: null, direction: null,
top: v.top, bottom: v.bottom, left: v.left, right: v.right,
visitedSet: buildVisitedSet(idx)
});
}
const dirLabels = {top:'→ 向右', right:'↓ 向下', bottom:'← 向左', left:'↑ 向上'};
steps.push({
stage: v.dir, msg: `${dirLabels[v.dir]}:访问 matrix[${v.r}][${v.c}] = ${v.val}`,
visitedCount: idx + 1, currentPos: `${v.r},${v.c}`, direction: v.dir,
top: v.top, bottom: v.bottom, left: v.left, right: v.right,
visitedSet: buildVisitedSet(idx + 1)
});
}
steps.push({
stage: 'done', msg: `螺旋遍历完成!共访问 ${visitOrder.length} 个元素`,
visitedCount: visitOrder.length, currentPos: null, direction: null,
top: -1, bottom: -1, left: -1, right: -1,
visitedSet: buildVisitedSet(visitOrder.length)
});
}
function buildVisitedSet(count) {
const set = {};
for (let i = 0; i < count; i++) {
const v = visitOrder[i];
set[`${v.r},${v.c}`] = i + 1; // order number 1-based
}
return set;
}
function render(step) {
const s = steps[step];
const m = origMatrix.length, n = origMatrix[0].length;
function cellClass(val, r, c) {
const key = `${r},${c}`;
let cls = '';
if (s.currentPos === key) cls += ' spiral-current';
else if (s.visitedSet[key]) cls += ' spiral-visited';
// Boundary classes
if (s.top >= 0) {
if (r === s.top) cls += ' spiral-boundary-top';
if (r === s.bottom) cls += ' spiral-boundary-bottom';
if (c === s.left) cls += ' spiral-boundary-left';
if (c === s.right) cls += ' spiral-boundary-right';
}
return cls.trim();
}
function cellStyle(val, r, c) {
const key = `${r},${c}`;
if (s.visitedSet[key] && s.currentPos !== key) {
return 'position:relative;';
}
return '';
}
// Custom render with order number overlays
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:#dcfce7;border:1px solid #16a34a;"></span> 已访问</span>';
viz += '<span style="font-size:11px;color:#64748b;">边框粗线 = 当前层边界</span>';
viz += '</div>';
// Render grid
const grid = renderGrid(origMatrix, { cellClass, cellSize: 48, cellStyle });
viz += grid;
// Add order number overlays via JavaScript after rendering
// We'll do it by wrapping with IDs and post-processing
// Instead, let's render manually with overlays
// Re-render with custom cell rendering to include order numbers
let gridHtml = `<div class="grid-viz" style="grid-template-columns:repeat(${n}, 48px);">`;
for (let r = 0; r < m; r++) {
for (let c = 0; c < n; c++) {
const val = origMatrix[r][c];
const key = `${r},${c}`;
const cls = cellClass(val, r, c);
const style = cellStyle(val, r, c);
const orderNum = s.visitedSet[key];
let overlay = '';
if (orderNum && s.currentPos !== key) {
overlay = `<span class="order-overlay">${orderNum}</span>`;
}
gridHtml += `<div class="grid-cell ${cls}" style="width:48px;height:48px;${style}">${val}${overlay}</div>`;
}
}
gridHtml += '</div>';
viz = viz.replace(grid, gridHtml); // replace the renderGrid output
// Direction badge
if (s.direction) {
const dirMap = {top:'→ 向右', right:'↓ 向下', bottom:'← 向左', left:'↑ 向上'};
viz += `<div style="margin-top:10px;"><span class="direction-badge ${s.direction}">${dirMap[s.direction]}</span></div>`;
}
// Boundary info
if (s.top >= 0) {
viz += '<div class="boundary-info">';
viz += `<div class="boundary-item top-b">top = ${s.top}</div>`;
viz += `<div class="boundary-item bottom-b">bottom = ${s.bottom}</div>`;
viz += `<div class="boundary-item left-b">left = ${s.left}</div>`;
viz += `<div class="boundary-item right-b">right = ${s.right}</div>`;
viz += '</div>';
}
// Visited order sequence
const visitedVals = [];
for (let i = 0; i < s.visitedCount; i++) visitedVals.push(visitOrder[i].val);
if (visitedVals.length > 0) {
viz += '<div style="margin-top:12px;"><b>已访问序列:</b></div>';
viz += '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;">';
visitedVals.forEach((v, i) => {
viz += `<span class="chip green" style="min-width:30px;height:28px;font-size:12px;padding:2px 6px;">${v}</span>`;
});
viz += '</div>';
}
$('vizArea').innerHTML = viz;
// Detail
let detail = '<div class="calc-block">' + s.msg + '</div>';
if (s.currentPos) {
const [cr, cc] = s.currentPos.split(',').map(Number);
detail += `<div style="margin-top:6px;font-size:13px;">当前位置:matrix[${cr}][${cc}] = ${origMatrix[cr][cc]}</div>`;
}
detail += `<div style="margin-top:6px;font-size:13px;">已访问:${s.visitedCount} / ${visitOrder.length} 个元素</div>`;
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
$('resultContent').innerHTML = `<div class="final-answer">螺旋顺序:<b>[${visitOrder.map(v=>v.val)}]</b><div class="complexity">时间 O(mn) | 空间 O(1)(不含输出数组)</div></div>`;
}
$('hintText').textContent = s.msg;
const pipe = [['init','初始化'],['new_layer','新层'],['top','→ 向右'],['right','↓ 向下'],['bottom','← 向左'],['left','↑ 向上'],['done','完成']];
$('pipeline').innerHTML = pipe.map(([k,l]) =>
`<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 = 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 二维数组'); }
};
$('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 spiralOrder(matrix):
m, n = len(matrix), len(matrix[0])
top, bottom = 0, m - 1
left, right = 0, n - 1
result = []
while top <= bottom and left <= right:
# 向右 →
for j in range(left, right + 1):
result.append(matrix[top][j])
top += 1
# 向下 ↓
for i in range(top, bottom + 1):
result.append(matrix[i][right])
right -= 1
# 向左 ←
if top <= bottom:
for j in range(right, left - 1, -1):
result.append(matrix[bottom][j])
bottom -= 1
# 向上 ↑
if left <= right:
for i in range(bottom, top - 1, -1):
result.append(matrix[i][left])
left += 1
return result`, { lang: 'Python' });
})();
</script>
</body>
</html>