This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>093. 最长回文子串 – 图解</title>
|
||||
<link rel="stylesheet" href="../shared/style.css">
|
||||
<style>.vis-area { min-height: 120px; padding: 16px 0; } .code-section { margin-top: 16px; }
|
||||
.expansion-line{display:flex;margin-top:4px;justify-content:center;font-family:var(--mono);font-size:.75rem;color:var(--text2);}
|
||||
.bracket{padding:0 2px;}
|
||||
.arrow-l,.arrow-r{color:var(--orange);font-weight:700;}
|
||||
</style>
|
||||
</head>
|
||||
<body><div class="container">
|
||||
<h1>🟡 093. 最长回文子串 <span class="badge medium">中等</span></h1>
|
||||
<p class="subtitle">分类:字符串 | LeetCode Hot 100</p>
|
||||
<div class="controls" id="controls">
|
||||
<label>输入:</label><input type="text" id="inputArea" value="babad">
|
||||
<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 longestPalindrome(s):
|
||||
def expand(l, r):
|
||||
while l >= 0 and r < len(s) and s[l] == s[r]:
|
||||
l -= 1
|
||||
r += 1
|
||||
return l + 1, r - 1 # 回文起止
|
||||
|
||||
best_l, best_r = 0, 0
|
||||
for i in range(len(s)):
|
||||
# 奇数长度
|
||||
l1, r1 = expand(i, i)
|
||||
if r1 - l1 > best_r - best_l:
|
||||
best_l, best_r = l1, r1
|
||||
# 偶数长度
|
||||
l2, r2 = expand(i, i + 1)
|
||||
if r2 - l2 > best_r - best_l:
|
||||
best_l, best_r = l2, r2
|
||||
return s[best_l:best_r+1]`;
|
||||
|
||||
const EXAMPLES = [
|
||||
{ name: '例1: babad', input: 'babad' },
|
||||
{ name: '例2: cbbd', input: 'cbbd' },
|
||||
{ name: '例3: a', input: 'a' },
|
||||
{ name: '例4: racecar', input: 'racecar' },
|
||||
{ name: '例5: abacdfgdcaba', input: 'abacdfgdcaba' },
|
||||
];
|
||||
|
||||
let controller = null;
|
||||
|
||||
function expandAroundCenter(s, l, r) {
|
||||
const steps = [];
|
||||
while (l >= 0 && r < s.length && s[l] === s[r]) {
|
||||
steps.push({ l, r, matched: true });
|
||||
l--; r++;
|
||||
}
|
||||
steps.push({ l: l + 1, r: r - 1, matched: false, isFinal: true });
|
||||
return steps;
|
||||
}
|
||||
|
||||
function genSteps(s) {
|
||||
const steps = [];
|
||||
const n = s.length;
|
||||
let bestL = 0, bestR = 0;
|
||||
|
||||
steps.push({
|
||||
desc: '初始化:对每个位置做中心扩展',
|
||||
hint: '枚举每个中心位置,向两侧扩展,找到最长回文。',
|
||||
detail: '<b>思路</b>:中心扩展法<br>• 对每个下标 i,分别以 (i,i) 和 (i,i+1) 为中心<br>• 向两侧扩展直到不等<br>• 记录最长回文的起止位置<br><br>时间 O(n²),空间 O(1)',
|
||||
hlLine: -1, centerIdx: -1, centerType: '',
|
||||
expL: -1, expR: -1, bestL: 0, bestR: 0,
|
||||
cellCls: s.split('').map(() => ''),
|
||||
});
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
// odd length
|
||||
const oddSteps = expandAroundCenter(s, i, i);
|
||||
for (const es of oddSteps) {
|
||||
const cellCls = s.split('').map(() => '');
|
||||
// mark palindrome range
|
||||
if (es.l >= 0 && es.r < n) {
|
||||
for (let j = es.l; j <= es.r; j++) cellCls[j] = 'palindrome';
|
||||
}
|
||||
// mark center
|
||||
cellCls[i] = 'center';
|
||||
// mark expansion pointers
|
||||
if (es.l !== i || es.r !== i) {
|
||||
if (es.l >= 0) cellCls[es.l] = es.matched ? 'expand' : 'palindrome';
|
||||
if (es.r < n) cellCls[es.r] = es.matched ? 'expand' : 'palindrome';
|
||||
}
|
||||
// mark best
|
||||
for (let j = bestL; j <= bestR; j++) cellCls[j] = 'best';
|
||||
// but center takes priority visual
|
||||
|
||||
if (es.isFinal) {
|
||||
const newBest = es.r - es.l > bestR - bestL;
|
||||
if (newBest) { bestL = es.l; bestR = es.r; }
|
||||
steps.push({
|
||||
desc: `中心 i=${i}(奇数):扩展到 [${es.l}, ${es.r}],回文 "${s.substring(es.l, es.r + 1)}"${newBest ? ' → 更新最优!' : ''}`,
|
||||
hint: newBest ? `找到更长回文 "${s.substring(es.l, es.r + 1)}"(长度 ${es.r - es.l + 1})` : `回文长度 ${es.r - es.l + 1},未超过当前最优 ${bestR - bestL + 1}`,
|
||||
detail: `中心 i=<b>${i}</b>(奇数长度)<br>扩展结果:[${es.l}, ${es.r}] = "${s.substring(es.l, es.r + 1)}"<br>长度 = ${es.r - es.l + 1}${newBest ? '<br><span style="color:var(--green)">更新最优!</span>' : '<br>未超过当前最优 ' + (bestR - bestL + 1)}`,
|
||||
hlLine: 10,
|
||||
centerIdx: i, centerType: 'odd',
|
||||
expL: es.l, expR: es.r, bestL, bestR,
|
||||
cellCls,
|
||||
});
|
||||
} else {
|
||||
steps.push({
|
||||
desc: `中心 i=${i}(奇数):s[${es.l}]='${s[es.l]}' == s[${es.r}]='${s[es.r]}',扩展到 [${es.l}, ${es.r}]`,
|
||||
hint: `字符匹配,继续向外扩展。`,
|
||||
detail: `中心 i=<b>${i}</b>(奇数)<br>比较 s[${es.l}]='${s[es.l]}' ↔ s[${es.r}]='${s[es.r]}'<br>✅ 匹配!范围 [${es.l}, ${es.r}]`,
|
||||
hlLine: 3,
|
||||
centerIdx: i, centerType: 'odd',
|
||||
expL: es.l, expR: es.r, bestL, bestR,
|
||||
cellCls,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// even length
|
||||
if (i + 1 < n) {
|
||||
const evenSteps = expandAroundCenter(s, i, i + 1);
|
||||
for (const es of evenSteps) {
|
||||
const cellCls = s.split('').map(() => '');
|
||||
if (es.l >= 0 && es.r < n) {
|
||||
for (let j = es.l; j <= es.r; j++) cellCls[j] = 'palindrome';
|
||||
}
|
||||
cellCls[i] = 'center';
|
||||
if (i + 1 < n) cellCls[i + 1] = 'center';
|
||||
if (es.l !== i || es.r !== i + 1) {
|
||||
if (es.l >= 0 && es.l !== i) cellCls[es.l] = es.matched ? 'expand' : 'palindrome';
|
||||
if (es.r < n && es.r !== i + 1) cellCls[es.r] = es.matched ? 'expand' : 'palindrome';
|
||||
}
|
||||
for (let j = bestL; j <= bestR; j++) cellCls[j] = 'best';
|
||||
|
||||
if (es.isFinal) {
|
||||
const newBest = es.r - es.l > bestR - bestL;
|
||||
if (newBest) { bestL = es.l; bestR = es.r; }
|
||||
steps.push({
|
||||
desc: `中心 i=${i},${i+1}(偶数):扩展到 [${es.l}, ${es.r}],回文 "${s.substring(es.l, es.r + 1)}"${newBest ? ' → 更新最优!' : ''}`,
|
||||
hint: newBest ? `找到更长回文 "${s.substring(es.l, es.r + 1)}"(长度 ${es.r - es.l + 1})` : `回文长度 ${es.r - es.l + 1},未超过当前最优 ${bestR - bestL + 1}`,
|
||||
detail: `中心 i=<b>${i},${i+1}</b>(偶数长度)<br>扩展结果:[${es.l}, ${es.r}] = "${s.substring(es.l, es.r + 1)}"<br>长度 = ${es.r - es.l + 1}${newBest ? '<br><span style="color:var(--green)">更新最优!</span>' : '<br>未超过当前最优 ' + (bestR - bestL + 1)}`,
|
||||
hlLine: 12,
|
||||
centerIdx: i, centerType: 'even',
|
||||
expL: es.l, expR: es.r, bestL, bestR,
|
||||
cellCls,
|
||||
});
|
||||
} else {
|
||||
steps.push({
|
||||
desc: `中心 i=${i},${i+1}(偶数):s[${es.l}]='${s[es.l]}' == s[${es.r}]='${s[es.r]}',扩展到 [${es.l}, ${es.r}]`,
|
||||
hint: `字符匹配,继续向外扩展。`,
|
||||
detail: `中心 i=<b>${i},${i+1}</b>(偶数)<br>比较 s[${es.l}]='${s[es.l]}' ↔ s[${es.r}]='${s[es.r]}'<br>✅ 匹配!范围 [${es.l}, ${es.r}]`,
|
||||
hlLine: 3,
|
||||
centerIdx: i, centerType: 'even',
|
||||
expL: es.l, expR: es.r, bestL, bestR,
|
||||
cellCls,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// final
|
||||
const finalCls = s.split('').map(() => '');
|
||||
for (let j = bestL; j <= bestR; j++) finalCls[j] = 'best';
|
||||
steps.push({
|
||||
desc: `遍历结束,最长回文子串 = "${s.substring(bestL, bestR + 1)}"`,
|
||||
hint: '中心扩展法穷举了所有可能的回文中心。',
|
||||
detail: `<b>结果</b>:最长回文子串 = <b>"${s.substring(bestL, bestR + 1)}"</b><br>位置:[${bestL}, ${bestR}]<br>长度:${bestR - bestL + 1}`,
|
||||
hlLine: -1, centerIdx: -1, centerType: '',
|
||||
expL: bestL, expR: bestR, bestL, bestR,
|
||||
cellCls: finalCls, result: s.substring(bestL, bestR + 1), 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;
|
||||
}
|
||||
|
||||
$('stepInfo').textContent = step.desc;
|
||||
$('hintText').textContent = step.hint;
|
||||
$('detailContent').innerHTML = step.detail;
|
||||
|
||||
const s = $('inputArea').value.trim();
|
||||
|
||||
// string cells
|
||||
const row = document.createElement('div');
|
||||
row.className = 'str-row';
|
||||
s.split('').forEach((c, i) => {
|
||||
const cell = document.createElement('div');
|
||||
const cls = step.cellCls[i] || '';
|
||||
cell.className = 'str-cell ' + cls;
|
||||
cell.innerHTML = `${c}<span class="str-idx">${i}</span>`;
|
||||
row.appendChild(cell);
|
||||
});
|
||||
$('vizArea').innerHTML = '';
|
||||
$('vizArea').appendChild(row);
|
||||
|
||||
// expansion arrows
|
||||
if (step.expL >= 0 && step.expR >= 0 && step.centerIdx >= 0) {
|
||||
const arrow = document.createElement('div');
|
||||
arrow.className = 'expansion-line';
|
||||
const ptrs = s.split('').map((_, i) => {
|
||||
if (i === step.expL && i === step.expR) return '<span style="color:var(--orange)">▲</span>';
|
||||
if (i === step.expL) return '<span class="arrow-l">◀</span>';
|
||||
if (i === step.expR) return '<span class="arrow-r">▶</span>';
|
||||
return ' ';
|
||||
});
|
||||
arrow.innerHTML = ptrs.join(' ');
|
||||
$('vizArea').appendChild(arrow);
|
||||
}
|
||||
|
||||
// stats
|
||||
const stats = document.createElement('div');
|
||||
stats.style.cssText = 'text-align:center;margin-top:8px;font-family:var(--mono);font-size:.82rem;';
|
||||
const best = step.bestR - step.bestL + 1;
|
||||
stats.innerHTML = `当前最优 = <span style="color:var(--green)">"${s.substring(step.bestL, step.bestR + 1)}"</span>(长度 ${best})`;
|
||||
$('vizArea').appendChild(stats);
|
||||
|
||||
const legend = document.createElement('div');
|
||||
legend.className = 'legend';
|
||||
legend.innerHTML = `
|
||||
<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(--purple)"></span> 扩展边界</span>
|
||||
<span class="legend-item"><span class="legend-dot" style="background:var(--green)"></span> 最长回文</span>
|
||||
`;
|
||||
$('vizArea').appendChild(legend);
|
||||
|
||||
renderCode($('codeArea'), CODE, step.hlLine);
|
||||
|
||||
if (step.isFinal) {
|
||||
$('resultContent').innerHTML = `<div class="result-box">最长回文子串 = <span class="val">"${step.result}"</span><br><small>位置 [${step.bestL}, ${step.bestR}],长度 ${step.bestR - step.bestL + 1}</small></div>`;
|
||||
} else {
|
||||
$('resultContent').innerHTML = `<div style="color:var(--text2);font-size:.85rem;">等待遍历完成…</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function build() {
|
||||
const s = $('inputArea').value.trim();
|
||||
const steps = genSteps(s);
|
||||
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();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
})()</script>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user