Files
illustrated-algorithm/best-time-to-buy-and-sell-stock/index.html
T

231 lines
9.9 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>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>