Files
illustrated-algorithm/linked-list-cycle/index.html
T
2026-08-24 04:35:13 +00:00

527 lines
20 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-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>