Files
illustrated-algorithm/linked_list_algos.py
T
2026-08-24 04:35:13 +00:00

1271 lines
71 KiB
Python
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.
"""Linked-list algorithm JS generators for LeetCode Hot 100 图解."""
# Common JS helper – inline in each function so pages are self-contained
_LLVIZ = r'''
function llViz(nodes, opts) {
opts = opts || {};
var highs = opts.highs || {};
var ptrs = opts.ptrs || {};
var cycle = opts.cycle != null ? opts.cycle : -1;
var label = opts.label || '';
var showNull = opts.showNull !== false;
var html = '';
if (label) html += '<div style="font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:4px;">' + label + '</div>';
html += '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:4px 0;">';
for (var i = 0; i < nodes.length; i++) {
var cls = highs[i] || '';
html += '<div style="position:relative;display:inline-flex;flex-direction:column;align-items:center;">';
html += '<div class="ll-node ' + cls + '">';
html += '<span class="val">' + nodes[i] + '</span>';
if (i === cycle) {
html += '<span class="arrow" style="color:var(--red);font-weight:bold;">&#8617;</span>';
} else if (i < nodes.length - 1) {
html += '<span class="arrow">&rarr;</span>';
} else if (showNull && i === nodes.length - 1) {
html += '<span class="arrow" style="color:var(--text-muted);">&empty;</span>';
}
html += '</div>';
var plabels = [];
var pkeys = Object.keys(ptrs);
for (var pi = 0; pi < pkeys.length; pi++) {
if (ptrs[pkeys[pi]] === i) plabels.push(pkeys[pi]);
}
if (plabels.length) {
html += '<div style="font-size:10px;font-weight:700;color:var(--blue);margin-top:2px;white-space:nowrap;">' + plabels.join(',') + '</div>';
}
html += '</div>';
}
html += '</div>';
return html;
}
'''
_PIPELINE_HELPER = r'''
function setPipeline(stages, current) {
$('pipeline').innerHTML = stages.map(function(st) {
var parts = st.split('\u2192');
return '<span class="pipe-step ' + (current===parts[0]?'active':'') + '">' + parts[1] + '</span>';
}).join('<i>\u2192</i>');
}
'''
def js_intersection_of_two_linked_lists():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{listA:[4,1,8,4,5], listB:[5,6,1,8,4,5], skipA:2, skipB:3, label:'示例1: 交点在8'},
{listA:[1,9,1,2,4], listB:[3,2,4], skipA:3, skipB:1, label:'示例2: 交点在2'},
{listA:[2,6,4], listB:[1,5], skipA:-1, skipB:-1, label:'示例3: 不相交'},
];
var steps, stepCtrl;
function buildSteps(ex) {
steps = [];
var a = ex.listA, b = ex.listB;
var skipA = ex.skipA, skipB = ex.skipB;
var intersect = skipA >= 0;
steps.push({stage:'init', msg:'双指针拼接法:pA走完A走B,pB走完B走A,若相交必在交点相遇', pA:0, pB:0, onA:true, pBonA:true});
var iA = 0, iB = 0, onA = true, pBonA = true;
var maxIter = a.length + b.length + 2;
for (var iter = 0; iter < maxIter; iter++) {
if (intersect && iA === skipA && iB === skipB) {
steps.push({stage:'found', msg:'pA 和 pB 在交点 ' + a[skipA] + ' 相遇!', pA:skipA, pB:skipB, onA:true, pBonA:true, found:true});
return;
}
if (iA >= a.length && iB >= b.length) {
steps.push({stage:'done', msg:'pA 和 pB 同时到达末尾,链表不相交', pA:a.length, pB:b.length, onA:false, pBonA:false, noIntersect:true});
return;
}
steps.push({stage:'step', msg:'pA 在 ' + (onA?'A':'B') + '[' + iA + ']=' + (onA?(iA<a.length?a[iA]:'null'):(iA<b.length?b[iA]:'null')) + ', pB 在 ' + (pBonA?'A':'B') + '[' + iB + ']=' + (pBonA?(iB<a.length?a[iB]:'null'):(iB<b.length?b[iB]:'null')), pA:iA, pB:iB, onA:onA, pBonA:pBonA});
iA++;
if (iA >= (onA ? a.length : b.length)) {
onA = !onA;
iA = 0;
}
iB++;
if (iB >= (pBonA ? a.length : b.length)) {
pBonA = !pBonA;
iB = 0;
}
}
steps.push({stage:'done', msg:'遍历完毕,链表不相交', pA:a.length, pB:b.length, onA:false, pBonA:false, noIntersect:true});
}
function render(step) {
var s = steps[step];
var ex = examples[0];
var exIdx = parseInt($('exampleSelect').value) || 0;
ex = examples[exIdx];
var hlA = {}, hlB = {};
if (s.pA < ex.listA.length && s.onA) hlA[s.pA] = 'current';
if (s.pB < ex.listA.length && s.pBonA) hlB[s.pB] = 'active';
if (!s.pBonA && s.pB < ex.listB.length) hlB[s.pB] = 'active';
if (!s.onA && s.pA < ex.listB.length) hlA[s.pA] = 'current';
if (s.found) { hlA[ex.skipA] = 'active'; hlB[ex.skipB] = 'active'; }
var viz = llViz(ex.listA, {highs:hlA, ptrs:{pA: s.onA ? s.pA : -1}, label:'链表 A'});
viz += llViz(ex.listB, {highs:hlB, ptrs:{pB: s.pBonA ? s.pB : -1}, label:'链表 B'});
if (s.found) viz += '<div style="margin-top:8px;color:var(--green);font-weight:700;">交点值为 ' + ex.listA[ex.skipA] + '</div>';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.found) $('resultContent').innerHTML = '<div class="final-answer">交点引用 = 节点 ' + ex.listA[ex.skipA] + '</div>';
if (s.noIntersect) $('resultContent').innerHTML = '<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">null — 链表不相交</div>';
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','step\u2192移动','found\u2192找到','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = 'A=[4,1,8,4,5] B=[5,6,1,8,4,5] skipA=2 skipB=3';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ var e=examples[parseInt($('exampleSelect').value)]; buildSteps(e); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def getIntersectionNode(headA, headB):\n pA, pB = headA, headB\n while pA != pB:\n pA = pA.next if pA else headB\n pB = pB.next if pB else headA\n return pA', {lang:'Python'});
'''
def js_reverse_linked_list():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{input:[1,2,3,4,5], label:'示例1: [1,2,3,4,5]'},
{input:[1,2], label:'示例2: [1,2]'},
{input:[], label:'示例3: []'},
];
var nodes, steps, stepCtrl;
function buildSteps(arr) {
nodes = arr.slice(); steps = [];
steps.push({stage:'init', msg:'迭代法:prev=null, curr=头节点, 每步将 curr.next 指向 prev', prev:-1, curr:0, next:1});
var prev = -1, curr = 0;
while (curr < arr.length) {
var nxt = curr + 1;
steps.push({stage:'visit', msg:'访问 curr=' + arr[curr] + ':保存 next=' + (nxt<arr.length?arr[nxt]:'null') + ',将 ' + arr[curr] + '.next 指向 ' + (prev>=0?arr[prev]:'null'), prev:prev, curr:curr, next:nxt});
prev = curr;
curr = nxt;
steps.push({stage:'shift', msg:'prev 移到 ' + arr[prev] + ',curr 移到 ' + (curr<arr.length?arr[curr]:'null'), prev:prev, curr:curr, next:curr});
}
steps.push({stage:'done', msg:'反转完成,新头节点为 ' + (prev>=0?arr[prev]:'null'), prev:prev, curr:-1, next:-1});
}
function render(step) {
var s = steps[step];
var hl = {};
if (s.curr >= 0 && s.curr < nodes.length) hl[s.curr] = 'current';
if (s.prev >= 0 && s.prev < nodes.length) hl[s.prev] = 'active';
if (s.next < nodes.length && s.next >= 0) hl[s.next] = 'null-node';
// Show reversed portion in green, unreversed in blue
for (var i = 0; i < nodes.length; i++) {
if (s.prev >= 0 && i <= s.prev && i > s.curr) {
hl[i] = 'active';
}
}
var viz = llViz(nodes, {highs:hl, ptrs:{prev:s.prev>=0&&s.prev<nodes.length?s.prev:-1, curr:s.curr>=0&&s.curr<nodes.length?s.curr:-1, next:s.next<nodes.length&&s.next>=0?s.next:-1}, label:'链表状态'});
if (s.prev >= 0) {
var reversed = [];
for (var i = s.prev; i >= 0; i--) reversed.push(nodes[i]);
viz += llViz(reversed, {highs:{}, label:'已反转部分', showNull:false});
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
var result = [];
for (var i = nodes.length-1; i >= 0; i--) result.push(nodes[i]);
$('resultContent').innerHTML = '<div class="final-answer">反转结果:<b>[' + result.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','visit\u2192访问','shift\u2192移动','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[1,2,3,4,5]';
buildSteps(examples[0].input);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ try { var arr=JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); } catch(e){ alert('请输入合法 JSON 数组'); } };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)].input); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def reverseList(head):\n prev, curr = None, head\n while curr:\n nxt = curr.next\n curr.next = prev\n prev = curr\n curr = nxt\n return prev', {lang:'Python'});
'''
def js_palindrome_linked_list():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{input:[1,2,2,1], label:'示例1: [1,2,2,1] 回文'},
{input:[1,2], label:'示例2: [1,2] 非回文'},
{input:[1,2,3,2,1], label:'示例3: [1,2,3,2,1] 回文'},
];
var nodes, steps, stepCtrl;
function buildSteps(arr) {
nodes = arr.slice(); steps = [];
if (arr.length === 0) { steps.push({stage:'done',msg:'空链表是回文',phase:'done'}); return; }
steps.push({stage:'init', msg:'快慢指针找中点 + 反转后半部分 + 比较', phase:'find_mid', slow:0, fast:0});
// Phase 1: find middle
var slow = 0, fast = 0;
while (fast < arr.length - 1 && fast + 1 < arr.length - 1) {
slow++; fast += 2;
steps.push({stage:'move', msg:'slow→'+arr[slow]+', fast→'+arr[Math.min(fast,arr.length-1)], phase:'find_mid', slow:slow, fast:Math.min(fast,arr.length-1)});
}
var mid = slow + 1;
steps.push({stage:'mid', msg:'中点后半起始位置:index ' + mid + ' (值=' + arr[mid] + ')', phase:'find_mid', slow:slow, fast:Math.min(fast,arr.length-1), mid:mid});
// Phase 2: reverse second half
var second = arr.slice(mid);
var reversed = second.slice().reverse();
steps.push({stage:'reverse', msg:'反转后半 [' + second.join(',') + '] → [' + reversed.join(',') + ']', phase:'reverse', second:second.slice(), reversed:reversed.slice()});
// Phase 3: compare
var first = arr.slice(0, arr.length - second.length);
var isPalin = true;
for (var i = 0; i < reversed.length; i++) {
if (first[i] !== reversed[i]) { isPalin = false; break; }
}
steps.push({stage:'compare', msg:'前半 ['+first.join(',')+'] vs 反转后半 ['+reversed.join(',')+'] → ' + (isPalin?'匹配 ✓':'不匹配 ✗'), phase:'compare', first:first, reversed:reversed, isPalin:isPalin});
steps.push({stage:'done', msg:'结果:' + (isPalin?'是回文链表':'不是回文链表'), phase:'done', isPalin:isPalin});
}
function render(step) {
var s = steps[step];
var viz = '';
if (s.phase === 'find_mid' || s.stage === 'init') {
var hl = {};
if (s.slow >= 0 && s.slow < nodes.length) hl[s.slow] = 'active';
if (s.fast >= 0 && s.fast < nodes.length) hl[s.fast] = 'current';
viz = llViz(nodes, {highs:hl, ptrs:{slow:s.slow, fast:s.fast}, label:'寻找中点'});
} else if (s.phase === 'reverse') {
viz = llViz(nodes, {highs:{}, label:'原始链表'});
viz += llViz(s.reversed, {highs:{}, label:'反转后半', showNull:false});
} else if (s.phase === 'compare' || s.phase === 'done') {
viz = llViz(s.first, {label:'前半部分', showNull:false});
viz += llViz(s.reversed, {label:'反转后半', showNull:false});
viz += '<div style="margin-top:8px;font-size:15px;">' + (s.isPalin ? '<span style="color:var(--green);font-weight:700;">✓ 匹配</span>' : '<span style="color:var(--red);font-weight:700;">✗ 不匹配</span>') + '</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">' + (s.isPalin?'是回文链表 ✓':'不是回文链表 ✗') + '</div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192开始','move\u2192快慢','mid\u2192中点','reverse\u2192反转','compare\u2192比较','done\u2192结果'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[1,2,2,1]';
buildSteps(examples[0].input);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ try { var arr=JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); } catch(e){ alert('请输入合法 JSON 数组'); } };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)].input); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def isPalindrome(head):\n slow = fast = head\n while fast and fast.next:\n slow = slow.next\n fast = fast.next.next\n prev = None\n while slow:\n nxt = slow.next\n slow.next = prev\n prev = slow\n slow = nxt\n left, right = head, prev\n while right:\n if left.val != right.val:\n return False\n left = left.next\n right = right.next\n return True', {lang:'Python'});
'''
def js_linked_list_cycle():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{input:[3,2,0,-4], pos:1, label:'示例1: [3,2,0,-4] pos=1 有环'},
{input:[1,2], pos:0, label:'示例2: [1,2] pos=0 有环'},
{input:[1], pos:-1, label:'示例3: [1] 无环'},
];
var nodes, steps, stepCtrl;
function buildSteps(ex) {
nodes = ex.input.slice(); steps = [];
var hasCycle = ex.pos >= 0;
steps.push({stage:'init', msg:'快慢指针:slow每次走1步,fast每次走2步,若相遇则有环', slow:0, fast:0, cyclePos:ex.pos});
var slow = 0, fast = 0, n = nodes.length;
var maxIter = n + 2;
for (var iter = 0; iter < maxIter; iter++) {
slow = (slow + 1) % n;
fast = (fast + 2) % n;
if (!hasCycle) {
if (fast >= n) {
steps.push({stage:'noreturn', msg:'fast 到达链表末尾,无环', slow:slow, fast:n, cyclePos:-1});
steps.push({stage:'done', msg:'链表无环', slow:slow, fast:n, cyclePos:-1, result:false});
return;
}
}
steps.push({stage:'step', msg:'slow→'+nodes[slow]+', fast→'+nodes[fast] + (slow===fast&&hasCycle?' 相遇!有环!':''), slow:slow, fast:fast, cyclePos:ex.pos, met:slow===fast&&hasCycle});
if (slow === fast && hasCycle) {
steps.push({stage:'done', msg:'快慢指针在节点 '+nodes[slow]+' 相遇,链表有环', slow:slow, fast:fast, cyclePos:ex.pos, result:true});
return;
}
}
steps.push({stage:'done', msg:'遍历完毕,链表无环', slow:0, fast:0, cyclePos:-1, result:false});
}
function render(step) {
var s = steps[step];
var hl = {};
if (s.slow >= 0 && s.slow < nodes.length) hl[s.slow] = 'active';
if (s.fast >= 0 && s.fast < nodes.length) {
hl[s.fast] = s.slow === s.fast ? 'active' : 'current';
}
var viz = llViz(nodes, {highs:hl, ptrs:{slow:s.slow>=0&&s.slow<nodes.length?s.slow:-1, fast:s.fast>=0&&s.fast<nodes.length?s.fast:-1}, cycle:s.cyclePos, label:'链表'});
if (s.met) viz += '<div style="margin-top:8px;font-size:15px;color:var(--green);font-weight:700;">⭕ 快慢指针相遇!有环!</div>';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = s.result
? '<div class="final-answer">链表有环 ✓ (环入口在 index ' + s.cyclePos + ')</div>'
: '<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">链表无环 ✗</div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','step\u2192移动','noreturn\u2192无返回','done\u2192结果'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[3,2,0,-4] pos=1';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def hasCycle(head):\n slow = fast = head\n while fast and fast.next:\n slow = slow.next\n fast = fast.next.next\n if slow is fast:\n return True\n return False', {lang:'Python'});
'''
def js_linked_list_cycle_ii():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{input:[3,2,0,-4], pos:1, label:'示例1: [3,2,0,-4] pos=1'},
{input:[1,2], pos:0, label:'示例2: [1,2] pos=0'},
{input:[1], pos:-1, label:'示例3: 无环'},
];
var nodes, steps, stepCtrl;
function buildSteps(ex) {
nodes = ex.input.slice(); steps = [];
var hasCycle = ex.pos >= 0;
var n = nodes.length;
steps.push({stage:'init', msg:'Floyd 算法:Phase1 快慢指针检测环,Phase2 找环入口', phase:'detect', slow:0, fast:0, cyclePos:ex.pos});
if (!hasCycle) {
steps.push({stage:'done', msg:'链表无环,返回 null', phase:'done', entry:-1, result:null});
return;
}
// Phase 1: detect
var slow = 0, fast = 0;
var maxD = n + 2;
for (var d = 0; d < maxD; d++) {
slow = (slow + 1) % n;
fast = (fast + 2) % n;
steps.push({stage:'detect', msg:'Phase1: slow→'+nodes[slow]+', fast→'+nodes[fast], phase:'detect', slow:slow, fast:fast, cyclePos:ex.pos});
if (slow === fast) {
steps.push({stage:'met', msg:'Phase1: 快慢在 '+nodes[slow]+' 相遇,确认有环', phase:'detect', slow:slow, fast:fast, cyclePos:ex.pos});
break;
}
}
// Phase 2: find entry
steps.push({stage:'phase2', msg:'Phase2: ptr1 从头出发,ptr2 从相遇点出发,各走1步', phase:'entry', ptr1:0, ptr2:slow, cyclePos:ex.pos});
var ptr1 = 0, ptr2 = slow;
for (var e = 0; e < n + 2; e++) {
if (ptr1 === ptr2) {
steps.push({stage:'found', msg:'Phase2: ptr1 和 ptr2 在 '+nodes[ptr1]+' 相遇,这就是环入口!', phase:'entry', ptr1:ptr1, ptr2:ptr2, cyclePos:ex.pos, entry:ptr1});
break;
}
ptr1 = (ptr1 + 1) % n;
ptr2 = (ptr2 + 1) % n;
steps.push({stage:'seek', msg:'Phase2: ptr1→'+nodes[ptr1]+', ptr2→'+nodes[ptr2], phase:'entry', ptr1:ptr1, ptr2:ptr2, cyclePos:ex.pos});
}
steps.push({stage:'done', msg:'环入口为节点 '+nodes[ex.pos]+' (index '+ex.pos+')', phase:'done', entry:ex.pos, cyclePos:ex.pos});
}
function render(step) {
var s = steps[step];
var hl = {};
var ptrs = {};
if (s.phase === 'detect') {
if (s.slow >= 0 && s.slow < nodes.length) hl[s.slow] = 'active';
if (s.fast >= 0 && s.fast < nodes.length) hl[s.fast] = s.slow===s.fast ? 'active' : 'current';
ptrs = {slow:s.slow, fast:s.fast};
} else if (s.phase === 'entry') {
if (s.ptr1 >= 0 && s.ptr1 < nodes.length) hl[s.ptr1] = 'active';
if (s.ptr2 >= 0 && s.ptr2 < nodes.length) hl[s.ptr2] = s.ptr1===s.ptr2 ? 'active' : 'current';
ptrs = {ptr1:s.ptr1, ptr2:s.ptr2};
}
var viz = llViz(nodes, {highs:hl, ptrs:ptrs, cycle:s.cyclePos||-1, label:'链表'});
if (s.stage === 'found' || s.stage === 'met') {
viz += '<div style="margin-top:8px;font-size:15px;color:var(--green);font-weight:700;">🎯 ' + s.msg + '</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
if (s.entry >= 0) {
$('resultContent').innerHTML = '<div class="final-answer">环入口 = 节点 <b>'+nodes[s.entry]+'</b> (index '+s.entry+')</div>';
} else {
$('resultContent').innerHTML = '<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">无环,返回 null</div>';
}
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','detect\u2192检测','met\u2192相遇','phase2\u2192找入口','seek\u2192搜索','found\u2192找到','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[3,2,0,-4] pos=1';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def detectCycle(head):\n slow = fast = head\n while fast and fast.next:\n slow = slow.next\n fast = fast.next.next\n if slow is fast:\n ptr1, ptr2 = head, slow\n while ptr1 is not ptr2:\n ptr1 = ptr1.next\n ptr2 = ptr2.next\n return ptr1\n return None', {lang:'Python'});
'''
def js_merge_two_sorted_lists():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{list1:[1,2,4], list2:[1,3,4], label:'示例1: [1,2,4]+[1,3,4]'},
{list1:[], list2:[], label:'示例2: 两空链表'},
{list1:[], list2:[0], label:'示例3: []+[0]'},
];
var steps, stepCtrl;
function buildSteps(ex) {
steps = [];
var l1 = ex.list1.slice(), l2 = ex.list2.slice();
steps.push({stage:'init', msg:'双指针合并:比较两个链表头,取较小者加入结果', i1:0, i2:0, merged:[], phase:'merge'});
var i1 = 0, i2 = 0, merged = [];
while (i1 < l1.length && i2 < l2.length) {
steps.push({stage:'compare', msg:'比较 l1['+i1+']=' + l1[i1] + ' vs l2['+i2+']=' + l2[i2], i1:i1, i2:i2, merged:merged.slice(), phase:'merge'});
if (l1[i1] <= l2[i2]) {
merged.push(l1[i1]);
steps.push({stage:'take1', msg:'取 l1['+i1+']=' + l1[i1] + ' 加入结果', i1:i1+1, i2:i2, merged:merged.slice(), phase:'merge'});
i1++;
} else {
merged.push(l2[i2]);
steps.push({stage:'take2', msg:'取 l2['+i2+']=' + l2[i2] + ' 加入结果', i1:i1, i2:i2+1, merged:merged.slice(), phase:'merge'});
i2++;
}
}
while (i1 < l1.length) { merged.push(l1[i1]); i1++; }
while (i2 < l2.length) { merged.push(l2[i2]); i2++; }
steps.push({stage:'done', msg:'合并完成:[' + merged.join(',') + ']', i1:i1, i2:i2, merged:merged.slice(), phase:'done'});
}
function render(step) {
var s = steps[step];
var exIdx = parseInt($('exampleSelect').value) || 0;
var ex = examples[exIdx];
var hl1 = {}, hl2 = {};
if (s.i1 < ex.list1.length) {
hl1[s.i1] = s.stage === 'take1' ? 'active' : 'current';
}
if (s.i2 < ex.list2.length) {
hl2[s.i2] = s.stage === 'take2' ? 'active' : 'current';
}
var viz = llViz(ex.list1, {highs:hl1, ptrs:{i1:s.i1<ex.list1.length?s.i1:-1}, label:'链表 1'});
viz += llViz(ex.list2, {highs:hl2, ptrs:{i2:s.i2<ex.list2.length?s.i2:-1}, label:'链表 2'});
if (s.merged.length > 0) {
viz += llViz(s.merged, {highs:{[s.merged.length-1]:'active'}, label:'合并结果', showNull:s.phase==='done'?true:false});
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">合并结果:<b>[' + s.merged.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','compare\u2192比较','take1\u2192取L1','take2\u2192取L2','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = 'L1=[1,2,4] L2=[1,3,4]';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def mergeTwoLists(l1, l2):\n dummy = ListNode(0)\n curr = dummy\n while l1 and l2:\n if l1.val <= l2.val:\n curr.next = l1\n l1 = l1.next\n else:\n curr.next = l2\n l2 = l2.next\n curr = curr.next\n curr.next = l1 or l2\n return dummy.next', {lang:'Python'});
'''
def js_add_two_numbers():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{l1:[2,4,3], l2:[5,6,4], label:'示例1: 342+465=807'},
{l1:[0], l2:[0], label:'示例2: 0+0=0'},
{l1:[9,9,9,9,9,9,9], l2:[9,9,9,9], label:'示例3: 大数相加'},
];
var steps, stepCtrl;
function buildSteps(ex) {
steps = [];
var l1 = ex.l1.slice(), l2 = ex.l2.slice();
steps.push({stage:'init', msg:'逐位相加 + 进位,链表已按逆序存储各位数字', i1:0, i2:0, carry:0, result:[], phase:'add'});
var i1 = 0, i2 = 0, carry = 0, result = [];
while (i1 < l1.length || i2 < l2.length || carry) {
var v1 = i1 < l1.length ? l1[i1] : 0;
var v2 = i2 < l2.length ? l2[i2] : 0;
var sum = v1 + v2 + carry;
var digit = sum % 10;
var newCarry = Math.floor(sum / 10);
steps.push({stage:'calc', msg: v1+' + '+v2+' + '+carry+'(carry) = '+sum+' → 写 '+digit+',进位 '+newCarry, i1:i1, i2:i2, carry:carry, sum:sum, digit:digit, newCarry:newCarry, result:result.slice(), phase:'add'});
result.push(digit);
carry = newCarry;
i1++; i2++;
}
steps.push({stage:'done', msg:'结果:[' + result.join(',') + ']', result:result.slice(), phase:'done'});
}
function render(step) {
var s = steps[step];
var exIdx = parseInt($('exampleSelect').value) || 0;
var ex = examples[exIdx];
var hl1 = {}, hl2 = {}, hlR = {};
if (s.i1 < ex.l1.length) hl1[s.i1] = 'current';
if (s.i2 < ex.l2.length) hl2[s.i2] = 'current';
if (s.digit !== undefined && s.result) {
for (var ri = 0; ri < s.result.length; ri++) hlR[ri] = 'active';
}
var viz = llViz(ex.l1, {highs:hl1, ptrs:{p1:s.i1<ex.l1.length?s.i1:-1}, label:'链表1 (逆序)'});
viz += llViz(ex.l2, {highs:hl2, ptrs:{p2:s.i2<ex.l2.length?s.i2:-1}, label:'链表2 (逆序)'});
if (s.carry !== undefined && s.stage !== 'done') {
viz += '<div style="margin:6px 0;font-size:14px;"><b>carry = ' + s.carry + '</b></div>';
}
if (s.result && s.result.length > 0) {
viz += llViz(s.result, {highs:hlR, label:'结果 (逆序)'});
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
var num1 = ex.l1.slice().reverse().join('');
var num2 = ex.l2.slice().reverse().join('');
var numR = s.result.slice().reverse().join('');
$('resultContent').innerHTML = '<div class="final-answer">' + num1 + ' + ' + num2 + ' = ' + numR + '<br>结果链表:<b>[' + s.result.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192开始','calc\u2192计算','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = 'l1=[2,4,3] l2=[5,6,4]';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def addTwoNumbers(l1, l2):\n dummy = ListNode(0)\n curr = dummy\n carry = 0\n while l1 or l2 or carry:\n v1 = l1.val if l1 else 0\n v2 = l2.val if l2 else 0\n s = v1 + v2 + carry\n carry = s // 10\n curr.next = ListNode(s % 10)\n curr = curr.next\n l1 = l1.next if l1 else None\n l2 = l2.next if l2 else None\n return dummy.next', {lang:'Python'});
'''
def js_remove_nth_node_from_end_of_list():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{input:[1,2,3,4,5], n:2, label:'示例1: [1,2,3,4,5] n=2 删除4'},
{input:[1], n:1, label:'示例2: [1] n=1 删除1'},
{input:[1,2], n:1, label:'示例3: [1,2] n=1 删除2'},
];
var nodes, steps, stepCtrl;
function buildSteps(ex) {
nodes = ex.input.slice(); steps = [];
var n = ex.n;
var len = nodes.length;
steps.push({stage:'init', msg:'快慢指针:fast 先走 n 步,然后 slow 和 fast 同步走', slow:-1, fast:0, phase:'advance'});
// Use dummy node represented as index -1
var slow = -1, fast = 0;
// fast advances n steps
for (var i = 0; i < n; i++) {
steps.push({stage:'advance', msg:'fast 前进:' + (i+1) + '/' + n + ' 步,fast=' + (fast < len ? nodes[fast] : 'null'), slow:-1, fast:fast, phase:'advance'});
fast++;
}
steps.push({stage:'ready', msg:'fast 已领先 ' + n + ' 步,现在 slow 和 fast 同步前进', slow:-1, fast:fast, phase:'move'});
// both advance until fast reaches end
while (fast < len) {
slow++;
if (fast < len) fast++;
steps.push({stage:'move', msg:'slow→' + (slow>=0?nodes[slow]:'dummy') + ', fast→' + (fast<len?nodes[fast]:'null'), slow:slow, fast:fast, phase:'move'});
}
// slow.next is the node to delete
var delIdx = slow + 1;
steps.push({stage:'delete', msg:'删除 slow.next = 节点 ' + nodes[delIdx] + ' (index ' + delIdx + ')', slow:slow, fast:fast, delIdx:delIdx, phase:'delete'});
var result = nodes.slice();
result.splice(delIdx, 1);
steps.push({stage:'done', msg:'删除后结果:[' + result.join(',') + ']', result:result, phase:'done'});
}
function render(step) {
var s = steps[step];
var hl = {};
var ptrs = {};
if (s.slow >= 0 && s.slow < nodes.length) { hl[s.slow] = 'active'; ptrs.slow = s.slow; }
if (s.fast >= 0 && s.fast < nodes.length) { hl[s.fast] = 'current'; ptrs.fast = s.fast; }
if (s.delIdx !== undefined && s.delIdx < nodes.length) hl[s.delIdx] = 'null-node';
var vizLabel = '链表 (n=' + examples[parseInt($('exampleSelect').value)||0].n + ')';
var viz = llViz(nodes, {highs:hl, ptrs:ptrs, label:vizLabel});
if (s.slow === -1 && (s.phase === 'advance' || s.phase === 'ready')) {
viz += '<div style="font-size:12px;color:var(--text-muted);">slow 在 dummy 节点</div>';
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">删除后:<b>[' + s.result.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','advance\u2192快进','ready\u2192就绪','move\u2192同步','delete\u2192删除','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[1,2,3,4,5] n=2';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def removeNthFromEnd(head, n):\n dummy = ListNode(0, head)\n fast = slow = dummy\n for _ in range(n):\n fast = fast.next\n while fast.next:\n slow = slow.next\n fast = fast.next\n slow.next = slow.next.next\n return dummy.next', {lang:'Python'});
'''
def js_swap_nodes_in_pairs():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{input:[1,2,3,4], label:'示例1: [1,2,3,4]'},
{input:[], label:'示例2: []'},
{input:[1], label:'示例3: [1]'},
{input:[1,2,3], label:'示例4: [1,2,3]'},
];
var nodes, steps, stepCtrl;
function buildSteps(arr) {
nodes = arr.slice(); steps = [];
if (arr.length < 2) {
steps.push({stage:'done', msg:'节点数不足2,无需交换', result:nodes.slice(), phase:'done'});
return;
}
steps.push({stage:'init', msg:'两两交换:对每对相邻节点 (a,b) 进行三步指针交换', pairStart:0, phase:'swap'});
var result = nodes.slice();
for (var i = 0; i + 1 < result.length; i += 2) {
steps.push({stage:'identify', msg:'识别交换对:' + result[i] + ' ↔ ' + result[i+1], pairStart:i, result:result.slice(), phase:'swap'});
// swap
var tmp = result[i];
result[i] = result[i+1];
result[i+1] = tmp;
steps.push({stage:'swap', msg:'交换 ' + nodes[i] + ' 和 ' + nodes[i+1] + ':[' + result.join(',') + ']', pairStart:i, result:result.slice(), phase:'swap'});
}
steps.push({stage:'done', msg:'交换完成:[' + result.join(',') + ']', result:result.slice(), phase:'done'});
}
function render(step) {
var s = steps[step];
var hl = {};
if (s.pairStart !== undefined && s.pairStart + 1 < nodes.length) {
if (s.stage === 'identify') {
hl[s.pairStart] = 'current';
hl[s.pairStart + 1] = 'current';
} else if (s.stage === 'swap') {
hl[s.pairStart] = 'active';
hl[s.pairStart + 1] = 'active';
}
}
var viz = llViz(s.result || nodes, {highs:hl, ptrs:{}, label:'链表状态'});
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">交换结果:<b>[' + s.result.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','identify\u2192识别','swap\u2192交换','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[1,2,3,4]';
buildSteps(examples[0].input);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ try { var arr=JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); } catch(e){ alert('请输入合法 JSON 数组'); } };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)].input); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def swapPairs(head):\n dummy = ListNode(0, head)\n prev = dummy\n while prev.next and prev.next.next:\n a, b = prev.next, prev.next.next\n prev.next = b\n a.next = b.next\n b.next = a\n prev = a\n return dummy.next', {lang:'Python'});
'''
def js_reverse_nodes_in_k_group():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{input:[1,2,3,4,5], k:2, label:'示例1: [1,2,3,4,5] k=2'},
{input:[1,2,3,4,5], k:3, label:'示例2: [1,2,3,4,5] k=3'},
{input:[1,2,3,4,5], k:1, label:'示例3: k=1 不翻转'},
];
var nodes, steps, stepCtrl;
function buildSteps(ex) {
nodes = ex.input.slice(); steps = [];
var k = ex.k;
var result = nodes.slice();
steps.push({stage:'init', msg:'K个一组翻转链表,k=' + k + ':每组k个节点内部反转,不足k个不翻转', phase:'group', result:result.slice(), groupIdx:0});
var groupIdx = 0;
for (var i = 0; i + k <= result.length; i += k) {
steps.push({stage:'identify', msg:'第 ' + (groupIdx+1) + ' 组:[' + result.slice(i, i+k).join(',') + ']', phase:'group', result:result.slice(), groupStart:i, groupIdx:groupIdx});
// reverse in-place
var left = i, right = i + k - 1;
while (left < right) {
var tmp = result[left];
result[left] = result[right];
result[right] = tmp;
left++; right--;
}
steps.push({stage:'reverse', msg:'第 ' + (groupIdx+1) + ' 组翻转后:[' + result.slice(i, i+k).join(',') + ']', phase:'group', result:result.slice(), groupStart:i, groupIdx:groupIdx});
groupIdx++;
}
var remaining = result.length % k;
if (remaining > 0) {
steps.push({stage:'skip', msg:'剩余 ' + remaining + ' 个节点不足 k=' + k + ',不翻转', phase:'group', result:result.slice()});
}
steps.push({stage:'done', msg:'翻转完成:[' + result.join(',') + ']', phase:'done', result:result.slice()});
}
function render(step) {
var s = steps[step];
var hl = {};
if (s.groupStart !== undefined) {
var k = examples[parseInt($('exampleSelect').value)||0].k;
for (var gi = s.groupStart; gi < s.groupStart + k && gi < nodes.length; gi++) {
hl[gi] = s.stage === 'reverse' ? 'active' : 'current';
}
}
var viz = llViz(s.result || nodes, {highs:hl, label:'链表状态'});
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">结果:<b>[' + s.result.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','identify\u2192识别组','reverse\u2192翻转','skip\u2192跳过','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[1,2,3,4,5] k=2';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def reverseKGroup(head, k):\n def reverse(head, tail):\n prev = None\n curr = head\n while curr != tail:\n nxt = curr.next\n curr.next = prev\n prev = curr\n curr = nxt\n return prev\n\n dummy = ListNode(0, head)\n prev = dummy\n while True:\n tail = prev.next\n for _ in range(k):\n if not tail: return dummy.next\n tail = tail.next\n new_head = reverse(prev.next, tail)\n tmp = prev.next\n prev.next = new_head\n tmp.next = tail\n prev = tmp\n return dummy.next', {lang:'Python'});
'''
def js_copy_list_with_random_pointer():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{nodes:[[7,null],[13,0],[11,4],[10,2],[1,0]], label:'示例1: 5个节点'},
{nodes:[[1,1],[2,1]], label:'示例2: 2个节点'},
{nodes:[[3,null],[3,0],[3,null]], label:'示例3: 3个节点'},
];
var steps, stepCtrl;
function buildSteps(ex) {
steps = [];
var n = ex.nodes.length;
steps.push({stage:'init', msg:'交错复制法:①在每个原节点后插入复制节点 ②设置random指针 ③拆分', phase:'interleave'});
// Phase 1: interleave
var interleaved = [];
for (var i = 0; i < n; i++) {
interleaved.push({val:ex.nodes[i][0], isCopy:false, origIdx:i});
interleaved.push({val:ex.nodes[i][0], isCopy:true, origIdx:i});
}
steps.push({stage:'interleave', msg:'Phase1:在每个原节点后插入其复制 → ' + interleaved.map(function(x){return x.val+(x.isCopy?"'":"");}).join(','), phase:'interleave', interleaved:interleaved.slice()});
// Phase 2: set random pointers
var randomLinks = [];
for (var i = 0; i < n; i++) {
var rnd = ex.nodes[i][1];
if (rnd !== null) {
randomLinks.push({from:i*2+1, to:rnd*2+1, msg:"copy["+i+"].random = copy["+rnd+"] (值="+ex.nodes[rnd][0]+")"});
} else {
randomLinks.push({from:i*2+1, to:-1, msg:"copy["+i+"].random = null"});
}
}
steps.push({stage:'random', msg:'Phase2:设置random指针 → ' + randomLinks.map(function(r){return r.msg;}).join('; '), phase:'random', interleaved:interleaved.slice(), randomLinks:randomLinks});
// Phase 3: split
var original = [], copy = [];
for (var i = 0; i < interleaved.length; i++) {
if (i%2===0) original.push(interleaved[i].val);
else copy.push(interleaved[i].val);
}
steps.push({stage:'split', msg:'Phase3:拆分为原链表和复制链表', phase:'split', original:original, copy:copy});
steps.push({stage:'done', msg:'深拷贝完成', phase:'done', copy:copy});
}
function renderInterleaved(interleaved, hlIdx) {
var html = '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:8px 0;">';
for (var i = 0; i < interleaved.length; i++) {
var n = interleaved[i];
var bg = n.isCopy ? 'var(--green-light)' : 'var(--blue-light)';
var border = n.isCopy ? 'var(--green)' : 'var(--blue)';
var label = n.val + (n.isCopy ? "'" : '');
var hl = (hlIdx === i) ? 'box-shadow:0 0 0 3px var(--orange);' : '';
html += '<div style="display:inline-flex;flex-direction:column;align-items:center;">';
html += '<div style="min-width:40px;height:36px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;padding:0 8px;border:2px solid '+border+';border-radius:8px;background:'+bg+';'+hl+';">'+label+'</div>';
html += '<div style="font-size:9px;color:var(--text-muted);">'+(n.isCopy?'copy':'orig')+'</div>';
html += '</div>';
if (i < interleaved.length - 1) html += '<span style="color:var(--text-muted);">&rarr;</span>';
}
html += '</div>';
return html;
}
function render(step) {
var s = steps[step];
var viz = '';
if (s.phase === 'interleave' && s.interleaved) {
viz = renderInterleaved(s.interleaved, -1);
} else if (s.phase === 'random' && s.interleaved) {
viz = renderInterleaved(s.interleaved, -1);
viz += '<div style="margin-top:8px;"><b>Random 指针设置:</b></div>';
s.randomLinks.forEach(function(r) {
viz += '<div style="font-size:13px;margin:2px 0;">' + r.msg + '</div>';
});
} else if (s.phase === 'split' || s.phase === 'done') {
viz = llViz(s.original, {highlights:{}, label:'原链表', showNull:false});
viz += llViz(s.copy, {highlights:{}, label:'复制链表', showNull:false});
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">深拷贝链表:<b>[' + s.copy.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','interleave\u2192交错插入','random\u2192设置随机','split\u2192拆分','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[[7,null],[13,0],[11,4],[10,2],[1,0]]';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def copyRandomList(head):\n if not head: return None\n # 1. interleave copies\n cur = head\n while cur:\n copy = Node(cur.val, cur.next, None)\n cur.next = copy\n cur = copy.next\n # 2. set random pointers\n cur = head\n while cur:\n if cur.random:\n cur.next.random = cur.random.next\n cur = cur.next.next\n # 3. split\n cur = head\n copy_head = head.next\n while cur:\n copy = cur.next\n cur.next = copy.next\n if copy.next:\n copy.next = copy.next.next\n cur = cur.next\n return copy_head', {lang:'Python'});
'''
def js_sort_list():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{input:[4,2,1,3], label:'示例1: [4,2,1,3]'},
{input:[-1,5,3,4,0], label:'示例2: [-1,5,3,4,0]'},
{input:[], label:'示例3: []'},
];
var steps, stepCtrl;
function buildSteps(arr) {
steps = [];
if (arr.length === 0) { steps.push({stage:'done',msg:'空链表',result:[],phase:'done'}); return; }
function mergeSort(a, depth) {
if (a.length <= 1) return a;
var mid = Math.floor(a.length / 2);
var left = a.slice(0, mid);
var right = a.slice(mid);
steps.push({stage:'split', msg:'分割 ['+a.join(',')+'] → ['+left.join(',')+'] + ['+right.join(',')+']', phase:'split', depth:depth, arr:a.slice(), left:left, right:right});
var sortedL = mergeSort(left, depth+1);
var sortedR = mergeSort(right, depth+1);
// merge
var merged = [];
var i = 0, j = 0;
while (i < sortedL.length && j < sortedR.length) {
if (sortedL[i] <= sortedR[j]) { merged.push(sortedL[i]); i++; }
else { merged.push(sortedR[j]); j++; }
}
while (i < sortedL.length) { merged.push(sortedL[i]); i++; }
while (j < sortedR.length) { merged.push(sortedR[j]); j++; }
steps.push({stage:'merge', msg:'合并 ['+sortedL.join(',')+'] + ['+sortedR.join(',')+'] → ['+merged.join(',')+']', phase:'merge', depth:depth, sortedL:sortedL, sortedR:sortedR, result:merged});
return merged;
}
steps.push({stage:'init', msg:'归并排序:递归分割 + 合并', phase:'init', arr:arr.slice()});
var result = mergeSort(arr.slice(), 0);
steps.push({stage:'done', msg:'排序完成:[' + result.join(',') + ']', phase:'done', result:result});
}
function render(step) {
var s = steps[step];
var viz = '';
if (s.phase === 'init') {
viz = llViz(s.arr, {label:'原始链表'});
} else if (s.phase === 'split') {
viz = llViz(s.arr, {label:'分割前', showNull:false});
viz += llViz(s.left, {label:'左半', showNull:false});
viz += llViz(s.right, {label:'右半', showNull:false});
viz += '<div style="margin-top:4px;font-size:12px;color:var(--text-muted);">递归深度: ' + s.depth + '</div>';
} else if (s.phase === 'merge') {
viz = llViz(s.sortedL, {label:'左半(已排序)', showNull:false});
viz += llViz(s.sortedR, {label:'右半(已排序)', showNull:false});
viz += llViz(s.result, {highlights:{}, label:'合并结果', showNull:false});
} else if (s.phase === 'done') {
viz = llViz(s.result, {label:'排序结果'});
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">排序结果:<b>[' + s.result.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192开始','split\u2192分割','merge\u2192合并','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[4,2,1,3]';
buildSteps(examples[0].input);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ try { var arr=JSON.parse($('inputArea').value); buildSteps(arr); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); } catch(e){ alert('请输入合法 JSON 数组'); } };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)].input); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('def sortList(head):\n if not head or not head.next:\n return head\n # find middle\n slow = fast = head\n while fast.next and fast.next.next:\n slow = slow.next\n fast = fast.next.next\n mid = slow.next\n slow.next = None\n left = sortList(head)\n right = sortList(mid)\n # merge\n dummy = ListNode(0)\n cur = dummy\n while left and right:\n if left.val <= right.val:\n cur.next = left\n left = left.next\n else:\n cur.next = right\n right = right.next\n cur = cur.next\n cur.next = left or right\n return dummy.next', {lang:'Python'});
'''
def js_merge_k_sorted_lists():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{lists:[[1,4,5],[1,3,4],[2,6]], label:'示例1: 3个链表'},
{lists:[], label:'示例2: 空列表'},
{lists:[[]], label:'示例3: 1个空链表'},
];
var steps, stepCtrl;
function buildSteps(ex) {
steps = [];
var lists = ex.lists.filter(function(l){return l.length>0;});
if (lists.length === 0) { steps.push({stage:'done',msg:'无有效链表',result:[],phase:'done'}); return; }
steps.push({stage:'init', msg:'使用最小堆合并K个有序链表:每次取当前所有链表头的最小值', phase:'init', heads:lists.map(function(l){return 0;}), result:[]});
// simulate min-heap merge
var ptrs = lists.map(function(){return 0;}); // current index in each list
var result = [];
var totalLen = lists.reduce(function(s,l){return s+l.length;}, 0);
for (var step = 0; step < totalLen; step++) {
// find min among current heads
var minVal = Infinity, minIdx = -1;
for (var i = 0; i < lists.length; i++) {
if (ptrs[i] < lists[i].length && lists[i][ptrs[i]] < minVal) {
minVal = lists[i][ptrs[i]];
minIdx = i;
}
}
steps.push({stage:'compare', msg:'比较各链表头:' + lists.map(function(l,i){return ptrs[i]<l.length?l[ptrs[i]]:'∅';}).join(',') + ' → 最小值 '+minVal+' 来自链表'+(minIdx+1), phase:'compare', heads:ptrs.slice(), minIdx:minIdx, minVal:minVal, result:result.slice()});
result.push(minVal);
ptrs[minIdx]++;
steps.push({stage:'pick', msg:'取 '+minVal+' 加入结果,链表'+(minIdx+1)+'指针前进', phase:'pick', heads:ptrs.slice(), minIdx:minIdx, result:result.slice()});
}
steps.push({stage:'done', msg:'合并完成:[' + result.join(',') + ']', phase:'done', result:result});
}
function render(step) {
var s = steps[step];
var exIdx = parseInt($('exampleSelect').value) || 0;
var ex = examples[exIdx];
var lists = ex.lists.filter(function(l){return l.length>0;});
var viz = '';
var hl, ptrs2;
for (var li = 0; li < lists.length; li++) {
hl = {};
ptrs2 = {};
if (s.heads && s.heads[li] < lists[li].length) {
hl[s.heads[li]] = (s.minIdx === li) ? 'active' : 'current';
ptrs2['L'+(li+1)] = s.heads[li];
}
viz += llViz(lists[li], {highs:hl, ptrs:ptrs2, label:'链表 '+(li+1), showNull:false});
}
if (s.result && s.result.length > 0) {
viz += llViz(s.result, {highlights:{[s.result.length-1]:'active'}, label:'合并结果', showNull:false});
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">合并结果:<b>[' + s.result.join(' → ') + ']</b></div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','compare\u2192比较','pick\u2192选取','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = '[[1,4,5],[1,3,4],[2,6]]';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('import heapq\ndef mergeKLists(lists):\n heap = []\n for i, node in enumerate(lists):\n if node:\n heapq.heappush(heap, (node.val, i, node))\n dummy = ListNode(0)\n cur = dummy\n while heap:\n val, i, node = heapq.heappop(heap)\n cur.next = node\n cur = cur.next\n if node.next:\n heapq.heappush(heap, (node.next.val, i, node.next))\n return dummy.next', {lang:'Python'});
'''
def js_lru_cache():
return _LLVIZ + _PIPELINE_HELPER + r'''
var examples = [
{ops:['LRUCache(2)','put(1,1)','put(2,2)','get(1)','put(3,3)','get(2)','put(4,4)','get(1)','get(3)','get(4)'], label:'示例1: capacity=2'},
{ops:['LRUCache(3)','put(1,1)','put(2,2)','put(3,3)','get(1)','put(4,4)','get(2)'], label:'示例2: capacity=3'},
];
var steps, stepCtrl;
function buildSteps(ex) {
steps = [];
var cap = 2;
var ops = ex.ops;
var capMatch = ops[0].match(/LRUCache\((\d+)\)/);
if (capMatch) cap = parseInt(capMatch[1]);
// DLL: head <-> ... <-> tail (head=most recent, tail=least recent)
var dll = []; // [{key, val}]
var map = {}; // key -> index in dll
var capacity = cap;
steps.push({stage:'init', msg:'LRU 缓存初始化,capacity=' + cap + '。双向链表(头=最近,尾=最久)+ 哈希表', dll:[], map:{}, capacity:capacity, op:ops[0]});
for (var opIdx = 1; opIdx < ops.length; opIdx++) {
var op = ops[opIdx];
var getMatch = op.match(/get\((\d+)\)/);
var putMatch = op.match(/put\((\d+),(\d+)\)/);
if (getMatch) {
var key = getMatch[1];
if (key in map) {
// hit: move to front
var idx = map[key];
var node = dll[idx];
dll.splice(idx, 1);
dll.unshift(node);
// rebuild map
map = {};
for (var mi = 0; mi < dll.length; mi++) map[dll[mi].key] = mi;
steps.push({stage:'hit', msg:'get('+key+') 命中!返回 '+node.val+',移到链表头部', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op, hitVal:node.val});
} else {
steps.push({stage:'miss', msg:'get('+key+') 未命中,返回 -1', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op, hitVal:-1});
}
} else if (putMatch) {
var key = putMatch[1];
var val = putMatch[2];
if (key in map) {
// update: move to front
var idx = map[key];
dll[idx].val = val;
var node = dll[idx];
dll.splice(idx, 1);
dll.unshift(node);
map = {};
for (var mi = 0; mi < dll.length; mi++) map[dll[mi].key] = mi;
steps.push({stage:'update', msg:'put('+key+','+val+') 更新已有key,移到头部', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op});
} else {
if (dll.length >= capacity) {
// evict LRU (tail)
var evicted = dll.pop();
delete map[evicted.key];
steps.push({stage:'evict', msg:'put('+key+','+val+') 容量已满,淘汰尾部 key='+evicted.key+' (val='+evicted.val+')', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op, evicted:evicted.key});
}
dll.unshift({key:key, val:val});
map = {};
for (var mi = 0; mi < dll.length; mi++) map[dll[mi].key] = mi;
steps.push({stage:'insert', msg:'put('+key+','+val+') 插入到头部', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity, op:op});
}
}
}
steps.push({stage:'done', msg:'操作完毕', dll:JSON.parse(JSON.stringify(dll)), map:JSON.parse(JSON.stringify(map)), capacity:capacity});
}
function renderDLL(dll) {
if (!dll || dll.length === 0) return '<div style="color:var(--text-muted);font-style:italic;padding:8px 0;">[空]</div>';
var html = '<div style="display:flex;flex-wrap:wrap;align-items:center;gap:2px;padding:4px 0;">';
html += '<div style="padding:2px 6px;font-size:11px;font-weight:700;color:var(--green);">HEAD</div>';
for (var i = 0; i < dll.length; i++) {
var n = dll[i];
var isHead = (i === 0);
var isTail = (i === dll.length - 1);
var bg = isHead ? 'var(--green-light)' : isTail ? 'var(--orange-light)' : 'var(--blue-light)';
var border = isHead ? 'var(--green)' : isTail ? 'var(--orange)' : 'var(--blue)';
html += '<div style="border:2px solid '+border+';border-radius:8px;overflow:hidden;display:inline-flex;">';
html += '<div style="min-width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;padding:0 6px;background:'+bg+';color:var(--text);">k'+n.key+'</div>';
html += '<div style="min-width:24px;height:32px;display:flex;align-items:center;justify-content:center;font-size:12px;padding:0 4px;background:#f8fafc;border-left:1px solid var(--border);">v'+n.val+'</div>';
html += '</div>';
if (i < dll.length - 1) html += '<span style="color:var(--text-muted);">&harr;</span>';
}
html += '<div style="padding:2px 6px;font-size:11px;font-weight:700;color:var(--orange);">TAIL</div>';
html += '</div>';
return html;
}
function render(step) {
var s = steps[step];
var viz = '<div style="margin-bottom:6px;font-size:14px;font-weight:600;color:var(--text-secondary);">当前操作:<code>' + s.op + '</code></div>';
viz += renderDLL(s.dll);
// Show map
if (s.map && Object.keys(s.map).length > 0) {
viz += '<div style="margin-top:8px;font-size:13px;"><b>HashMap:</b>';
Object.keys(s.map).forEach(function(k) {
viz += '<code>'+k+'→idx'+s.map[k]+'</code> ';
});
viz += '</div>';
}
viz += '<div style="margin-top:6px;font-size:12px;color:var(--text-muted);">容量: ' + s.dll.length + '/' + s.capacity + '</div>';
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = '<div class="calc-block">' + s.msg + '</div>';
if (s.hitVal !== undefined) {
$('detailContent').innerHTML += '<div class="current-answer">返回值:<b>' + s.hitVal + '</b></div>';
}
if (s.stage === 'done') {
$('resultContent').innerHTML = '<div class="final-answer">缓存最终状态:[' + s.dll.map(function(n){return n.key+':'+n.val;}).join(', ') + ']</div>';
}
$('hintText').textContent = s.msg;
setPipeline(['init\u2192初始化','hit\u2192命中','miss\u2192未命中','update\u2192更新','evict\u2192淘汰','insert\u2192插入','done\u2192完成'], s.stage);
}
function init() {
var sel = $('exampleSelect');
examples.forEach(function(e,i){ sel.innerHTML += '<option value="'+i+'">'+e.label+'</option>'; });
$('inputArea').value = 'LRUCache(2) put(1,1) put(2,2) get(1) put(3,3) get(2)';
buildSteps(examples[0]);
stepCtrl = new StepController({onStep:render});
stepCtrl.setSteps(steps.map(function(_,i){return i;}));
$('stepInfo').textContent = '步骤 1 / ' + steps.length;
stepCtrl.onStep = function(idx){ render(idx); $('stepInfo').textContent = '步骤 '+(idx+1)+' / '+steps.length; };
$('applyBtn').onclick = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('exampleSelect').onchange = function(){ buildSteps(examples[parseInt($('exampleSelect').value)]); stepCtrl.setSteps(steps.map(function(_,i){return i;})); render(0); };
$('prevBtn').onclick = function(){ stepCtrl.prev(); };
$('nextBtn').onclick = function(){ stepCtrl.next(); };
$('jumpBtn').onclick = function(){ stepCtrl.jumpToEnd(); };
$('autoBtn').onclick = function(){ var on=stepCtrl.toggleAuto(); $('autoBtn').textContent=on?'暂停':'自动播放'; };
$('resetBtn').onclick = function(){ stepCtrl.reset(); $('autoBtn').textContent='自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode('class LRUCache:\n def __init__(self, capacity):\n self.cap = capacity\n self.cache = {}\n self.head = DLinkedNode()\n self.tail = DLinkedNode()\n self.head.next = self.tail\n self.tail.prev = self.head\n\n def _remove(self, node):\n node.prev.next = node.next\n node.next.prev = node.prev\n\n def _add_to_head(self, node):\n node.prev = self.head\n node.next = self.head.next\n self.head.next.prev = node\n self.head.next = node\n\n def get(self, key):\n if key not in self.cache:\n return -1\n node = self.cache[key]\n self._remove(node)\n self._add_to_head(node)\n return node.val\n\n def put(self, key, value):\n if key in self.cache:\n node = self.cache[key]\n node.val = value\n self._remove(node)\n self._add_to_head(node)\n else:\n if len(self.cache) >= self.cap:\n tail = self.tail.prev\n self._remove(tail)\n del self.cache[tail.key]\n node = DLinkedNode(key, value)\n self.cache[key] = node\n self._add_to_head(node)', {lang:'Python'});
'''