<p class="hint">Click any node to <b>splay</b> it to the root. The tree rotates until that key sits at the top. Access the same key repeatedly and watch it stay near the root โ the tree learns it is popular.</p>
<div id="tree-wrap">
<svg id="tree-svg" width="560" height="260" viewBox="0 0 560 260"></svg>
</div>
<div class="status" id="status">Click a node to splay it.</div>
<div class="btns">
<button id="reset" type="button" class="ghost">Reset tree</button>
<button id="seq" type="button">Access 1, 1, 1, 8, 8, 15</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#tree-wrap { width: 100%; overflow-x: auto; }
#tree-svg { display: block; }
.edge { stroke: #adb1b8; stroke-width: 1.5; fill: none; }
.node circle { stroke-width: 2; cursor: pointer; transition: r .15s; }
.node circle.cold { fill: #c9ccd1; stroke: #adb1b8; }
.node circle.warm { fill: #f4a261; stroke: #e07b3c; }
.node circle.hot { fill: #e63946; stroke: #c92f3c; }
.node circle:hover { r: 18; }
.node text { font: 700 13px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.node circle.hot ~ text { fill: #fff; }
.node circle.warm ~ text { fill: #fff; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0 .3rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Splay tree demo โ vanilla JS, no imports.
// Nodes hold: key, left, right, parent. The splay() method brings a node to root via zig, zig-zig (same side) and zig-zag (opposite side) rotations.
// Heat tracks access frequency so we can colour cold/warm/hot nodes.
// โโ BST node โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function Node(key){ this.key=key; this.left=this.right=this.parent=null; this.heat=0; }
// โโ Splay tree โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
var root = null;
function setParent(node, par){ if(node) node.parent = par; }
function rotateRight(x){
var y = x.left; if(!y) return;
x.left = y.right; setParent(y.right, x);
y.parent = x.parent;
if(!x.parent) root = y;
else if(x === x.parent.left) x.parent.left = y;
else x.parent.right = y;
y.right = x; x.parent = y;
}
function rotateLeft(x){
var y = x.right; if(!y) return;
x.right = y.left; setParent(y.left, x);
y.parent = x.parent;
if(!x.parent) root = y;
else if(x === x.parent.left) x.parent.left = y;
else x.parent.right = y;
y.left = x; x.parent = y;
}
function splay(x){
while(x.parent){
var p = x.parent, g = p.parent;
if(!g){
// zig
if(x === p.left) rotateRight(p); else rotateLeft(p);
} else if(x === p.left && p === g.left){
// zig-zig left
rotateRight(g); rotateRight(p);
} else if(x === p.right && p === g.right){
// zig-zig right
rotateLeft(g); rotateLeft(p);
} else if(x === p.left && p === g.right){
// zig-zag
rotateRight(p); rotateLeft(g);
} else {
// zig-zag
rotateLeft(p); rotateRight(g);
}
}
root = x;
}
function insert(key){
var n = new Node(key);
if(!root){ root = n; return; }
var cur = root, par = null;
while(cur){ par = cur; if(key < cur.key) cur = cur.left; else cur = cur.right; }
n.parent = par;
if(key < par.key) par.left = n; else par.right = n;
}
function find(key){
var cur = root;
while(cur){ if(key === cur.key) return cur; cur = key < cur.key ? cur.left : cur.right; }
return null;
}
// โโ Build initial tree by inserting 1..15 in order (gives a right-leaning chain, which makes the first splay dramatic). โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildTree(){
root = null;
// Insert in a shuffled order to get a reasonable starting shape
var keys = [8,4,12,2,6,10,14,1,3,5,7,9,11,13,15];
keys.forEach(function(k){ insert(k); });
// Reset heat
var n = find(8); // touch root area
}
// โโ Access: splay the node and increase its heat โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function access(key, skipRender){
var n = find(key);
if(!n) return;
splay(n);
n.heat = Math.min(n.heat + 1, 5);
if(!skipRender) render(key);
return n;
}
// โโ Heat class โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function heatClass(h){ if(h >= 3) return 'hot'; if(h >= 1) return 'warm'; return 'cold'; }
// โโ Layout: in-order traversal assigns x positions; depth gives y โโโโโโโโโโโโโ
var SVG_W = 560, SVG_H = 260, RADIUS = 15, LEVEL_H = 52;
function layout(node, depth, counter){
if(!node) return;
layout(node.left, depth+1, counter);
node._x = (counter[0]++) * (SVG_W / 16) + (SVG_W / 32);
node._y = depth * LEVEL_H + 28;
layout(node.right, depth+1, counter);
}
function svgEl(tag, attrs){
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
Object.keys(attrs).forEach(function(k){ el.setAttribute(k, attrs[k]); });
return el;
}
function render(splayed){
layout(root, 0, [0]);
var svg = document.getElementById('tree-svg');
svg.innerHTML = '';
// Draw edges first
function drawEdges(node){
if(!node) return;
if(node.left){
svg.appendChild(svgEl('line',{class:'edge',x1:node._x,y1:node._y,x2:node.left._x,y2:node.left._y}));
drawEdges(node.left);
}
if(node.right){
svg.appendChild(svgEl('line',{class:'edge',x1:node._x,y1:node._y,x2:node.right._x,y2:node.right._y}));
drawEdges(node.right);
}
}
drawEdges(root);
// Draw nodes
function drawNodes(node){
if(!node) return;
var g = svgEl('g',{class:'node','data-key':node.key});
var cls = heatClass(node.heat);
var isSplayed = (node.key === splayed);
var r = isSplayed ? 17 : 14;
var c = svgEl('circle',{cx:node._x,cy:node._y,r:r,class:cls+(isSplayed?' splayed':'')});
var t = svgEl('text',{x:node._x,y:node._y}); t.textContent = node.key;
g.appendChild(c); g.appendChild(t);
g.addEventListener('click', function(){ onAccess(node.key); });
svg.appendChild(g);
drawNodes(node.left);
drawNodes(node.right);
}
drawNodes(root);
}
function onAccess(key){
var n = find(key);
access(key);
var hc = heatClass(root.heat);
var label = hc==='hot' ? fmt('๐ฅ Hot key %key% is at the root!', { key: key }) : hc==='warm' ? fmt('Warm key %key% splayed to root.', { key: key }) : fmt('Key %key% splayed to root.', { key: key });
var s = document.getElementById('status');
s.textContent = label;
s.className = 'status ok';
}
document.getElementById('reset').addEventListener('click', function(){
buildTree();
render(null);
document.getElementById('status').textContent = 'Tree reset. Click a node to splay it.';
document.getElementById('status').className = 'status';
});
document.getElementById('seq').addEventListener('click', function(){
var seq = [1,1,1,8,8,15];
var i = 0;
function step(){
if(i >= seq.length){ render(seq[seq.length-1]);
document.getElementById('status').textContent = 'Done! Key 1 is hot (red), key 8 is warm, key 15 just splayed.';
document.getElementById('status').className = 'status ok';
return;
}
access(seq[i], true);
i++;
setTimeout(step, 320);
}
buildTree();
step();
});
buildTree();
render(null);
document.getElementById('status').textContent = 'Click a node to splay it.';
<p class="hint">Click any node to <b>splay</b> it to the root. The tree rotates until that key sits at the top. Access the same key repeatedly and watch it stay near the root โ the tree learns it is popular.</p>
<div id="tree-wrap">
<svg id="tree-svg" width="560" height="260" viewBox="0 0 560 260"></svg>
</div>
<div class="status" id="status">Click a node to splay it.</div>
<div class="btns">
<button id="reset" type="button" class="ghost">Reset tree</button>
<button id="seq" type="button">Access 1, 1, 1, 8, 8, 15</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#tree-wrap { width: 100%; overflow-x: auto; }
#tree-svg { display: block; }
.edge { stroke: #adb1b8; stroke-width: 1.5; fill: none; }
.node circle { stroke-width: 2; cursor: pointer; transition: r .15s; }
.node circle.cold { fill: #c9ccd1; stroke: #adb1b8; }
.node circle.warm { fill: #f4a261; stroke: #e07b3c; }
.node circle.hot { fill: #e63946; stroke: #c92f3c; }
.node circle:hover { r: 18; }
.node text { font: 700 13px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.node circle.hot ~ text { fill: #fff; }
.node circle.warm ~ text { fill: #fff; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0 .3rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Splay tree demo โ vanilla JS, no imports.
// Nodes hold: key, left, right, parent. The splay() method brings a node to root via zig, zig-zig (same side) and zig-zag (opposite side) rotations.
// Heat tracks access frequency so we can colour cold/warm/hot nodes.
// โโ BST node โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function Node(key){ this.key=key; this.left=this.right=this.parent=null; this.heat=0; }
// โโ Splay tree โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
var root = null;
function setParent(node, par){ if(node) node.parent = par; }
function rotateRight(x){
var y = x.left; if(!y) return;
x.left = y.right; setParent(y.right, x);
y.parent = x.parent;
if(!x.parent) root = y;
else if(x === x.parent.left) x.parent.left = y;
else x.parent.right = y;
y.right = x; x.parent = y;
}
function rotateLeft(x){
var y = x.right; if(!y) return;
x.right = y.left; setParent(y.left, x);
y.parent = x.parent;
if(!x.parent) root = y;
else if(x === x.parent.left) x.parent.left = y;
else x.parent.right = y;
y.left = x; x.parent = y;
}
function splay(x){
while(x.parent){
var p = x.parent, g = p.parent;
if(!g){
// zig
if(x === p.left) rotateRight(p); else rotateLeft(p);
} else if(x === p.left && p === g.left){
// zig-zig left
rotateRight(g); rotateRight(p);
} else if(x === p.right && p === g.right){
// zig-zig right
rotateLeft(g); rotateLeft(p);
} else if(x === p.left && p === g.right){
// zig-zag
rotateRight(p); rotateLeft(g);
} else {
// zig-zag
rotateLeft(p); rotateRight(g);
}
}
root = x;
}
function insert(key){
var n = new Node(key);
if(!root){ root = n; return; }
var cur = root, par = null;
while(cur){ par = cur; if(key < cur.key) cur = cur.left; else cur = cur.right; }
n.parent = par;
if(key < par.key) par.left = n; else par.right = n;
}
function find(key){
var cur = root;
while(cur){ if(key === cur.key) return cur; cur = key < cur.key ? cur.left : cur.right; }
return null;
}
// โโ Build initial tree by inserting 1..15 in order (gives a right-leaning chain, which makes the first splay dramatic). โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildTree(){
root = null;
// Insert in a shuffled order to get a reasonable starting shape
var keys = [8,4,12,2,6,10,14,1,3,5,7,9,11,13,15];
keys.forEach(function(k){ insert(k); });
// Reset heat
var n = find(8); // touch root area
}
// โโ Access: splay the node and increase its heat โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function access(key, skipRender){
var n = find(key);
if(!n) return;
splay(n);
n.heat = Math.min(n.heat + 1, 5);
if(!skipRender) render(key);
return n;
}
// โโ Heat class โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function heatClass(h){ if(h >= 3) return 'hot'; if(h >= 1) return 'warm'; return 'cold'; }
// โโ Layout: in-order traversal assigns x positions; depth gives y โโโโโโโโโโโโโ
var SVG_W = 560, SVG_H = 260, RADIUS = 15, LEVEL_H = 52;
function layout(node, depth, counter){
if(!node) return;
layout(node.left, depth+1, counter);
node._x = (counter[0]++) * (SVG_W / 16) + (SVG_W / 32);
node._y = depth * LEVEL_H + 28;
layout(node.right, depth+1, counter);
}
function svgEl(tag, attrs){
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
Object.keys(attrs).forEach(function(k){ el.setAttribute(k, attrs[k]); });
return el;
}
function render(splayed){
layout(root, 0, [0]);
var svg = document.getElementById('tree-svg');
svg.innerHTML = '';
// Draw edges first
function drawEdges(node){
if(!node) return;
if(node.left){
svg.appendChild(svgEl('line',{class:'edge',x1:node._x,y1:node._y,x2:node.left._x,y2:node.left._y}));
drawEdges(node.left);
}
if(node.right){
svg.appendChild(svgEl('line',{class:'edge',x1:node._x,y1:node._y,x2:node.right._x,y2:node.right._y}));
drawEdges(node.right);
}
}
drawEdges(root);
// Draw nodes
function drawNodes(node){
if(!node) return;
var g = svgEl('g',{class:'node','data-key':node.key});
var cls = heatClass(node.heat);
var isSplayed = (node.key === splayed);
var r = isSplayed ? 17 : 14;
var c = svgEl('circle',{cx:node._x,cy:node._y,r:r,class:cls+(isSplayed?' splayed':'')});
var t = svgEl('text',{x:node._x,y:node._y}); t.textContent = node.key;
g.appendChild(c); g.appendChild(t);
g.addEventListener('click', function(){ onAccess(node.key); });
svg.appendChild(g);
drawNodes(node.left);
drawNodes(node.right);
}
drawNodes(root);
}
function onAccess(key){
var n = find(key);
access(key);
var hc = heatClass(root.heat);
var label = hc==='hot' ? fmt('๐ฅ Hot key %key% is at the root!', { key: key }) : hc==='warm' ? fmt('Warm key %key% splayed to root.', { key: key }) : fmt('Key %key% splayed to root.', { key: key });
var s = document.getElementById('status');
s.textContent = label;
s.className = 'status ok';
}
document.getElementById('reset').addEventListener('click', function(){
buildTree();
render(null);
document.getElementById('status').textContent = 'Tree reset. Click a node to splay it.';
document.getElementById('status').className = 'status';
});
document.getElementById('seq').addEventListener('click', function(){
var seq = [1,1,1,8,8,15];
var i = 0;
function step(){
if(i >= seq.length){ render(seq[seq.length-1]);
document.getElementById('status').textContent = 'Done! Key 1 is hot (red), key 8 is warm, key 15 just splayed.';
document.getElementById('status').className = 'status ok';
return;
}
access(seq[i], true);
i++;
setTimeout(step, 320);
}
buildTree();
step();
});
buildTree();
render(null);
document.getElementById('status').textContent = 'Click a node to splay it.';
Notice the asymmetry. A single access can trigger a long chain of rotations โ O(n) in the worst case. But after that reorganization, all the keys you passed through on the way up are now shallower. The tree has paid a large upfront cost and then pre-cached the neighborhood. The access lemma proves that any sequence of m operations on an n-key splay tree costs at most O((m+n)logn) total โ so the amortized cost per operation is O(logn).
Comments
Loading comments...