Files
illustrated-algorithm/product-of-array-except-self/index.html
T
2026-08-24 04:35:13 +00:00

276 lines
10 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>016. 除自身以外数组的乘积 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 220px; padding: 16px 0; }
.code-section { margin-top: 16px; }
.arr-label {
font-size: 13px; font-weight: 600; margin: 10px 0 4px; display: flex; align-items: center; gap: 6px;
}
.arr-label .dot { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.arr-label .dot.blue { background: #3b82f6; }
.arr-label .dot.orange { background: #f59e0b; }
.arr-label .dot.green { background: #16a34a; }
.arr-label .dot.gray { background: #94a3b8; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 12px; font-weight: 600; margin: 0 2px; }
.tag.blue { background: #dbeafe; color: #1e40af; }
.tag.orange { background: #fff7ed; color: #9a3412; }
.tag.green { background: #dcfce7; color: #166534; }
.calc-line { font-family: monospace; font-size: 13px; line-height: 1.8; margin: 2px 0; }
</style>
</head>
<body>
<div class="container">
<h1>🟡 016. 除自身以外数组的乘积 <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], label:'示例1: [1,2,3,4]'},
{input:[-1,1,0,-3,3], label:'示例2: [-1,1,0,-3,3]'},
{input:[2,3,0,4], label:'含零: [2,3,0,4]'},
{input:[1,2,3], label:'短数组: [1,2,3]'},
];
let nums, steps, stepCtrl;
function buildSteps(arr) {
nums = [...arr];
steps = [];
const n = nums.length;
const left = new Array(n).fill(1);
const right = new Array(n).fill(1);
const answer = new Array(n).fill(1);
// Init step
steps.push({
stage:'init', i:-1, left:[...left], right:[...right], answer:[...answer],
msg:`初始化:left = right = answer = [${Array(n).fill(1).join(',')}]`
});
// Left pass
for (let i = 1; i < n; i++) {
left[i] = left[i-1] * nums[i-1];
steps.push({
stage:'left_pass', i, left:[...left], right:[...right], answer:[...answer],
msg:`← 左乘积: left[${i}] = left[${i-1}] × nums[${i-1}] = ${left[i-1]} × ${nums[i-1]} = ${left[i]}`
});
}
// Right pass
for (let i = n - 2; i >= 0; i--) {
right[i] = right[i+1] * nums[i+1];
steps.push({
stage:'right_pass', i, left:[...left], right:[...right], answer:[...answer],
msg:`→ 右乘积: right[${i}] = right[${i+1}] × nums[${i+1}] = ${right[i+1]} × ${nums[i+1]} = ${right[i]}`
});
}
// Multiply to get answer
for (let i = 0; i < n; i++) {
answer[i] = left[i] * right[i];
steps.push({
stage:'compute', i, left:[...left], right:[...right], answer:[...answer],
msg:`计算: answer[${i}] = left[${i}] × right[${i}] = ${left[i]} × ${right[i]} = ${answer[i]}`
});
}
steps.push({
stage:'done', i:-1, left:[...left], right:[...right], answer:[...answer],
msg:`计算完毕!answer = [${answer.join(', ')}]`
});
}
function render(rowArr, options = {}) {
const {highlights = {}, label = '', labelColor = 'gray'} = options;
let html = '';
if (label) {
html += `<div class="arr-label"><span class="dot ${labelColor}"></span>${label}</div>`;
}
html += renderArray(rowArr, {highlights});
return html;
}
function renderViz(step) {
const s = steps[step];
const n = nums.length;
let viz = '';
// Original array
viz += render(nums, {label: '原数组 nums', labelColor: 'gray'});
// Left prefix products
const leftHL = {};
if (s.stage === 'left_pass' && s.i >= 0) leftHL[s.i] = 'blue';
if (s.stage !== 'init' && s.stage !== 'left_pass') {
// highlight all filled
for (let i = 1; i < n; i++) leftHL[i] = 'blue';
}
viz += render(s.left, {highlights: leftHL, label: '左乘积 left (→)', labelColor: 'blue'});
// Right prefix products
const rightHL = {};
if (s.stage === 'right_pass' && s.i >= 0) rightHL[s.i] = 'orange';
if (s.stage === 'compute' || s.stage === 'done') {
for (let i = 0; i < n - 1; i++) rightHL[i] = 'orange';
}
viz += render(s.right, {highlights: rightHL, label: '右乘积 right (←)', labelColor: 'orange'});
// Answer
const ansHL = {};
if (s.stage === 'compute' && s.i >= 0) ansHL[s.i] = 'green';
if (s.stage === 'done') {
for (let i = 0; i < n; i++) ansHL[i] = 'green';
}
viz += render(s.answer, {highlights: ansHL, label: '结果 answer', labelColor: 'green'});
// Formula
viz += '<div class="formula-box" style="margin-top:12px;">';
viz += `<code>answer[i] = left[i] × right[i]</code><br>`;
viz += `<code>left[i] = nums[0] × ... × nums[i-1]</code> <code>right[i] = nums[i+1] × ... × nums[n-1]</code>`;
if (s.i >= 0 && (s.stage === 'left_pass' || s.stage === 'right_pass' || s.stage === 'compute')) {
viz += '<br>当前计算: ' + s.msg.split(':')[1]?.trim() || '';
}
viz += '</div>';
$('vizArea').innerHTML = viz;
}
function renderMain(step) {
const s = steps[step];
renderViz(step);
// Detail
let detail = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'left_pass' && s.i >= 0) {
detail += `<div class="calc-line"><span class="tag blue">left[${s.i}]</span> = left[${s.i-1}] × nums[${s.i-1}] = ${s.left[s.i-1]} × ${nums[s.i-1]} = <b>${s.left[s.i]}</b></div>`;
} else if (s.stage === 'right_pass' && s.i >= 0) {
detail += `<div class="calc-line"><span class="tag orange">right[${s.i}]</span> = right[${s.i+1}] × nums[${s.i+1}] = ${s.right[s.i+1]} × ${nums[s.i+1]} = <b>${s.right[s.i]}</b></div>`;
} else if (s.stage === 'compute' && s.i >= 0) {
detail += `<div class="calc-line"><span class="tag green">answer[${s.i}]</span> = left[${s.i}] × right[${s.i}] = ${s.left[s.i]} × ${s.right[s.i]} = <b>${s.answer[s.i]}</b></div>`;
}
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
$('resultContent').innerHTML = `<div class="final-answer">
结果:<b>[${s.answer.join(', ')}]</b><br>
除自身以外数组的乘积
<div class="complexity">时间复杂度 O(n) | 空间复杂度 O(1)(不算输出数组) | 左右前缀积</div>
</div>`;
}
$('hintText').textContent = s.msg;
const stages = [['init','初始化'],['left_pass','左乘积'],['right_pass','右乘积'],['compute','计算结果'],['done','完成']];
const activeMap = {
'init':'init','left_pass':'left_pass','right_pass':'right_pass','compute':'compute','done':'done'
};
$('pipeline').innerHTML = stages.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: renderMain});
stepCtrl.setSteps(steps.map((_,i)=>i));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = (idx) => { renderMain(idx); $('stepInfo').textContent = `步骤 ${idx+1} / ${steps.length}`; };
$('applyBtn').onclick = () => {
try {
const arr = JSON.parse($('inputArea').value);
if (!Array.isArray(arr) || arr.length < 2) throw new Error();
buildSteps(arr); stepCtrl.setSteps(steps.map((_,i)=>i)); renderMain(0);
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
} catch(e) { alert('请输入长度 ≥ 2 的合法 JSON 数组,例如 [1,2,3,4]'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = JSON.stringify(e.input);
buildSteps(e.input); stepCtrl.setSteps(steps.map((_,i)=>i)); renderMain(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 productExceptSelf(nums):
n = len(nums)
answer = [1] * n
# 左乘积
left = 1
for i in range(n):
answer[i] = left
left *= nums[i]
# 右乘积
right = 1
for i in range(n - 1, -1, -1):
answer[i] *= right
right *= nums[i]
return answer`, {lang:'Python'});
})();
</script>
</body>
</html>