Files
illustrated-algorithm/rotate-image/index.html
T

425 lines
18 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>020. 旋转图像 – 图解</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; }
.rotate-pos0 { background:#dbeafe !important; border-color:#3b82f6 !important; color:#1e40af; font-weight:700; box-shadow:0 0 0 3px rgba(59,130,246,.25); }
.rotate-pos1 { background:#dcfce7 !important; border-color:#16a34a !important; color:#166534; font-weight:700; box-shadow:0 0 0 3px rgba(22,163,74,.25); }
.rotate-pos2 { background:#fef3c7 !important; border-color:#f59e0b !important; color:#92400e; font-weight:700; box-shadow:0 0 0 3px rgba(245,158,11,.25); }
.rotate-pos3 { background:#ede9fe !important; border-color:#8b5cf6 !important; color:#5b21b6; font-weight:700; box-shadow:0 0 0 3px rgba(139,92,246,.25); }
.rotate-swapped { background:#f0fdf4 !important; border-color:#86efac !important; color:#166534; }
.transpose-diag { background:#fef3c7 !important; border-color:#f59e0b !important; }
.transpose-swap { background:#dbeafe !important; border-color:#3b82f6 !important; box-shadow:0 0 0 3px rgba(59,130,246,.2); }
.flip-swap { background:#fef3c7 !important; border-color:#f59e0b !important; box-shadow:0 0 0 3px rgba(245,158,11,.2); }
.method-tabs { display:flex; gap:4px; margin-bottom:12px; }
.method-tab {
padding:6px 16px; border-radius:8px; cursor:pointer; font-size:14px;
border:1px solid #cbd5e1; background:#f8fafc; transition:all .2s;
}
.method-tab.active { background:#4f46e5; color:white; border-color:#4f46e5; }
.rotation-diagram {
display:flex; align-items:center; gap:8px; margin-top:10px;
font-size:13px; line-height:1.8; padding:10px;
background:#f8fafc; border-radius:8px;
}
.arrow-flow { font-size:16px; font-weight:700; }
.arrow-flow.blue { color:#3b82f6; }
.arrow-flow.green { color:#16a34a; }
.arrow-flow.orange { color:#f59e0b; }
.arrow-flow.purple { color:#8b5cf6; }
.pos-label {
display:inline-block; padding:1px 6px; border-radius:4px;
font-size:11px; font-weight:700; margin:0 2px;
}
.pos-label.p0 { background:#dbeafe; color:#1e40af; }
.pos-label.p1 { background:#dcfce7; color:#166534; }
.pos-label.p2 { background:#fef3c7; color:#92400e; }
.pos-label.p3 { background:#ede9fe; color:#5b21b6; }
</style>
</head>
<body>
<div class="container">
<h1>🔄 020. 旋转图像 <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:矩阵 | LeetCode Hot 100</p>
<div class="method-tabs" id="methodTabs">
<div class="method-tab active" data-method="rotation">方法一:四角旋转</div>
<div class="method-tab" data-method="transpose">方法二:转置+翻转</div>
</div>
<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: [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]], label: '4×4'},
];
let origMatrix, steps, stepCtrl, currentMethod = 'rotation';
// ===== Method 1: Four-corner rotation =====
function buildRotationSteps(mat) {
const n = mat.length;
const work = mat.map(r => [...r]);
steps = [];
steps.push({
stage: 'init', msg: '方法一:四角原地旋转 — 每次旋转4个元素,依次赋值',
phase: 'init', posGroup: [], swapped: {}, currentMatrix: work.map(r=>[...r])
});
for (let i = 0; i < Math.floor(n / 2); i++) {
for (let j = i; j < n - 1 - i; j++) {
const positions = [
[i, j], // pos0: top-left
[j, n-1-i], // pos1: top-right
[n-1-i, n-1-j], // pos2: bottom-right
[n-1-j, i], // pos3: bottom-left
];
const vals = positions.map(([r,c]) => work[r][c]);
// Show 4 positions
steps.push({
stage: 'show_4', msg: `四角组 (${i},${j}):<span class="pos-label p0">[${positions[0]}]=${vals[0]}</span> → <span class="pos-label p1">[${positions[1]}]=${vals[1]}</span> → <span class="pos-label p2">[${positions[2]}]=${vals[2]}</span> → <span class="pos-label p3">[${positions[3]}]=${vals[3]}</span>`,
phase: 'show', posGroup: positions, swapped: {}, values: vals,
currentMatrix: work.map(r=>[...r])
});
// Perform rotation: temp = pos0; pos0←pos3; pos3←pos2; pos2←pos1; pos1←temp
const temp = work[i][j];
work[i][j] = work[n-1-j][i];
work[n-1-j][i] = work[n-1-i][n-1-j];
work[n-1-i][n-1-j] = work[j][n-1-i];
work[j][n-1-i] = temp;
const newVals = positions.map(([r,c]) => work[r][c]);
steps.push({
stage: 'rotated', msg: `旋转完成:<span class="pos-label p0">[${positions[0]}]=${newVals[0]}</span> <span class="pos-label p1">[${positions[1]}]=${newVals[1]}</span> <span class="pos-label p2">[${positions[2]}]=${newVals[2]}</span> <span class="pos-label p3">[${positions[3]}]=${newVals[3]}</span>`,
phase: 'rotated', posGroup: positions, swapped: Object.fromEntries(positions.map(([r,c]) => [`${r},${c}`, true])),
values: newVals, currentMatrix: work.map(r=>[...r])
});
}
}
steps.push({
stage: 'done', msg: '顺时针旋转90°完成!',
phase: 'done', posGroup: [], swapped: {}, currentMatrix: work.map(r=>[...r])
});
}
// ===== Method 2: Transpose + Flip =====
function buildTransposeSteps(mat) {
const n = mat.length;
const work = mat.map(r => [...r]);
steps = [];
steps.push({
stage: 'init', msg: '方法二:先沿主对角线转置,再翻转每行',
phase: 'init', posGroup: [], swapped: {}, currentMatrix: work.map(r=>[...r])
});
// Phase 1: Transpose
steps.push({
stage: 'transpose_start', msg: '第一步:转置 — 交换 matrix[i][j] ↔ matrix[j][i](i < j)',
phase: 'transpose', posGroup: [], swapped: {}, currentMatrix: work.map(r=>[...r])
});
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
steps.push({
stage: 'transpose', msg: `转置:matrix[${i}][${j}]=${work[i][j]} ↔ matrix[${j}][${i}]=${work[j][i]}`,
phase: 'transpose', posGroup: [[i,j],[j,i]], swapped: {},
currentMatrix: work.map(r=>[...r])
});
[work[i][j], work[j][i]] = [work[j][i], work[i][j]];
steps.push({
stage: 'transpose_done', msg: `完成:matrix[${i}][${j}]=${work[i][j]}, matrix[${j}][${i}]=${work[j][i]}`,
phase: 'transpose', posGroup: [[i,j],[j,i]], swapped: {[[i,j],[j,i]].map(([r,c])=>`${r},${c}`).join(','): true},
currentMatrix: work.map(r=>[...r]),
swappedCells: {[`${i},${j}`]: true, [`${j},${i}`]: true}
});
}
}
steps.push({
stage: 'transpose_complete', msg: '转置完成!',
phase: 'transpose_done', posGroup: [], swapped: {},
currentMatrix: work.map(r=>[...r])
});
// Phase 2: Reverse each row
steps.push({
stage: 'reverse_start', msg: '第二步:翻转每行(左右交换)',
phase: 'reverse', posGroup: [], swapped: {},
currentMatrix: work.map(r=>[...r])
});
for (let i = 0; i < n; i++) {
let l = 0, r = n - 1;
while (l < r) {
steps.push({
stage: 'reverse', msg: `翻转第${i}行:matrix[${i}][${l}]=${work[i][l]} ↔ matrix[${i}][${r}]=${work[i][r]}`,
phase: 'reverse', posGroup: [[i,l],[i,r]], swapped: {},
currentMatrix: work.map(r=>[...r])
});
[work[i][l], work[i][r]] = [work[i][r], work[i][l]];
steps.push({
stage: 'reverse_done', msg: `完成:matrix[${i}][${l}]=${work[i][l]}, matrix[${i}][${r}]=${work[i][r]}`,
phase: 'reverse', posGroup: [[i,l],[i,r]], swapped: {},
currentMatrix: work.map(r=>[...r]),
swappedCells: {[`${i},${l}`]: true, [`${i},${r}`]: true}
});
l++; r--;
}
}
steps.push({
stage: 'done', msg: '旋转完成!顺时针90° = 转置 + 翻转每行',
phase: 'done', posGroup: [], swapped: {},
currentMatrix: work.map(r=>[...r])
});
}
function render(step) {
const s = steps[step];
function cellClass(val, r, c) {
const key = `${r},${c}`;
// Method 1: 4-corner rotation
if (currentMethod === 'rotation') {
const idx = s.posGroup ? s.posGroup.findIndex(([pr,pc]) => pr===r && pc===c) : -1;
if (idx >= 0) {
return `rotate-pos${idx}`;
}
if (s.swappedCells && s.swappedCells[key]) return 'rotate-swapped';
}
// Method 2: transpose/flip
if (currentMethod === 'transpose') {
if (s.phase === 'transpose') {
const idx = s.posGroup ? s.posGroup.findIndex(([pr,pc]) => pr===r && pc===c) : -1;
if (idx >= 0) return 'transpose-swap';
if (r === c) return 'transpose-diag';
}
if (s.phase === 'reverse') {
const idx = s.posGroup ? s.posGroup.findIndex(([pr,pc]) => pr===r && pc===c) : -1;
if (idx >= 0) return 'flip-swap';
}
if (s.swappedCells && s.swappedCells[key]) return 'rotate-swapped';
}
return '';
}
let viz = '<div class="legend">';
if (currentMethod === 'rotation') {
viz += '<span><span class="dot" style="background:#dbeafe;border:1px solid #3b82f6;"></span> <span class="pos-label p0">pos0</span></span>';
viz += '<span><span class="dot" style="background:#dcfce7;border:1px solid #16a34a;"></span> <span class="pos-label p1">pos1</span></span>';
viz += '<span><span class="dot" style="background:#fef3c7;border:1px solid #f59e0b;"></span> <span class="pos-label p2">pos2</span></span>';
viz += '<span><span class="dot" style="background:#ede9fe;border:1px solid #8b5cf6;"></span> <span class="pos-label p3">pos3</span></span>';
viz += '<span><span class="dot" style="background:#f0fdf4;border:1px solid #86efac;"></span> 已交换</span>';
} else {
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:#f0fdf4;border:1px solid #86efac;"></span> 已交换</span>';
}
viz += '</div>';
if (s.currentMatrix) {
viz += '<div style="margin-bottom:6px;font-weight:600;font-size:14px;">当前矩阵:</div>';
viz += renderGrid(s.currentMatrix, { cellClass, cellSize: 48 });
}
// Rotation diagram for method 1
if (currentMethod === 'rotation' && s.posGroup && s.posGroup.length === 4) {
viz += '<div class="rotation-diagram">';
viz += '<span class="pos-label p0">pos0 → pos1</span>';
viz += '<span class="arrow-flow blue">↻</span>';
viz += '<span class="pos-label p1">pos1 → pos2</span>';
viz += '<span class="arrow-flow green">↻</span>';
viz += '<span class="pos-label p2">pos2 → pos3</span>';
viz += '<span class="arrow-flow orange">↻</span>';
viz += '<span class="pos-label p3">pos3 → pos0</span>';
viz += '</div>';
if (s.values) {
viz += '<div style="margin-top:6px;font-size:13px;padding:8px 12px;background:#f8fafc;border-radius:8px;line-height:1.8;">';
viz += `<b>赋值链:</b>temp = ${s.values[0]}; `;
viz += `<span class="pos-label p0">[${s.posGroup[0]}]</span>=${s.stage==='rotated' ? s.values[3] : '?'}; `;
viz += `<span class="pos-label p3">[${s.posGroup[3]}]</span>=${s.stage==='rotated' ? s.values[2] : '?'}; `;
viz += `<span class="pos-label p2">[${s.posGroup[2]}]</span>=${s.stage==='rotated' ? s.values[1] : '?'}; `;
viz += `<span class="pos-label p1">[${s.posGroup[1]}]</span>=temp=${s.values[0]}`;
viz += '</div>';
}
}
// Transpose/flip diagram for method 2
if (currentMethod === 'transpose' && s.posGroup && s.posGroup.length === 2) {
const [p1, p2] = s.posGroup;
if (s.phase === 'transpose') {
viz += '<div style="margin-top:8px;padding:8px 12px;background:#eff6ff;border-radius:8px;font-size:13px;line-height:1.8;">';
viz += `<b>转置交换:</b>matrix[${p1}][${p1}] ↔ matrix[${p2}][${p2}](沿主对角线 ↗)`;
viz += '</div>';
} else if (s.phase === 'reverse') {
viz += '<div style="margin-top:8px;padding:8px 12px;background:#fef3c7;border-radius:8px;font-size:13px;line-height:1.8;">';
viz += `<b>行翻转:</b>第${p1[0]}行 matrix[${p1}][${p1[1]}] ↔ matrix[${p2}][${p2[1]}](左右对称 ←→)`;
viz += '</div>';
}
}
$('vizArea').innerHTML = viz;
// Detail
let detail = '<div class="calc-block">' + s.msg + '</div>';
if (currentMethod === 'rotation') {
detail += '<div style="margin-top:6px;font-size:13px;">方法:四角原地旋转 (temp→赋值链)</div>';
} else {
detail += `<div style="margin-top:6px;font-size:13px;">当前阶段:${s.phase==='transpose'?'转置':s.phase==='transpose_done'?'转置完成':s.phase==='reverse'?'翻转每行':'完成'}</div>`;
}
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
const rows = s.currentMatrix.map(r => '[' + r.join(',') + ']');
$('resultContent').innerHTML = `<div class="final-answer">旋转后:<b>[${rows.join(', ')}]</b><div class="complexity">时间 O(n²) | 空间 O(1)</div></div>`;
}
$('hintText').textContent = s.msg.replace(/<[^>]*>/g, '');
const pipe = currentMethod === 'rotation'
? [['init','初始化'],['show_4','显示四角'],['rotated','旋转完成'],['done','完成']]
: [['init','初始化'],['transpose','转置'],['transpose_done','转置完成'],['reverse','翻转'],['done','完成']];
$('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($('inputArea').value);
if (!Array.isArray(mat) || !Array.isArray(mat[0])) throw 0;
origMatrix = mat.map(r => [...r]);
if (currentMethod === 'rotation') buildRotationSteps(mat);
else buildTransposeSteps(mat);
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('请输入合法 n×n JSON 二维数组'); }
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e, i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
$('inputArea').value = JSON.stringify(examples[0].input);
// Method tabs
document.querySelectorAll('.method-tab').forEach(tab => {
tab.onclick = () => {
document.querySelectorAll('.method-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentMethod = tab.dataset.method;
updateCodeArea();
rebuild();
};
});
rebuild();
$('applyBtn').onclick = () => rebuild();
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = JSON.stringify(e.input);
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 = '自动播放'; };
}
function updateCodeArea() {
if (currentMethod === 'rotation') {
$('codeArea').innerHTML = renderCode(`def rotate(matrix):
n = len(matrix)
# 四角原地旋转
for i in range(n // 2):
for j in range(i, n - 1 - i):
# 记录4个位置
tmp = matrix[i][j]
matrix[i][j] = matrix[n-1-j][i] # pos3 → pos0
matrix[n-1-j][i] = matrix[n-1-i][n-1-j] # pos2 → pos3
matrix[n-1-i][n-1-j] = matrix[j][n-1-i] # pos1 → pos2
matrix[j][n-1-i] = tmp # pos0 → pos1`, { lang: 'Python' });
} else {
$('codeArea').innerHTML = renderCode(`def rotate(matrix):
n = len(matrix)
# 方法二:转置 + 翻转每行
# 1. 转置:沿主对角线交换
for i in range(n):
for j in range(i + 1, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# 2. 翻转每行
for i in range(n):
matrix[i].reverse()`, { lang: 'Python' });
}
}
init();
updateCodeArea();
})();
</script>
</body>
</html>