Files
illustrated-algorithm/longest-substring-without-repeating-chars/index.html
T
2026-08-24 04:35:13 +00:00

264 lines
10 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>008. 无重复字符的最长子串 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 120px; padding: 16px 0; }
.code-section { margin-top: 16px; }
.freq-table { margin-top: 12px; }
.freq-table td, .freq-table th { font-size: 13px; padding: 4px 8px; }
.char-in-set { background: #dcfce7 !important; color: #166534; font-weight: 700; }
.window-info { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px; }
.window-stat { padding: 6px 14px; background: #f0f9ff; border-radius: 8px; font-size: 14px; }
.window-stat b { color: var(--blue-dark); }
.best-stat { padding: 6px 14px; background: #f0fdf4; border-radius: 8px; font-size: 14px; }
.best-stat b { color: var(--green-dark); }
</style>
</head>
<body>
<div class="container">
<h1>🟡 008. 无重复字符的最长子串 <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:滑动窗口 | LeetCode Hot 100</p>
<div class="controls" id="controls">
<label for="inputStr">字符串:</label>
<input type="text" id="inputStr" value="abcabcbb" placeholder="如 abcabcbb">
<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: 'abcabcbb', label: '示例1: abcabcbb → 3'},
{input: 'bbbbb', label: '示例2: bbbbb → 1'},
{input: 'pwwkew', label: '示例3: pwwkew → 3'},
{input: 'abcdefgh', label: '示例4: abcdefgh → 8 (无重复)'},
];
let steps, stepCtrl, inputStr;
function buildSteps(s) {
inputStr = s;
steps = [];
const n = s.length;
if (n === 0) {
steps.push({stage:'done', left:0, right:-1, charSet:{}, maxLen:0, maxStart:0, msg:'字符串为空,返回 0'});
return;
}
const charSet = {};
let left = 0, maxLen = 0, maxStart = 0;
steps.push({stage:'init', left:0, right:-1, charSet:{}, maxLen:0, maxStart:0, msg:'初始化:left = 0, 空字符集, maxLen = 0'});
for (let right = 0; right < n; right++) {
const ch = s[right];
if (charSet[ch] !== undefined) {
steps.push({
stage:'repeat', left, right, charSet: {...charSet},
maxLen, maxStart, dupChar:ch, msg:`右指针 → ${right},s[${right}] = '${ch}',字符集中已存在 '${ch}',需收缩左边界`
});
while (charSet[ch] !== undefined) {
const rem = s[left];
delete charSet[rem];
const still = charSet[ch] !== undefined;
steps.push({
stage:'shrink', left:left+1, right, charSet:{...charSet},
maxLen, maxStart, removed:rem, msg:`移除 s[${left}] = '${rem}',left → ${left+1}${still ? ',仍有重复,继续收缩' : ',重复已消除'}`
});
left++;
}
charSet[ch] = right;
steps.push({
stage:'add', left, right, charSet:{...charSet},
maxLen, maxStart, added:ch, msg:`将 '${ch}' 加入字符集,窗口 [${left}, ${right}]`
});
} else {
charSet[ch] = right;
steps.push({
stage:'expand', left, right, charSet:{...charSet},
maxLen, maxStart, added:ch, msg:`右指针 → ${right},s[${right}] = '${ch}',不在字符集中,加入,窗口 [${left}, ${right}]`
});
}
const curLen = right - left + 1;
if (curLen > maxLen) {
maxLen = curLen;
maxStart = left;
steps.push({
stage:'update', left, right, charSet:{...charSet},
maxLen, maxStart, msg:`窗口长度 ${curLen} > maxLen,更新 maxLen = ${maxLen},起始于 ${maxStart}`
});
}
}
steps.push({
stage:'done', left, right:n-1, charSet:{...charSet},
maxLen, maxStart, msg:`遍历完毕,最长不含重复字符的子串长度 = ${maxLen}`
});
}
function render(stepIdx) {
const s = steps[stepIdx];
const arr = inputStr.split('');
const hl = {};
// highlight current window
if (s.right >= 0) {
for (let i = s.left; i <= Math.min(s.right, arr.length-1); i++) {
if (s.stage === 'update' || s.stage === 'done') {
hl[i] = (i >= s.maxStart && i < s.maxStart + s.maxLen) ? 'selected' : 'blue';
} else {
hl[i] = 'blue';
}
}
}
// mark duplicate / newly added
if (s.dupChar && s.right >= 0) {
for (let i = s.left; i <= s.right; i++) {
if (arr[i] === s.dupChar && i < s.right) hl[i] = 'red';
}
if (s.right < arr.length) hl[s.right] = 'red';
} else if (s.stage === 'expand' && s.right >= 0) {
hl[s.right] = 'green';
} else if (s.stage === 'add' && s.right >= 0) {
hl[s.right] = 'green';
}
const pointers = {};
if (s.left >= 0 && s.left < arr.length) pointers['L'] = s.left;
if (s.right >= 0 && s.right < arr.length) pointers['R'] = s.right;
let viz = '<div style="margin-bottom:6px;font-size:13px;color:#475569;">原字符串:</div>';
viz += renderArray(arr, {highlights:hl, pointers, width:42});
const curLen = s.right >= s.left ? s.right - s.left + 1 : 0;
viz += '<div class="window-info">';
viz += `<div class="window-stat">窗口: [<b>${s.left}</b>, <b>${s.right >= 0 ? s.right : '?'}</b>] 长度 = <b>${curLen}</b></div>`;
viz += `<div class="best-stat">maxLen = <b>${s.maxLen}</b>${s.maxLen > 0 ? `,起始 ${s.maxStart}` : ''}</div>`;
viz += '</div>';
// Character set table
const uniqueChars = [...new Set(arr)].sort();
if (uniqueChars.length > 0) {
viz += '<div class="freq-table"><table><tr><th>字符</th>';
uniqueChars.forEach(c => viz += `<th>'${c}'</th>`);
viz += '</tr><tr><td style="font-weight:600;">在窗口中</td>';
uniqueChars.forEach(c => {
const inSet = s.charSet[c] !== undefined;
viz += `<td class="${inSet ? 'char-in-set' : ''}">${inSet ? '✓' : '—'}</td>`;
});
viz += '</tr></table></div>';
}
$('vizArea').innerHTML = viz;
let detail = '<div class="calc-block">' + s.msg + '</div>';
const keys = Object.keys(s.charSet);
if (keys.length > 0) {
detail += '<div style="margin-top:6px;font-size:14px;">字符集: <code>{' + keys.sort().map(c => `'${c}'`).join(', ') + '}</code></div>';
} else {
detail += '<div style="margin-top:6px;font-size:14px;">字符集: <code>{空}</code></div>';
}
$('detailContent').innerHTML = detail;
if (s.stage === 'done') {
const bestStr = inputStr.substring(s.maxStart, s.maxStart + s.maxLen);
$('resultContent').innerHTML = `<div class="final-answer">最长不含重复字符的子串长度 = <b>${s.maxLen}</b><br>子串: <code>"${bestStr}"</code>(位置 ${s.maxStart} ~ ${s.maxStart + s.maxLen - 1})</div>`;
}
$('hintText').textContent = s.msg;
const pipeStages = [
{k:'init',l:'初始化'},{k:'expand',l:'右扩'},{k:'repeat',l:'发现重复'},
{k:'shrink',l:'左缩'},{k:'add',l:'加入'},{k:'update',l:'更新最大'},{k:'done',l:'完成'}
];
$('pipeline').innerHTML = pipeStages.map(p =>
`<span class="pipe-step ${s.stage===p.k?'active':''}">${p.l}</span>`
).join('<i>→</i>');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
buildSteps(examples[0].input);
stepCtrl = new StepController({onStep: render});
stepCtrl.setSteps(steps.map((_,i)=>i));
stepCtrl.onStep = (idx) => { render(idx); $('stepInfo').textContent = `步骤 ${idx+1} / ${steps.length}`; };
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
$('applyBtn').onclick = () => {
const v = $('inputStr').value.trim();
if (!v) { alert('请输入字符串'); return; }
buildSteps(v); stepCtrl.setSteps(steps.map((_,i)=>i)); render(0);
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputStr').value = e.input;
buildSteps(e.input); stepCtrl.setSteps(steps.map((_,i)=>i)); render(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 lengthOfLongestSubstring(s: str) -> int:
char_set = set()
left = 0
max_len = 0
for right in range(len(s)):
while s[right] in char_set:
char_set.remove(s[left])
left += 1
char_set.add(s[right])
max_len = max(max_len, right - left + 1)
return max_len`, {lang:'Python'});
})();
</script>
</body>
</html>