527 lines
20 KiB
HTML
527 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>LeetCode 141. Linked List Cycle | 算法可视化</title>
|
||
<link rel="stylesheet" href="../shared/style.css">
|
||
<style>
|
||
:root {
|
||
--node-bg: #1e293b;
|
||
--node-border: #475569;
|
||
--node-text: #e2e8f0;
|
||
--cycle-bg: #4c1d95;
|
||
--cycle-border: #a78bfa;
|
||
--cycle-text: #ede9fe;
|
||
--slow-color: #22c55e;
|
||
--fast-color: #ef4444;
|
||
--meet-color: #facc15;
|
||
--arrow-color: #64748b;
|
||
--cycle-arrow-color: #a78bfa;
|
||
--null-color: #94a3b8;
|
||
}
|
||
.viz-area {
|
||
display: flex; flex-direction: column; align-items: center;
|
||
gap: 18px; min-height: 260px; padding: 24px 16px 16px; overflow-x: auto;
|
||
}
|
||
.ll-container {
|
||
position: relative; display: inline-flex;
|
||
flex-direction: column; align-items: center;
|
||
}
|
||
.ll-row { display: flex; align-items: center; gap: 0; }
|
||
.ll-node-wrapper {
|
||
display: flex; flex-direction: column; align-items: center; position: relative;
|
||
}
|
||
.ll-node {
|
||
width: 56px; height: 56px; border-radius: 12px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 16px; font-weight: 700;
|
||
border: 2.5px solid var(--node-border); background: var(--node-bg); color: var(--node-text);
|
||
position: relative; z-index: 2; transition: all 0.35s ease;
|
||
}
|
||
.ll-node.in-cycle {
|
||
background: var(--cycle-bg); border-color: var(--cycle-border); color: var(--cycle-text);
|
||
}
|
||
.ll-node.slow-here {
|
||
box-shadow: 0 0 0 3px var(--slow-color), 0 0 16px rgba(34,197,94,0.4);
|
||
}
|
||
.ll-node.fast-here {
|
||
box-shadow: 0 0 0 3px var(--fast-color), 0 0 16px rgba(239,68,68,0.4);
|
||
}
|
||
.ll-node.both-here {
|
||
box-shadow: 0 0 0 3px var(--meet-color), 0 0 16px rgba(250,204,21,0.5);
|
||
border-color: var(--meet-color);
|
||
}
|
||
.ll-node.null-node {
|
||
width: 44px; height: 44px; border-radius: 8px;
|
||
font-size: 13px; color: var(--null-color);
|
||
border: 2px dashed var(--null-color); background: transparent;
|
||
}
|
||
.ll-arrow { display: flex; align-items: center; margin: 0 -2px; z-index: 1; }
|
||
.ll-arrow svg { display: block; }
|
||
.ptr-label-area {
|
||
height: 52px; display: flex; flex-direction: column;
|
||
align-items: center; justify-content: flex-start;
|
||
gap: 2px; margin-top: 4px;
|
||
}
|
||
.ptr-tag {
|
||
font-size: 12px; font-weight: 700; padding: 2px 8px;
|
||
border-radius: 6px; letter-spacing: 0.5px; white-space: nowrap;
|
||
animation: ptrFadeIn 0.3s ease;
|
||
}
|
||
@keyframes ptrFadeIn {
|
||
from { opacity: 0; transform: translateY(6px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
.ptr-tag.slow { background: rgba(34,197,94,0.18); color: var(--slow-color); }
|
||
.ptr-tag.fast { background: rgba(239,68,68,0.18); color: var(--fast-color); }
|
||
.ptr-tag.meet { background: rgba(250,204,21,0.22); color: var(--meet-color); }
|
||
.cycle-arc-svg {
|
||
position: absolute; left: 0; top: 0; width: 100%; height: 100%;
|
||
pointer-events: none; z-index: 3;
|
||
}
|
||
.cycle-arc-path {
|
||
fill: none; stroke: var(--cycle-arrow-color); stroke-width: 2.5;
|
||
stroke-dasharray: 7 4; marker-end: url(#cycleArrowHead); opacity: 0.85;
|
||
}
|
||
.node-index-label { font-size: 10px; color: #64748b; margin-bottom: 2px; }
|
||
.result-badge {
|
||
display: inline-block; padding: 6px 18px; border-radius: 999px;
|
||
font-weight: 700; font-size: 15px; animation: resultPop 0.4s ease;
|
||
}
|
||
.result-badge.true {
|
||
background: rgba(34,197,94,0.15); color: #22c55e; border: 1.5px solid rgba(34,197,94,0.4);
|
||
}
|
||
.result-badge.false {
|
||
background: rgba(239,68,68,0.12); color: #ef4444; border: 1.5px solid rgba(239,68,68,0.35);
|
||
}
|
||
@keyframes resultPop {
|
||
0% { transform: scale(0.7); opacity: 0; }
|
||
60% { transform: scale(1.08); }
|
||
100% { transform: scale(1); opacity: 1; }
|
||
}
|
||
.detail-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.detail-table th, .detail-table td { padding: 6px 10px; text-align: left; border-bottom: 1px solid #1e293b; }
|
||
.detail-table th { color: #94a3b8; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.detail-table td { color: #e2e8f0; }
|
||
.slow-text { color: var(--slow-color); font-weight: 600; }
|
||
.fast-text { color: var(--fast-color); font-weight: 600; }
|
||
.meet-text { color: var(--meet-color); font-weight: 600; }
|
||
.code-line-highlight { background: rgba(167,139,250,0.12) !important; border-left: 3px solid #a78bfa !important; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
|
||
<header class="problem-header">
|
||
<span class="problem-badge easy">Easy #025</span>
|
||
<h1>141. Linked List Cycle</h1>
|
||
<p class="problem-desc">给定链表头节点 <code>head</code>,判断链表中是否有环。若有环返回 <code>true</code>,否则返回 <code>false</code>。</p>
|
||
</header>
|
||
|
||
<section class="input-section">
|
||
<label for="inputBox">输入:</label>
|
||
<input type="text" id="inputBox" value="head=[3,2,0,-4], pos=1" placeholder="head=[3,2,0,-4], pos=1" spellcheck="false">
|
||
<button id="btnLoad" class="btn-primary">加载</button>
|
||
<select id="exampleSelect" title="选择示例">
|
||
<option value="0">示例1: [3,2,0,-4], pos=1</option>
|
||
<option value="1">示例2: [1,2], pos=0</option>
|
||
<option value="2">示例3: [1], pos=-1</option>
|
||
</select>
|
||
</section>
|
||
|
||
<div class="pipeline" id="pipeline">
|
||
<div class="stage" data-stage="0">初始化</div>
|
||
<div class="connector"></div>
|
||
<div class="stage" data-stage="1">慢指针步进</div>
|
||
<div class="connector"></div>
|
||
<div class="stage" data-stage="2">快指针步进</div>
|
||
<div class="connector"></div>
|
||
<div class="stage" data-stage="3">判定</div>
|
||
</div>
|
||
|
||
<div class="hint-box" id="hintBox">点击「加载」或切换示例开始可视化</div>
|
||
|
||
<div class="viz-area" id="vizArea"></div>
|
||
|
||
<div class="controls" id="controls">
|
||
<button id="btnPrev" class="btn-step" disabled>◀ 上一步</button>
|
||
<button id="btnPlay" class="btn-play" disabled>▶ 自动播放</button>
|
||
<button id="btnNext" class="btn-step" disabled>下一步 ▶</button>
|
||
<span class="step-counter" id="stepCounter">0 / 0</span>
|
||
</div>
|
||
|
||
<div class="detail-panel" id="detailPanel">
|
||
<h3>📋 步骤详情</h3>
|
||
<div id="detailContent">—</div>
|
||
</div>
|
||
|
||
<div class="result-panel" id="resultPanel" style="display:none;">
|
||
<h3>🎯 结果</h3>
|
||
<div id="resultContent"></div>
|
||
</div>
|
||
|
||
<div class="code-panel">
|
||
<h3>🐍 Python 代码</h3>
|
||
<pre id="codeBlock"><code>class Solution:
|
||
def hasCycle(self, head):
|
||
if not head or not head.next:
|
||
return False
|
||
slow = fast = head
|
||
while fast and fast.next:
|
||
slow = slow.next
|
||
fast = fast.next.next
|
||
if slow == fast:
|
||
return True
|
||
return False</code></pre>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script src="../shared/algo-viz.js"></script>
|
||
<script>
|
||
(function() {
|
||
'use strict';
|
||
|
||
var inputBox = document.getElementById('inputBox');
|
||
var btnLoad = document.getElementById('btnLoad');
|
||
var exampleSelect = document.getElementById('exampleSelect');
|
||
var pipeline = document.getElementById('pipeline');
|
||
var hintBox = document.getElementById('hintBox');
|
||
var vizArea = document.getElementById('vizArea');
|
||
var btnPrev = document.getElementById('btnPrev');
|
||
var btnPlay = document.getElementById('btnPlay');
|
||
var btnNext = document.getElementById('btnNext');
|
||
var stepCounter = document.getElementById('stepCounter');
|
||
var detailContent = document.getElementById('detailContent');
|
||
var resultPanel = document.getElementById('resultPanel');
|
||
var resultContent = document.getElementById('resultContent');
|
||
var codeBlock = document.getElementById('codeBlock');
|
||
|
||
var currentSteps = [];
|
||
var controller = null;
|
||
|
||
var EXAMPLES = [
|
||
{ vals: [3,2,0,-4], pos: 1 },
|
||
{ vals: [1,2], pos: 0 },
|
||
{ vals: [1], pos: -1 }
|
||
];
|
||
|
||
function parseInput(str) {
|
||
var vm = str.match(/head\s*=\s*\[([^\]]*)\]/i);
|
||
var pm = str.match(/pos\s*=\s*(-?\d+)/i);
|
||
if (!vm) return null;
|
||
var vals = vm[1].split(',').map(function(s){ return Number(s.trim()); }).filter(function(v){ return !isNaN(v); });
|
||
var pos = pm ? parseInt(pm[1]) : -1;
|
||
return { vals: vals, pos: pos };
|
||
}
|
||
|
||
function buildSteps(vals, pos) {
|
||
var n = vals.length;
|
||
var steps = [];
|
||
var slow = 0, fast = 0;
|
||
|
||
function nextIdx(i) {
|
||
if (i === n - 1) return pos;
|
||
return i + 1;
|
||
}
|
||
function isNull(i) { return i < 0 || i >= n; }
|
||
|
||
steps.push({
|
||
stage: 0, slow: slow, fast: fast, slowStep: -1, fastStep: -1,
|
||
hint: '初始化:slow 和 fast 指针都指向头节点',
|
||
detail: { slow: slow, fast: fast, slowVal: vals[slow], fastVal: vals[fast], action: '初始化' },
|
||
codeLines: [2,3], done: false, result: null
|
||
});
|
||
|
||
if (n === 0) {
|
||
steps.push({ stage: 3, slow: -1, fast: -1, slowStep: -1, fastStep: -1,
|
||
hint: '链表为空,直接返回 false', detail: { action: '空链表' }, codeLines: [2], done: true, result: false });
|
||
return steps;
|
||
}
|
||
if (n === 1 && pos === -1) {
|
||
steps.push({ stage: 3, slow: 0, fast: 0, slowStep: -1, fastStep: -1,
|
||
hint: '只有一个节点且无环,head.next 为 null,返回 false',
|
||
detail: { slow: 0, fast: 0, action: '单节点无环' }, codeLines: [2], done: true, result: false });
|
||
return steps;
|
||
}
|
||
|
||
var maxIter = n * n + 10;
|
||
var iter = 0;
|
||
while (iter++ < maxIter) {
|
||
var prevSlow = slow, prevFast = fast;
|
||
|
||
if (isNull(fast)) {
|
||
steps.push({ stage: 3, slow: slow, fast: fast, slowStep: -1, fastStep: -1,
|
||
hint: 'fast 已经为 null → 无环,返回 false',
|
||
detail: { slow: slow, fast: fast, action: 'fast 为 null' }, codeLines: [5,9], done: true, result: false });
|
||
break;
|
||
}
|
||
if (pos === -1 && fast === n - 1) {
|
||
steps.push({ stage: 3, slow: slow, fast: fast, slowStep: -1, fastStep: -1,
|
||
hint: 'fast 在末尾节点,fast.next 为 null → 无环,返回 false',
|
||
detail: { slow: slow, fast: fast, action: 'fast.next 为 null' }, codeLines: [5,9], done: true, result: false });
|
||
break;
|
||
}
|
||
|
||
var newSlow = nextIdx(slow);
|
||
if (isNull(newSlow)) newSlow = -1;
|
||
slow = newSlow;
|
||
|
||
var fast1 = nextIdx(fast);
|
||
if (isNull(fast1)) {
|
||
steps.push({ stage: 2, slow: prevSlow, fast: -1, slowStep: newSlow, fastStep: -1,
|
||
hint: 'fast 第一步就到达 null → 无环,返回 false',
|
||
detail: { slow: prevSlow, fast: prevFast, action: 'fast 到达 null' }, codeLines: [5,6,9], done: true, result: false });
|
||
break;
|
||
}
|
||
var fast2 = nextIdx(fast1);
|
||
if (isNull(fast2)) {
|
||
fast = fast1;
|
||
steps.push({ stage: 2, slow: slow, fast: fast, slowStep: newSlow, fastStep: fast1,
|
||
hint: 'slow → 节点' + vals[newSlow] + ',fast → 节点' + vals[fast1] + '(fast.next 为 null)',
|
||
detail: { slow: newSlow, fast: fast1, slowVal: vals[newSlow], fastVal: vals[fast1], action: '步进' },
|
||
codeLines: [5,6,7], done: false, result: null });
|
||
steps.push({ stage: 3, slow: slow, fast: fast, slowStep: -1, fastStep: -1,
|
||
hint: 'fast.next 为 null → 无环,返回 false',
|
||
detail: { slow: slow, fast: fast, action: 'fast.next 为 null' }, codeLines: [5,9], done: true, result: false });
|
||
break;
|
||
}
|
||
|
||
fast = fast2;
|
||
steps.push({
|
||
stage: 1, slow: slow, fast: fast,
|
||
slowStep: newSlow, fastStep: fast2,
|
||
hint: 'slow → ' + vals[newSlow] + ' (1步),fast → ' + vals[fast2] + ' (2步)',
|
||
detail: {
|
||
slow: slow, fast: fast,
|
||
slowFrom: prevSlow, fastFrom: prevFast,
|
||
slowVal: vals[newSlow], fastVal: vals[fast2],
|
||
action: '步进'
|
||
},
|
||
codeLines: [5,6,7], done: false, result: null
|
||
});
|
||
|
||
if (slow === fast) {
|
||
steps.push({
|
||
stage: 3, slow: slow, fast: fast, slowStep: -1, fastStep: -1,
|
||
hint: 'slow 和 fast 在节点 ' + vals[slow] + ' 相遇 → 检测到环,返回 true',
|
||
detail: { slow: slow, fast: fast, meetVal: vals[slow], action: '相遇' },
|
||
codeLines: [8,9], done: true, result: true
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (steps.length > 0 && !steps[steps.length - 1].done) {
|
||
steps.push({ stage: 3, slow: slow, fast: fast, slowStep: -1, fastStep: -1,
|
||
hint: '检测完成', detail: { action: '结束' }, codeLines: [9], done: true, result: false });
|
||
}
|
||
return steps;
|
||
}
|
||
|
||
function renderViz(vals, pos, step) {
|
||
var n = vals.length;
|
||
var slow = step.slow, fast = step.fast;
|
||
var html = '<div class="ll-container" id="llContainer">';
|
||
html += '<div class="ll-row">';
|
||
|
||
for (var i = 0; i < n; i++) {
|
||
var inCycle = pos !== -1 && i >= pos;
|
||
var isSlow = (i === slow);
|
||
var isFast = (i === fast);
|
||
var isMeet = isSlow && isFast;
|
||
var cls = 'll-node';
|
||
if (inCycle) cls += ' in-cycle';
|
||
if (isMeet) cls += ' both-here';
|
||
else {
|
||
if (isSlow) cls += ' slow-here';
|
||
if (isFast) cls += ' fast-here';
|
||
}
|
||
|
||
html += '<div class="ll-node-wrapper">';
|
||
html += '<div class="node-index-label">' + i + '</div>';
|
||
html += '<div class="' + cls + '">' + vals[i] + '</div>';
|
||
html += '<div class="ptr-label-area">';
|
||
if (isMeet && step.done) {
|
||
html += '<span class="ptr-tag meet">⚡ 相遇</span>';
|
||
} else if (isMeet) {
|
||
html += '<span class="ptr-tag slow">slow</span>';
|
||
html += '<span class="ptr-tag fast">fast</span>';
|
||
} else {
|
||
if (isSlow) html += '<span class="ptr-tag slow">slow</span>';
|
||
if (isFast) html += '<span class="ptr-tag fast">fast</span>';
|
||
}
|
||
html += '</div></div>';
|
||
|
||
if (i < n - 1) {
|
||
var nextInCycle = pos !== -1 && (i + 1) >= pos;
|
||
var arrowCol = nextInCycle ? 'var(--cycle-arrow-color)' : 'var(--arrow-color)';
|
||
html += '<div class="ll-arrow"><svg width="28" height="20">' +
|
||
'<path d="M2 10 L22 10" stroke="' + arrowCol + '" stroke-width="2" fill="none"/>' +
|
||
'<polygon points="22,6 28,10 22,14" fill="' + arrowCol + '"/>' +
|
||
'</svg></div>';
|
||
}
|
||
}
|
||
|
||
if (pos === -1) {
|
||
html += '<div class="ll-arrow"><svg width="28" height="20">' +
|
||
'<path d="M2 10 L22 10" stroke="var(--null-color)" stroke-width="2" stroke-dasharray="4 3" fill="none"/>' +
|
||
'<polygon points="22,7 28,10 22,13" fill="var(--null-color)"/>' +
|
||
'</svg></div>';
|
||
html += '<div class="ll-node-wrapper"><div class="ll-node null-node">null</div><div class="ptr-label-area"></div></div>';
|
||
}
|
||
|
||
html += '</div></div>';
|
||
vizArea.innerHTML = html;
|
||
|
||
if (pos !== -1 && n > 1) {
|
||
setTimeout(function() { drawCycleArc(pos, n); }, 0);
|
||
}
|
||
}
|
||
|
||
function drawCycleArc(pos, n) {
|
||
var container = document.getElementById('llContainer');
|
||
if (!container) return;
|
||
var wrappers = container.querySelectorAll('.ll-node-wrapper');
|
||
if (wrappers.length < n) return;
|
||
|
||
var lastRect = wrappers[n - 1].getBoundingClientRect();
|
||
var entryRect = wrappers[pos].getBoundingClientRect();
|
||
var contRect = container.getBoundingClientRect();
|
||
var svgW = contRect.width;
|
||
var svgH = contRect.height + 60;
|
||
var lastCx = lastRect.left + lastRect.width / 2 - contRect.left;
|
||
var lastBot = lastRect.bottom - contRect.top;
|
||
var entryCx = entryRect.left + entryRect.width / 2 - contRect.left;
|
||
var entryBotY = lastBot + 36;
|
||
var bulgeY = lastBot + 50;
|
||
|
||
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
svg.setAttribute('class', 'cycle-arc-svg');
|
||
svg.setAttribute('viewBox', '0 0 ' + svgW + ' ' + svgH);
|
||
svg.style.height = svgH + 'px';
|
||
svg.innerHTML =
|
||
'<defs><marker id="cycleArrowHead" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">' +
|
||
'<polygon points="0,0 8,3 0,6" fill="var(--cycle-arrow-color)"/></marker></defs>' +
|
||
'<path class="cycle-arc-path" d="M ' + lastCx + ' ' + (lastBot + 4) +
|
||
' C ' + lastCx + ' ' + bulgeY + ', ' + entryCx + ' ' + bulgeY +
|
||
', ' + entryCx + ' ' + entryBotY + '"/>';
|
||
container.style.height = svgH + 'px';
|
||
container.appendChild(svg);
|
||
}
|
||
|
||
function renderDetail(step, vals) {
|
||
if (!step.detail) { detailContent.innerHTML = '—'; return; }
|
||
var d = step.detail;
|
||
var html = '<table class="detail-table"><tbody>';
|
||
if (d.action) html += '<tr><th>动作</th><td>' + d.action + '</td></tr>';
|
||
if (d.slow !== undefined && d.fast !== undefined) {
|
||
var sv = d.slow >= 0 && d.slow < vals.length ? vals[d.slow] : 'null';
|
||
var fv = d.fast >= 0 && d.fast < vals.length ? vals[d.fast] : 'null';
|
||
html += '<tr><th>slow</th><td class="slow-text">节点' + d.slow + '(值=' + sv + ')</td></tr>';
|
||
html += '<tr><th>fast</th><td class="fast-text">节点' + d.fast + '(值=' + fv + ')</td></tr>';
|
||
}
|
||
if (d.slowFrom !== undefined && d.fastFrom !== undefined) {
|
||
var sfv = vals[d.slowFrom] !== undefined ? vals[d.slowFrom] : 'null';
|
||
var ffv = vals[d.fastFrom] !== undefined ? vals[d.fastFrom] : 'null';
|
||
html += '<tr><th>slow 从</th><td class="slow-text">节点' + d.slowFrom + '(' + sfv + ')→ 节点' + d.slow + '(' + vals[d.slow] + ')</td></tr>';
|
||
html += '<tr><th>fast 从</th><td class="fast-text">节点' + d.fastFrom + '(' + ffv + ')→ 节点' + d.fast + '(' + vals[d.fast] + ')</td></tr>';
|
||
}
|
||
if (d.meetVal !== undefined) {
|
||
html += '<tr><th>相遇点</th><td class="meet-text">值 = ' + d.meetVal + ' ⚡</td></tr>';
|
||
}
|
||
html += '</tbody></table>';
|
||
detailContent.innerHTML = html;
|
||
}
|
||
|
||
function renderResult(step) {
|
||
if (!step || !step.done) { resultPanel.style.display = 'none'; return; }
|
||
resultPanel.style.display = '';
|
||
var r = step.result;
|
||
var label = r ? 'true — 检测到环' : 'false — 无环';
|
||
var cls = r ? 'true' : 'false';
|
||
resultContent.innerHTML = '<span class="result-badge ' + cls + '">' + label + '</span>';
|
||
}
|
||
|
||
function highlightCode(lineIndices) {
|
||
var lineEls = codeBlock.querySelectorAll('.code-line');
|
||
for (var i = 0; i < lineEls.length; i++) {
|
||
lineEls[i].classList.remove('code-line-highlight');
|
||
}
|
||
for (var j = 0; j < lineIndices.length; j++) {
|
||
var idx = lineIndices[j];
|
||
if (lineEls[idx]) lineEls[idx].classList.add('code-line-highlight');
|
||
}
|
||
}
|
||
|
||
function wrapCodeLines() {
|
||
var lines = codeBlock.innerHTML.split('\n');
|
||
var out = [];
|
||
for (var i = 0; i < lines.length; i++) {
|
||
out.push('<span class="code-line">' + lines[i] + '</span>');
|
||
}
|
||
codeBlock.innerHTML = out.join('\n');
|
||
}
|
||
|
||
function setPipeline(stage) {
|
||
var stages = pipeline.querySelectorAll('.stage');
|
||
for (var i = 0; i < stages.length; i++) {
|
||
var si = parseInt(stages[i].getAttribute('data-stage'));
|
||
if (si === stage) {
|
||
stages[i].classList.add('active');
|
||
stages[i].classList.remove('completed');
|
||
} else if (si < stage) {
|
||
stages[i].classList.add('completed');
|
||
stages[i].classList.remove('active');
|
||
} else {
|
||
stages[i].classList.remove('active');
|
||
stages[i].classList.remove('completed');
|
||
}
|
||
}
|
||
}
|
||
|
||
function load() {
|
||
var parsed = parseInput(inputBox.value);
|
||
if (!parsed || parsed.vals.length === 0) {
|
||
hintBox.textContent = '⚠️ 输入格式错误,请使用 head=[3,2,0,-4], pos=1';
|
||
return;
|
||
}
|
||
var vals = parsed.vals, pos = parsed.pos;
|
||
currentSteps = buildSteps(vals, pos);
|
||
|
||
if (controller) controller.destroy();
|
||
|
||
controller = new StepController({
|
||
steps: currentSteps,
|
||
onStep: function(step, idx) {
|
||
setPipeline(step.stage);
|
||
hintBox.textContent = step.hint;
|
||
renderViz(vals, pos, step);
|
||
renderDetail(step, vals);
|
||
renderResult(step);
|
||
highlightCode(step.codeLines || []);
|
||
stepCounter.textContent = (idx + 1) + ' / ' + currentSteps.length;
|
||
},
|
||
controls: { prev: btnPrev, play: btnPlay, next: btnNext }
|
||
});
|
||
}
|
||
|
||
btnLoad.addEventListener('click', load);
|
||
exampleSelect.addEventListener('change', function() {
|
||
var ex = EXAMPLES[parseInt(this.value)];
|
||
inputBox.value = 'head=[' + ex.vals + '], pos=' + ex.pos;
|
||
load();
|
||
});
|
||
inputBox.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') load();
|
||
});
|
||
|
||
wrapCodeLines();
|
||
load();
|
||
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|