1271 lines
71 KiB
Python
1271 lines
71 KiB
Python
"""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;">↩</span>';
|
||
} else if (i < nodes.length - 1) {
|
||
html += '<span class="arrow">→</span>';
|
||
} else if (showNull && i === nodes.length - 1) {
|
||
html += '<span class="arrow" style="color:var(--text-muted);">∅</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);">→</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);">↔</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'});
|
||
'''
|