This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>022. 相交链表 – 图解</title>
|
||||
<link rel="stylesheet" href="../shared/style.css">
|
||||
<style>
|
||||
.vis-area{min-height:200px;padding:16px 0}
|
||||
.code-section{margin-top:16px}
|
||||
|
||||
/* ---- Y-shape layout ---- */
|
||||
.y-list{display:inline-flex;flex-direction:column;padding:8px 0;gap:0}
|
||||
.y-row{display:flex;align-items:center;gap:0;flex-wrap:nowrap;min-height:54px}
|
||||
.y-head{font-size:13px;font-weight:700;min-width:60px;text-align:right;margin-right:6px;white-space:nowrap;flex-shrink:0}
|
||||
.y-head.ha{color:var(--blue)}
|
||||
.y-head.hb{color:var(--orange)}
|
||||
.y-spacer{flex-shrink:0}
|
||||
.y-conn{font-size:22px;color:var(--purple);font-weight:700;flex-shrink:0;margin:0 4px;line-height:1}
|
||||
|
||||
/* Node wrapper for pointer labels */
|
||||
.nw{position:relative;display:inline-flex;flex-direction:column;align-items:center;flex-shrink:0}
|
||||
.nw .ptr{display:flex;flex-direction:column;align-items:center;font-size:10px;font-weight:700;margin-top:2px;line-height:1.2;gap:1px}
|
||||
.nw .ptr-pa{color:var(--blue)}
|
||||
.nw .ptr-pb{color:var(--orange)}
|
||||
.nw .ptr-ph{height:14px}
|
||||
|
||||
/* Shared / intersection highlight */
|
||||
.ll-node.is-intersect{border-color:var(--purple);box-shadow:0 0 0 3px rgba(139,92,246,.22)}
|
||||
.ll-node.is-intersect .val{background:#ede9fe;color:var(--purple-dark)}
|
||||
.ll-node.is-shared{border-color:#a78bfa}
|
||||
.ll-node.is-shared .val{background:#f5f3ff;color:#6d28d9}
|
||||
.ll-node.is-meet{border-color:var(--green)!important;box-shadow:0 0 0 4px rgba(22,163,74,.25)!important}
|
||||
.ll-node.is-meet .val{background:#dcfce7!important;color:var(--green-dark)!important}
|
||||
|
||||
/* null indicator in Y-list */
|
||||
.null-terminal{display:inline-flex;align-items:center;flex-shrink:0}
|
||||
|
||||
/* ---- Walk tracker ---- */
|
||||
.walk-wrap{margin-top:18px;padding:12px;background:#f8fafc;border-radius:12px;border:1px solid var(--border);overflow-x:auto}
|
||||
.walk-title{font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:8px}
|
||||
.walk-row{display:flex;align-items:center;gap:0;margin:6px 0;flex-wrap:nowrap}
|
||||
.walk-tag{font-size:12px;font-weight:700;min-width:26px;margin-right:6px;flex-shrink:0}
|
||||
.walk-tag.wta{color:var(--blue)}
|
||||
.walk-tag.wtb{color:var(--orange)}
|
||||
.wc{
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
min-width:30px;height:26px;padding:1px 5px;border-radius:6px;
|
||||
font-size:11px;font-weight:600;font-family:monospace;transition:all .2s;
|
||||
background:#e2e8f0;color:var(--text);margin:0 1px;
|
||||
}
|
||||
.wc.wc-null{background:#f1f5f9;color:var(--text-muted);font-style:italic}
|
||||
.wc.wc-sh{border-bottom:2px solid var(--purple)}
|
||||
.wc.wc-sw{border-top:2px dashed #94a3b8}
|
||||
.wc.wc-a{box-shadow:0 0 0 2px var(--blue);background:#dbeafe;color:var(--blue-dark)}
|
||||
.wc.wc-b{box-shadow:0 0 0 2px var(--orange);background:#ffedd5;color:#9a3412}
|
||||
.wc.wc-meet{box-shadow:0 0 0 2px var(--green);background:#bbf7d0;color:var(--green-dark);font-weight:800}
|
||||
.ws{font-size:9px;color:var(--purple);font-weight:700;margin:0 2px}
|
||||
|
||||
/* ---- Info chips ---- */
|
||||
.info-row{display:flex;gap:10px;flex-wrap:wrap;margin:10px 0 4px;font-size:13px}
|
||||
.ic{padding:3px 10px;border-radius:999px;font-weight:600;font-size:12px}
|
||||
.ic-a{background:#dbeafe;color:var(--blue-dark)}
|
||||
.ic-b{background:#ffedd5;color:#9a3412}
|
||||
.ic-s{background:#ede9fe;color:var(--purple-dark)}
|
||||
.ic-g{background:#dcfce7;color:var(--green-dark)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🟢 022. 相交链表 <span class="badge easy">简单</span></h1>
|
||||
<p class="subtitle">分类:链表 | LeetCode 160 · Hot 100 #022</p>
|
||||
|
||||
<div class="controls" id="controls">
|
||||
<label for="inputArea">输入:</label>
|
||||
<input type="text" id="inputArea" placeholder="listA=[4,1,8,4,5], listB=[5,6,1,8,4,5], intersect=8">
|
||||
<button id="applyBtn" class="primary">生成图解</button>
|
||||
<select id="exampleSelect"></select>
|
||||
<span style="flex:1"></span>
|
||||
<button id="prevBtn">◀ 上一步</button>
|
||||
<button id="nextBtn">下一步 ▶</button>
|
||||
<button id="jumpBtn">⏭ 跳到结果</button>
|
||||
<button id="autoBtn">自动播放</button>
|
||||
<button id="resetBtn">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="pipeline" id="pipeline"></div>
|
||||
|
||||
<div class="hint info" id="hintBox">
|
||||
<span id="stepInfo"></span><br>
|
||||
<span id="hintText"></span>
|
||||
</div>
|
||||
|
||||
<div class="panels">
|
||||
<div class="panel" id="mainPanel">
|
||||
<h3>📊 可视化</h3>
|
||||
<div class="vis-area" id="vizArea">点击「生成图解」开始</div>
|
||||
</div>
|
||||
<div class="panel-grid">
|
||||
<div class="panel" id="detailPanel">
|
||||
<h3>📝 当前步骤详情</h3>
|
||||
<div id="detailContent">等待开始...</div>
|
||||
</div>
|
||||
<div class="panel" id="resultPanel">
|
||||
<h3>✅ 结果</h3>
|
||||
<div id="resultContent">等待完成...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel code-section">
|
||||
<h3>💻 参考代码(Python)</h3>
|
||||
<div id="codeArea"></div>
|
||||
</div>
|
||||
|
||||
<footer>Powered by QwenPaw · 图解算法 · LeetCode Hot 100</footer>
|
||||
</div>
|
||||
|
||||
<script src="../shared/algo-viz.js"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
(function(){
|
||||
|
||||
/* ================================================================
|
||||
LeetCode 160 · 相交链表 · 双指针可视化
|
||||
算法:pA 走 listA 再走 listB,pB 走 listB 再走 listA,
|
||||
两者总步数相同 (m+n+2),必在交点或 null 相遇。
|
||||
================================================================ */
|
||||
|
||||
var PIPE = ['init→初始化','step→步进','null→抵达末尾','switch→切换链表','meet→相遇'];
|
||||
|
||||
var examples = [
|
||||
{la:[4,1,8,4,5], lb:[5,6,1,8,4,5], iv:8, lbl:'示例1: intersect=8'},
|
||||
{la:[1,9,1,2,4], lb:[3,2,4], iv:2, lbl:'示例2: intersect=2'},
|
||||
{la:[2,6,4], lb:[1,5], iv:null, lbl:'示例3: 不相交'}
|
||||
];
|
||||
|
||||
var gLA, gLB, gSA, gSB, gHI;
|
||||
var steps, ctrl;
|
||||
|
||||
// ────────── parse & compute ──────────
|
||||
|
||||
function parseInput(s){
|
||||
var a=s.match(/listA=\[([^\]]*)\]/), b=s.match(/listB=\[([^\]]*)\]/), v=s.match(/intersect=(\S+)/);
|
||||
if(!a||!b) return null;
|
||||
return {
|
||||
la: a[1]?a[1].split(',').map(Number):[],
|
||||
lb: b[1]?b[1].split(',').map(Number):[],
|
||||
iv: v?(v[1]==='null'||v[1]==='0'?null:Number(v[1])):null
|
||||
};
|
||||
}
|
||||
|
||||
function calcSkips(la,lb,iv){
|
||||
if(iv==null) return {sa:la.length,sb:lb.length,hi:false};
|
||||
var sa=-1;
|
||||
for(var i=0;i<la.length;i++) if(la[i]===iv){sa=i;break;}
|
||||
if(sa===-1) return {sa:la.length,sb:lb.length,hi:false};
|
||||
var tail=la.slice(sa), sb=-1;
|
||||
for(var s=0;s<=lb.length-tail.length;s++){
|
||||
var ok=true;
|
||||
for(var j=0;j<tail.length;j++) if(lb[s+j]!==tail[j]){ok=false;break;}
|
||||
if(ok){sb=s;break;}
|
||||
}
|
||||
if(sb===-1) return {sa:la.length,sb:lb.length,hi:false};
|
||||
return {sa:sa,sb:sb,hi:true};
|
||||
}
|
||||
|
||||
function nid(list,idx,skip,pfx){
|
||||
if(idx>=list.length) return 'null';
|
||||
if(gHI && idx>=skip) return 's'+(idx-skip);
|
||||
return pfx+idx;
|
||||
}
|
||||
|
||||
// ────────── build walk sequences ──────────
|
||||
|
||||
function mkWalk(list,len,skip,pfx,otherList,otherLen,otherSkip,otherPfx){
|
||||
var arr=[];
|
||||
for(var i=0;i<len;i++)
|
||||
arr.push({L:list===gLA?'A':'B',i:i,n:nid(list,i,skip,pfx),v:list[i],nu:false,sw:false});
|
||||
arr.push({L:list===gLA?'A':'B',i:len,n:'null',v:null,nu:true,sw:false});
|
||||
var swAt=arr.length-1;
|
||||
var oL=otherList===gLA?'A':'B';
|
||||
for(var i=0;i<otherLen;i++)
|
||||
arr.push({L:oL,i:i,n:nid(otherList,i,otherSkip,otherPfx),v:otherList[i],nu:false,sw:true});
|
||||
arr.push({L:oL,i:otherLen,n:'null2',v:null,nu:true,sw:true});
|
||||
return {arr:arr,swAt:swAt};
|
||||
}
|
||||
|
||||
// ────────── step generation ──────────
|
||||
|
||||
function buildSteps(){
|
||||
steps=[];
|
||||
var la=gLA,lb=gLB,sA=gSA,sB=gSB,hi=gHI;
|
||||
var wA=mkWalk(la,la.length,sA,'a', lb,lb.length,sB,'b');
|
||||
var wB=mkWalk(lb,lb.length,sB,'b', la,la.length,sA,'a');
|
||||
|
||||
// Step 0: init
|
||||
var a0=wA.arr[0],b0=wB.arr[0];
|
||||
steps.push({
|
||||
stg:'init', wi:0, pa:a0, pb:b0,
|
||||
msg:'初始化:pA = headA → node '+a0.n+'(val='+a0.v+'),pB = headB → node '+b0.n+'(val='+b0.v+')',
|
||||
cl:4
|
||||
});
|
||||
if(a0.n===b0.n){steps[0].stg='meet';steps[0].msg='两指针从同一节点出发 → 直接相遇';return steps;}
|
||||
|
||||
var maxW=Math.max(wA.arr.length,wB.arr.length);
|
||||
for(var w=1;w<maxW;w++){
|
||||
if(w>=wA.arr.length||w>=wB.arr.length) break;
|
||||
var a=wA.arr[w], b=wB.arr[w];
|
||||
var pa=wA.arr[w-1], pb=wB.arr[w-1];
|
||||
var met=(a.n===b.n);
|
||||
var aRn=a.nu&&!pa.nu; // pA just reached null
|
||||
var bRn=b.nu&&!pb.nu;
|
||||
var aSw=a.sw&&!pa.sw; // pA just switched lists
|
||||
var bSw=b.sw&&!pb.sw;
|
||||
|
||||
var stg,msg,cl;
|
||||
|
||||
if(met){
|
||||
stg='meet'; cl=7;
|
||||
msg=a.nu
|
||||
?'pA 和 pB 都到达 null → 两链表不相交,返回 null'
|
||||
:'pA 和 pB 在 node '+a.n+'(val='+a.v+') 相遇 → 找到交点!';
|
||||
} else if(aRn&&bRn){
|
||||
stg='null'; cl=5;
|
||||
msg='pA 抵达 listA 末尾(null),pB 抵达 listB 末尾(null)';
|
||||
} else if(aRn&&bSw){
|
||||
stg='null'; cl=5;
|
||||
msg='pA 抵达 '+a.L+' 末尾(null),pB 从 null 切换到 head'+
|
||||
(b.L==='A'?'A':'B')+'(node '+b.n+', val='+b.v+')';
|
||||
} else if(bRn&&aSw){
|
||||
stg='null'; cl=6;
|
||||
msg='pB 抵达 '+b.L+' 末尾(null),pA 从 null 切换到 head'+
|
||||
(a.L==='B'?'B':'A')+'(node '+a.n+', val='+a.v+')';
|
||||
} else if(aRn){
|
||||
stg='null'; cl=5;
|
||||
msg='pA 抵达 '+pa.L+' 末尾(null),pB 在 '+b.L+'['+b.i+']=node '+b.n+'(val='+b.v+')';
|
||||
} else if(bRn){
|
||||
stg='null'; cl=6;
|
||||
msg='pB 抵达 '+pb.L+' 末尾(null),pA 在 '+a.L+'['+a.i+']=node '+a.n+'(val='+a.v+')';
|
||||
} else if(aSw&&bSw){
|
||||
stg='switch'; cl=5;
|
||||
msg='pA 切换到 headB(node '+a.n+', val='+a.v+'),pB 切换到 headA(node '+b.n+', val='+b.v+')';
|
||||
} else if(aSw){
|
||||
stg='switch'; cl=5;
|
||||
msg='pA 从 null 切换到 headB(node '+a.n+', val='+a.v+'),pB 走到 '+b.L+'['+b.i+']=node '+b.n+'(val='+b.v+')';
|
||||
} else if(bSw){
|
||||
stg='switch'; cl=6;
|
||||
msg='pB 从 null 切换到 headA(node '+b.n+', val='+b.v+'),pA 走到 '+a.L+'['+a.i+']=node '+a.n+'(val='+a.v+')';
|
||||
} else {
|
||||
stg='step'; cl=5;
|
||||
msg='pA: '+a.L+'['+a.i+']='+a.v+' (node '+a.n+'),pB: '+b.L+'['+b.i+']='+b.v+' (node '+b.n+'),pA ≠ pB';
|
||||
}
|
||||
steps.push({stg:stg,wi:w,pa:a,pb:b,met:met,msg:msg,cl:cl});
|
||||
if(stg==='meet') break;
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
// ────────── render: node helper ──────────
|
||||
|
||||
function rNode(val, cls, ptrs, arrowOverride){
|
||||
var html='<div class="nw">';
|
||||
html+='<div class="ll-node '+cls+'"><span class="val">'+val+'</span>';
|
||||
html+='<span class="arrow">'+(arrowOverride||'→')+'</span></div>';
|
||||
if(ptrs&&ptrs.length){
|
||||
html+='<div class="ptr">';
|
||||
for(var p=0;p<ptrs.length;p++){
|
||||
html+=ptrs[p]==='pA'
|
||||
?'<span class="ptr-pa">▼pA</span>'
|
||||
:'<span class="ptr-pb">▼pB</span>';
|
||||
}
|
||||
html+='</div>';
|
||||
} else {
|
||||
html+='<div class="ptr-ph"></div>';
|
||||
}
|
||||
html+='</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// ────────── render: Y-shape linked list ──────────
|
||||
|
||||
function renderYList(pa,pb){
|
||||
var la=gLA,lb=gLB,sA=gSA,sB=gSB,hi=gHI;
|
||||
var aExcl=la.slice(0,sA), bExcl=lb.slice(0,sB), shared=hi?la.slice(sA):[];
|
||||
|
||||
if(!hi){
|
||||
// ── No intersection: two complete rows ──
|
||||
return renderTwoRows(la,lb,pa,pb);
|
||||
}
|
||||
|
||||
// ── Y-shape: three segments ──
|
||||
var unitPx=104, headPx=66;
|
||||
var aBW=headPx+aExcl.length*unitPx;
|
||||
var bBW=headPx+bExcl.length*unitPx;
|
||||
var maxBW=Math.max(aBW,bBW);
|
||||
var sharedML=maxBW+10;
|
||||
var aPad=Math.max(0,bBW-aBW);
|
||||
var bPad=Math.max(0,aBW-bBW);
|
||||
|
||||
var h='<div class="y-list">';
|
||||
|
||||
// ── Branch A ──
|
||||
h+='<div class="y-row">';
|
||||
h+='<span class="y-head ha">headA →</span>';
|
||||
for(var i=0;i<aExcl.length;i++){
|
||||
var nd='a'+i;
|
||||
var isPA=pa&&!pa.nu&&pa.L==='A'&&pa.i===i;
|
||||
var isPB=pb&&!pb.nu&&pb.L==='A'&&pb.i===i&&pb.n===nd;
|
||||
var ptrs=[]; if(isPA) ptrs.push('pA'); if(isPB) ptrs.push('pB');
|
||||
h+=rNode(aExcl[i],isPA||isPB?'current':'',ptrs);
|
||||
}
|
||||
if(aPad>0) h+='<span class="y-spacer" style="width:'+aPad+'px"></span>';
|
||||
h+='<span class="y-conn">↘</span>';
|
||||
h+='</div>';
|
||||
|
||||
// ── Shared row ──
|
||||
h+='<div class="y-row" style="margin-left:'+sharedML+'px">';
|
||||
for(var i=0;i<shared.length;i++){
|
||||
var nd='s'+i;
|
||||
var isPA=pa&&!pa.nu&&pa.n===nd;
|
||||
var isPB=pb&&!pb.nu&&pb.n===nd;
|
||||
var cls=i===0?'is-intersect':'is-shared';
|
||||
if(isPA&&isPB) cls+=' is-meet';
|
||||
else if(isPA||isPB) cls+=' current';
|
||||
var ptrs=[]; if(isPA) ptrs.push('pA'); if(isPB) ptrs.push('pB');
|
||||
var isLast=i===shared.length-1;
|
||||
h+=rNode(shared[i],cls,ptrs,isLast?'∅':'→');
|
||||
}
|
||||
if(shared.length===0){
|
||||
h+='<div class="nw"><div class="ll-node null-node"><span class="val">∅</span></div><div class="ptr-ph"></div></div>';
|
||||
}
|
||||
h+='</div>';
|
||||
|
||||
// ── Branch B ──
|
||||
h+='<div class="y-row">';
|
||||
h+='<span class="y-head hb">headB →</span>';
|
||||
for(var i=0;i<bExcl.length;i++){
|
||||
var nd='b'+i;
|
||||
var isPA=pa&&!pa.nu&&pa.L==='B'&&pa.i===i&&pa.n===nd;
|
||||
var isPB=pb&&!pb.nu&&pb.L==='B'&&pb.i===i&&pb.n===nd;
|
||||
var ptrs=[]; if(isPA) ptrs.push('pA'); if(isPB) ptrs.push('pB');
|
||||
h+=rNode(bExcl[i],isPA||isPB?'current':'',ptrs);
|
||||
}
|
||||
if(bPad>0) h+='<span class="y-spacer" style="width:'+bPad+'px"></span>';
|
||||
h+='<span class="y-conn">↗</span>';
|
||||
h+='</div>';
|
||||
|
||||
h+='</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
// ── No intersection: two full rows ──
|
||||
function renderTwoRows(la,lb,pa,pb){
|
||||
var h='<div class="y-list">';
|
||||
// ListA
|
||||
h+='<div class="y-row">';
|
||||
h+='<span class="y-head ha">headA →</span>';
|
||||
for(var i=0;i<la.length;i++){
|
||||
var isPA=pa&&!pa.nu&&pa.L==='A'&&pa.i===i;
|
||||
var isPB=pb&&!pb.nu&&pb.L==='A'&&pb.i===i;
|
||||
var ptrs=[]; if(isPA) ptrs.push('pA'); if(isPB) ptrs.push('pB');
|
||||
h+=rNode(la[i],isPA||isPB?'current':'',ptrs,i===la.length-1?'∅':'→');
|
||||
}
|
||||
h+='</div>';
|
||||
// ListB
|
||||
h+='<div class="y-row" style="margin-top:12px">';
|
||||
h+='<span class="y-head hb">headB →</span>';
|
||||
for(var i=0;i<lb.length;i++){
|
||||
var isPA=pa&&!pa.nu&&pa.L==='B'&&pa.i===i;
|
||||
var isPB=pb&&!pb.nu&&pb.L==='B'&&pb.i===i;
|
||||
var ptrs=[]; if(isPA) ptrs.push('pA'); if(isPB) ptrs.push('pB');
|
||||
h+=rNode(lb[i],isPA||isPB?'current':'',ptrs,i===lb.length-1?'∅':'→');
|
||||
}
|
||||
h+='</div>';
|
||||
h+='</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
// ────────── render: walk path tracker ──────────
|
||||
|
||||
function renderWalk(pa,pb,wi){
|
||||
var la=gLA,lb=gLB,sA=gSA,sB=gSB,hi=gHI;
|
||||
|
||||
var wADat=mkWalk(la,la.length,sA,'a', lb,lb.length,sB,'b');
|
||||
var wBDat=mkWalk(lb,lb.length,sB,'b', la,la.length,sA,'a');
|
||||
var wA=wADat.arr, wB=wBDat.arr;
|
||||
var swA=wADat.swAt, swB=wBDat.swAt;
|
||||
|
||||
function chip(c,i,tag){
|
||||
var cls='wc';
|
||||
if(c.nu) cls+=' wc-null';
|
||||
if(!c.nu&&c.sh()) cls+=' wc-sh';
|
||||
if(c.sw) cls+=' wc-sw';
|
||||
if(i===wi){
|
||||
var bothMet=pa&&pb&&pa.n===pb.n&&!pa.nu;
|
||||
if(tag==='pA') cls+=bothMet?' wc-meet':' wc-a';
|
||||
else cls+=bothMet?' wc-meet':' wc-b';
|
||||
}
|
||||
return '<span class="'+cls+'" title="node '+c.n+'">'+c.v+'</span>';
|
||||
}
|
||||
// add .sh() method to walk items
|
||||
wA.forEach(function(x){ x.sh=function(){return hi&&x.n.charAt(0)==='s';}; });
|
||||
wB.forEach(function(x){ x.sh=function(){return hi&&x.n.charAt(0)==='s';}; });
|
||||
|
||||
function row(arr,swAt,tag){
|
||||
var h='<div class="walk-row"><span class="walk-tag '+(tag==='pA'?'wta':'wtb')+'">'+tag+'</span>';
|
||||
for(var i=0;i<arr.length;i++){
|
||||
if(i===swAt+1) h+='<span class="ws">│</span>';
|
||||
h+=chip(arr[i],i,tag);
|
||||
}
|
||||
h+='</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
var h='<div class="walk-wrap">';
|
||||
h+='<div class="walk-title">🚶 指针行走路径 <span style="margin-left:10px;font-size:11px;color:var(--text-muted)">│=切换链表分界 紫色下划线=共享节点</span></div>';
|
||||
h+=row(wA,swA,'pA');
|
||||
h+=row(wB,swB,'pB');
|
||||
|
||||
// Step counter
|
||||
if(wi>0){
|
||||
var totalA=la.length+1+lb.length+1, totalB=lb.length+1+la.length+1;
|
||||
h+='<div style="margin-top:4px;font-size:11px;color:var(--text-secondary)">';
|
||||
h+='pA 已走 '+Math.min(wi+1,totalA)+' / '+totalA+' 步';
|
||||
h+=' · ';
|
||||
var paOnB=wi>la.length;
|
||||
var pbOnA=wi>lb.length;
|
||||
if(paOnB) h+='<span style="color:var(--orange)">pA 在 listB 上</span>';
|
||||
else h+='pA 在 listA 上';
|
||||
h+=' · ';
|
||||
if(pbOnA) h+='<span style="color:var(--blue)">pB 在 listA 上</span>';
|
||||
else h+='pB 在 listB 上';
|
||||
h+='</div>';
|
||||
}
|
||||
h+='</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
// ────────── render: info chips ──────────
|
||||
|
||||
function renderInfo(pa,pb){
|
||||
var h='<div class="info-row">';
|
||||
if(pa){
|
||||
if(pa.nu) h+='<span class="ic ic-a">pA = null</span>';
|
||||
else h+='<span class="ic ic-a">pA → '+pa.L+'['+pa.i+'] = node '+pa.n+'</span>';
|
||||
}
|
||||
if(pb){
|
||||
if(pb.nu) h+='<span class="ic ic-b">pB = null</span>';
|
||||
else h+='<span class="ic ic-b">pB → '+pb.L+'['+pb.i+'] = node '+pb.n+'</span>';
|
||||
}
|
||||
if(pa&&pb&&!pa.nu&&!pb.nu&&pa.n===pb.n) h+='<span class="ic ic-g">pA == pB ✓</span>';
|
||||
if(pa&&pb&&pa.nu&&pb.nu) h+='<span class="ic ic-g">pA == pB (null) ✓</span>';
|
||||
h+='</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
// ────────── pipeline ──────────
|
||||
|
||||
function setPipeline(cur){
|
||||
$('pipeline').innerHTML=PIPE.map(function(s){
|
||||
var p=s.split('→');
|
||||
return '<span class="pipe-step '+(cur===p[0]?'active':'')+'">'+p[1]+'</span>';
|
||||
}).join('<i>→</i>');
|
||||
}
|
||||
|
||||
// ────────── main render ──────────
|
||||
|
||||
function render(idx){
|
||||
if(!steps||!steps[idx]) return;
|
||||
var s=steps[idx];
|
||||
|
||||
// Viz area
|
||||
var viz=renderYList(s.pa,s.pb);
|
||||
viz+=renderWalk(s.pa,s.pb,s.wi||0);
|
||||
viz+=renderInfo(s.pa,s.pb);
|
||||
$('vizArea').innerHTML=viz;
|
||||
|
||||
// Detail panel
|
||||
var d='<div class="calc-block">'+s.msg+'</div>';
|
||||
d+='<div style="margin-top:8px">';
|
||||
d+=s.pa.nu?'<code>pA = null</code>':'<code>pA → '+s.pa.L+'['+s.pa.i+'] = node '+s.pa.n+'</code>';
|
||||
d+=' ';
|
||||
d+=s.pb.nu?'<code>pB = null</code>':'<code>pB → '+s.pb.L+'['+s.pb.i+'] = node '+s.pb.n+'</code>';
|
||||
d+='</div>';
|
||||
if(!s.pa.nu&&!s.pb.nu){
|
||||
d+='<div style="margin-top:6px;font-size:13px">';
|
||||
if(s.pa.n===s.pb.n)
|
||||
d+='<span style="color:var(--green);font-weight:700">pA.nodeId === pB.nodeId → 指向同一内存节点!</span>';
|
||||
else
|
||||
d+='<span style="color:var(--red)">pA.nodeId ('+s.pa.n+') ≠ pB.nodeId ('+s.pb.n+') → 不同节点</span>';
|
||||
d+='</div>';
|
||||
}
|
||||
if(s.stg==='step')
|
||||
d+='<div style="margin-top:8px;font-size:12px;color:var(--text-secondary)">while pA ≠ pB → 继续前进:pA = pA.next, pB = pB.next</div>';
|
||||
else if(s.stg==='null')
|
||||
d+='<div style="margin-top:8px;font-size:12px;color:var(--text-secondary)">指针到达链表末尾 → 下一步将切换到另一链表头部 (pA→headB / pB→headA)</div>';
|
||||
else if(s.stg==='switch')
|
||||
d+='<div style="margin-top:8px;font-size:12px;color:var(--text-secondary)">走完一条链表后切换,保证两指针走的总步数相同 (lenA+lenB)</div>';
|
||||
$('detailContent').innerHTML=d;
|
||||
|
||||
// Result panel
|
||||
if(s.stg==='meet'){
|
||||
if(s.pa.nu){
|
||||
$('resultContent').innerHTML='<div class="final-answer" style="border-color:#f87171;background:#fef2f2">两链表不相交,返回 <b>null</b><br><span style="font-size:13px;color:#64748b">pA 和 pB 各走 lenA+lenB 步后在 null 处相遇</span><br><div class="complexity">时间 O(m+n) · 空间 O(1)</div></div>';
|
||||
} else {
|
||||
$('resultContent').innerHTML='<div class="final-answer">交点节点:<b>node '+s.pa.n+' (val='+s.pa.v+')</b><br><span style="font-size:13px;color:#64748b">经过 '+s.wi+' 步迭代后 pA == pB → 找到交点</span><br><div class="complexity">时间 O(m+n) · 空间 O(1)</div></div>';
|
||||
}
|
||||
} else {
|
||||
$('resultContent').innerHTML='等待完成…';
|
||||
}
|
||||
|
||||
// Hint & counters
|
||||
$('hintText').textContent=s.msg;
|
||||
$('stepInfo').textContent='步骤 '+(idx+1)+' / '+steps.length;
|
||||
setPipeline(s.stg);
|
||||
|
||||
// Code highlight
|
||||
$('codeArea').innerHTML=renderCode(PY,{highlightLines:s.cl?[s.cl]:[],lang:'Python'});
|
||||
}
|
||||
|
||||
// ────────── Python code ──────────
|
||||
|
||||
var PY=[
|
||||
'class Solution:',
|
||||
' def getIntersectionNode(self, headA, headB):',
|
||||
' if not headA or not headB:',
|
||||
' return None',
|
||||
' pA, pB = headA, headB',
|
||||
' while pA != pB:',
|
||||
' pA = pA.next if pA else headB',
|
||||
' pB = pB.next if pB else headA',
|
||||
' return pA'
|
||||
].join('\n');
|
||||
|
||||
// ────────── init ──────────
|
||||
|
||||
function loadExample(idx){
|
||||
var e=examples[idx];
|
||||
gLA=e.la.slice(); gLB=e.lb.slice();
|
||||
var sk=calcSkips(gLA,gLB,e.iv);
|
||||
gSA=sk.sa; gSB=sk.sb; gHI=sk.hi;
|
||||
$('inputArea').value='listA=['+gLA.join(',')+'], listB=['+gLB.join(',')+'], intersect='+(e.iv===null?'null':e.iv);
|
||||
}
|
||||
|
||||
function init(){
|
||||
var sel=$('exampleSelect');
|
||||
examples.forEach(function(e,i){ sel.innerHTML+='<option value="'+i+'">'+e.lbl+'</option>'; });
|
||||
|
||||
loadExample(0);
|
||||
buildSteps();
|
||||
|
||||
ctrl=new StepController({onStep:render,autoInterval:700});
|
||||
ctrl.setSteps(steps.map(function(_,i){return i;}));
|
||||
|
||||
$('applyBtn').onclick=function(){
|
||||
var p=parseInput($('inputArea').value);
|
||||
if(!p){alert('格式: listA=[4,1,8,4,5], listB=[5,6,1,8,4,5], intersect=8');return;}
|
||||
gLA=p.la; gLB=p.lb;
|
||||
var sk=calcSkips(gLA,gLB,p.iv); gSA=sk.sa; gSB=sk.sb; gHI=sk.hi;
|
||||
buildSteps(); ctrl.setSteps(steps.map(function(_,i){return i;})); render(0);
|
||||
};
|
||||
$('exampleSelect').onchange=function(){
|
||||
loadExample(parseInt($('exampleSelect').value));
|
||||
buildSteps(); ctrl.setSteps(steps.map(function(_,i){return i;})); render(0);
|
||||
$('autoBtn').textContent='自动播放';
|
||||
};
|
||||
$('prevBtn').onclick=function(){ctrl.prev();};
|
||||
$('nextBtn').onclick=function(){ctrl.next();};
|
||||
$('jumpBtn').onclick=function(){ctrl.jumpToEnd();};
|
||||
$('autoBtn').onclick=function(){var on=ctrl.toggleAuto();$('autoBtn').textContent=on?'暂停':'自动播放';};
|
||||
$('resetBtn').onclick=function(){ctrl.reset();$('autoBtn').textContent='自动播放';};
|
||||
|
||||
render(0);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user