This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>077. 买卖股票的最佳时机 – 图解</title>
|
||||
<link rel="stylesheet" href="../shared/style.css">
|
||||
<style>.vis-area { min-height: 120px; padding: 16px 0; } .code-section { margin-top: 16px; }</style>
|
||||
</head>
|
||||
<body><div class="container">
|
||||
<h1>🟢 077. 买卖股票的最佳时机 <span class="badge easy">简单</span></h1>
|
||||
<p class="subtitle">分类:贪心 | LeetCode Hot 100</p>
|
||||
<div class="controls" id="controls">
|
||||
<label>输入:</label><input type="text" id="inputArea" value="[7,1,5,3,6,4]">
|
||||
<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 CODE = `def maxProfit(prices):
|
||||
min_price = float('inf')
|
||||
max_profit = 0
|
||||
for i, price in enumerate(prices):
|
||||
if price < min_price:
|
||||
min_price = price # 更新最低价
|
||||
elif price - min_price > max_profit:
|
||||
max_profit = price - min_price # 更新最大利润
|
||||
return max_profit`;
|
||||
|
||||
const EXAMPLES = [
|
||||
{ name: '例1: [7,1,5,3,6,4]', input: '[7,1,5,3,6,4]' },
|
||||
{ name: '例2: [7,6,4,3,1]', input: '[7,6,4,3,1]' },
|
||||
{ name: '例3: [2,4,1]', input: '[2,4,1]' },
|
||||
{ name: '例4: [3,2,6,5,0,3]', input: '[3,2,6,5,0,3]' },
|
||||
];
|
||||
|
||||
let controller = null;
|
||||
|
||||
function genSteps(prices) {
|
||||
const steps = [];
|
||||
let minPrice = Infinity;
|
||||
let minIdx = -1;
|
||||
let maxProfit = 0;
|
||||
let buyIdx = -1, sellIdx = -1;
|
||||
|
||||
steps.push({
|
||||
desc: '初始化:min_price = ∞, max_profit = 0',
|
||||
hint: '遍历价格数组,维护历史最低价和最大利润差。',
|
||||
detail: '<b>思路</b>:只需一次遍历。<br>• 遇到更低价格 → 更新 min_price<br>• 否则计算 price - min_price,若更大则更新 max_profit<br><br>时间复杂度 O(n),空间 O(1)',
|
||||
hlLine: -1,
|
||||
arrCls: prices.map(() => ''),
|
||||
arrTags: prices.map(() => ''),
|
||||
minIdx: -1, curIdx: -1,
|
||||
maxProfit: 0, bestBuy: -1, bestSell: -1,
|
||||
});
|
||||
|
||||
prices.forEach((price, i) => {
|
||||
const arrCls = prices.map(() => '');
|
||||
const arrTags = prices.map(() => '');
|
||||
if (minIdx >= 0) arrCls[minIdx] = 'min-price';
|
||||
arrCls[i] = 'current';
|
||||
|
||||
if (price < minPrice) {
|
||||
const oldMin = minPrice;
|
||||
minPrice = price;
|
||||
minIdx = i;
|
||||
|
||||
arrCls[minIdx] = 'min-price';
|
||||
arrTags[minIdx] = '最低';
|
||||
|
||||
steps.push({
|
||||
desc: `i=${i}: prices[${i}] = ${price} < ${oldMin === Infinity ? '∞' : oldMin} → 更新最低价`,
|
||||
hint: `发现更低价格 ${price},更新 min_price。此时卖出不产生利润,继续寻找更高卖价。`,
|
||||
detail: `prices[${i}] = <b>${price}</b><br>当前 min_price: ${oldMin === Infinity ? '∞' : oldMin} → <b>${minPrice}</b><br>max_profit 不变 = ${maxProfit}`,
|
||||
hlLine: 4,
|
||||
arrCls: [...arrCls], arrTags: [...arrTags],
|
||||
minIdx, curIdx: i, maxProfit, bestBuy, bestSell,
|
||||
});
|
||||
} else {
|
||||
const profit = price - minPrice;
|
||||
if (profit > maxProfit) {
|
||||
maxProfit = profit;
|
||||
buyIdx = minIdx;
|
||||
sellIdx = i;
|
||||
|
||||
arrCls[sellIdx] = 'best';
|
||||
arrTags[sellIdx] = '利润' + profit;
|
||||
if (buyIdx >= 0) { arrCls[buyIdx] = 'min-price'; arrTags[buyIdx] = '买入'; }
|
||||
|
||||
steps.push({
|
||||
desc: `i=${i}: prices[${i}] = ${price},利润 ${price} - ${minPrice} = ${profit} > ${maxProfit - profit === 0 && profit > 0 ? 0 : maxProfit - profit} → 更新最大利润`,
|
||||
hint: `当前利润 ${profit} 超过之前最大利润,更新 max_profit = ${profit}。`,
|
||||
detail: `prices[${i}] = <b>${price}</b><br>利润 = ${price} - ${minPrice} = <b>${profit}</b><br>max_profit 更新为 <b>${maxProfit}</b><br>买入: prices[${buyIdx}] = ${prices[buyIdx]},卖出: prices[${sellIdx}] = ${prices[sellIdx]}`,
|
||||
hlLine: 6,
|
||||
arrCls: [...arrCls], arrTags: [...arrTags],
|
||||
minIdx, curIdx: i, maxProfit, bestBuy: buyIdx, bestSell: sellIdx,
|
||||
});
|
||||
} else {
|
||||
steps.push({
|
||||
desc: `i=${i}: prices[${i}] = ${price},利润 ${profit} ≤ ${maxProfit} → 不更新`,
|
||||
hint: `当前利润 ${profit} 未超过 max_profit(${maxProfit}),保持不变。`,
|
||||
detail: `prices[${i}] = <b>${price}</b><br>利润 = ${price} - ${minPrice} = ${profit}<br>max_profit 不变 = ${maxProfit}`,
|
||||
hlLine: 6,
|
||||
arrCls: [...arrCls], arrTags: [...arrTags],
|
||||
minIdx, curIdx: i, maxProfit, bestBuy: buyIdx, bestSell: sellIdx,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// final
|
||||
const finalCls = prices.map(() => '');
|
||||
const finalTags = prices.map(() => '');
|
||||
if (bestBuy >= 0) { finalCls[bestBuy] = 'min-price'; finalTags[bestBuy] = '买入'; }
|
||||
if (bestSell >= 0) { finalCls[bestSell] = 'best'; finalTags[bestSell] = '卖出'; }
|
||||
steps.push({
|
||||
desc: `遍历结束,最大利润 = ${maxProfit}`,
|
||||
hint: maxProfit > 0 ? `在 prices[${buyIdx}]=${prices[buyIdx]} 买入,prices[${sellIdx}]=${prices[sellIdx]} 卖出,利润 = ${maxProfit}` : '价格持续下降,无法获得正利润,返回 0',
|
||||
detail: `<b>最终结果</b>:max_profit = <b>${maxProfit}</b>${maxProfit > 0 ? `<br>买入点: i=${buyIdx}, price=${prices[buyIdx]}<br>卖出点: i=${sellIdx}, price=${prices[sellIdx]}` : ''}`,
|
||||
hlLine: -1,
|
||||
arrCls: finalCls, arrTags: finalTags,
|
||||
minIdx, curIdx: -1, maxProfit, bestBuy: buyIdx, bestSell: sellIdx,
|
||||
isFinal: true,
|
||||
});
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
function renderStep(step) {
|
||||
if (!step) {
|
||||
$('vizArea').innerHTML = '<div style="color:var(--text2);text-align:center;padding:40px;">点击「生成图解」开始</div>';
|
||||
$('detailContent').innerHTML = '';
|
||||
$('resultContent').innerHTML = '';
|
||||
$('stepInfo').textContent = '';
|
||||
$('hintText').textContent = '';
|
||||
return;
|
||||
}
|
||||
// pipeline info
|
||||
$('stepInfo').textContent = step.desc;
|
||||
$('hintText').textContent = step.hint;
|
||||
$('detailContent').innerHTML = step.detail;
|
||||
|
||||
// render array
|
||||
const prices = parseInput($('inputArea').value);
|
||||
const items = prices.map((p, i) => ({ val: p, cls: step.arrCls[i] || '', tag: step.arrTags[i] || '' }));
|
||||
renderArray($('vizArea'), items, { barMaxH: 140, barMinW: 36 });
|
||||
|
||||
// add legend
|
||||
const legend = document.createElement('div');
|
||||
legend.className = 'legend';
|
||||
legend.innerHTML = `
|
||||
<span class="legend-item"><span class="legend-dot" style="background:var(--green)"></span> 最低价/买入</span>
|
||||
<span class="legend-item"><span class="legend-dot" style="background:var(--orange)"></span> 当前遍历</span>
|
||||
<span class="legend-item"><span class="legend-dot" style="background:var(--accent)"></span> 卖出/最大利润</span>
|
||||
`;
|
||||
$('vizArea').appendChild(legend);
|
||||
|
||||
// stats
|
||||
const stats = document.createElement('div');
|
||||
stats.style.cssText = 'text-align:center;margin-top:12px;font-family:var(--mono);font-size:.85rem;';
|
||||
stats.innerHTML = `min_price = <span style="color:var(--green)">${step.minIdx >= 0 ? prices[step.minIdx] : '∞'}</span> max_profit = <span style="color:var(--orange)">${step.maxProfit}</span>`;
|
||||
$('vizArea').appendChild(stats);
|
||||
|
||||
// code highlight
|
||||
renderCode($('codeArea'), CODE, step.hlLine);
|
||||
|
||||
// result
|
||||
if (step.isFinal) {
|
||||
$('resultContent').innerHTML = `<div class="result-box">最大利润 = <span class="val">${step.maxProfit}</span>${step.bestBuy >= 0 ? `<br><small>买入 prices[${step.bestBuy}]=${prices[step.bestBuy]},卖出 prices[${step.bestSell}]=${prices[step.bestSell]}</small>` : '<br><small>无正利润交易</small>'}</div>`;
|
||||
} else {
|
||||
$('resultContent').innerHTML = `<div style="color:var(--text2);font-size:.85rem;">等待遍历完成…</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function parseInput(s) {
|
||||
try {
|
||||
const a = JSON.parse(s.replace(/'/g, '"'));
|
||||
if (Array.isArray(a) && a.every(x => typeof x === 'number')) return a;
|
||||
} catch(e) {}
|
||||
return [7,1,5,3,6,4];
|
||||
}
|
||||
|
||||
function build() {
|
||||
const prices = parseInput($('inputArea').value);
|
||||
const steps = genSteps(prices);
|
||||
if (controller) controller.stopAuto();
|
||||
controller = new StepController(steps, { onRender: renderStep });
|
||||
$('nextBtn').onclick = () => controller.next();
|
||||
$('prevBtn').onclick = () => controller.prev();
|
||||
$('jumpBtn').onclick = () => controller.jumpEnd();
|
||||
$('resetBtn').onclick = () => controller.reset();
|
||||
$('autoBtn').onclick = () => { if (controller.autoTimer) controller.stopAuto(); else controller.startAuto(); };
|
||||
controller.next();
|
||||
}
|
||||
|
||||
function init() {
|
||||
const sel = $('exampleSelect');
|
||||
EXAMPLES.forEach((ex, i) => {
|
||||
const o = document.createElement('option');
|
||||
o.value = i; o.textContent = ex.name;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
sel.onchange = () => { $('inputArea').value = EXAMPLES[sel.value].input; build(); };
|
||||
$('applyBtn').onclick = build;
|
||||
$('inputArea').onkeydown = e => { if (e.key === 'Enter') build(); };
|
||||
build();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
})()</script>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user