Files

527 lines
20 KiB
HTML
Raw Permalink Normal View History

<!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>