Files
illustrated-algorithm/rotate-image/index.html
T
2026-08-24 04:35:13 +00:00

425 lines
18 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>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>