Insert a few keys below. Each new node flips a coin repeatedly to decide its tower height — heads keeps building up, tails stops — so most nodes stay short and a lucky few rise into the express lanes.
<p class="hint">Insert keys to build the list — each node flips a coin for its <b>tower height</b>, so a lucky few rise into the express lanes. Then <b>search</b> and watch the cursor hop along the high lanes, dropping down only when the next node would overshoot.</p>
<div class="controls">
<input id="key" type="number" value="42" min="1" max="99" />
<button id="insert" type="button">Insert</button>
<button id="rand" type="button">Insert random</button>
<button id="search" type="button">Search</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="grid" class="grid"></div>
<div class="status" id="status">Insert a few keys, then search for one.</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; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
input { width: 64px; font: 600 14px system-ui, sans-serif; padding: .4rem .5rem;
border: 1px solid #adb1b8; border-radius: 8px; }
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; }
.grid { display: flex; flex-direction: column-reverse; gap: 4px; overflow-x: auto;
padding: .6rem .4rem; background: #f4f7fa; border-radius: 10px; }
.lane { display: flex; gap: 4px; align-items: center; min-height: 38px; }
.lane-label { flex: 0 0 34px; font: 700 11px ui-monospace, monospace; color: #6b7785; text-align: right; padding-right: 4px; }
.node { flex: 0 0 38px; height: 32px; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; border-radius: 8px; background: #e8eef3;
color: #1d3557; border: 1px solid #cdd9e3; transition: all .12s; }
.node.head { background: #1d3557; color: #fff; border-color: #1d3557; font-size: 11px; }
.node.gap { background: transparent; border: 1px dashed #d6dee6; color: transparent; }
.node.cursor { background: #f4a900; border-color: #c98700; color: #1d3557; transform: scale(1.12); }
.node.found { background: #0a7d33; border-color: #086628; color: #fff; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A skip list: an ordered linked list with random "express lanes". Each inserted
// node gets a random TOWER HEIGHT (flip a coin: heads => grow taller, P=1/2).
// Search starts on the top lane, moves right while the next key <= target, and
// drops a level otherwise — so high lanes skip over many nodes at once.
// Expected search/insert/delete: O(log n). (William Pugh, 1990.)
const P = 0.5, MAXLVL = 5;
let keys = []; // sorted unique keys
let height = {}; // key -> tower height (1..MAXLVL)
let topLevel = 1;
function randomHeight(){ let h = 1; while (Math.random() < P && h < MAXLVL) h++; return h; }
function insert(k){
if (height[k]) return false; // no duplicates
keys.push(k); keys.sort((a,b)=>a-b);
height[k] = randomHeight();
topLevel = Math.max(1, ...keys.map(x=>height[x]));
return true;
}
// Returns the ordered list of nodes visited during the search (for animation),
// and whether the key was found. This is the real skip-list search: at each
// level the "next" pointer jumps to the next node tall enough to reach this
// level, skipping over the short ones — that is what makes it O(log n).
function searchPath(target){
const path = []; // {level, key} key=null means HEAD
let lvl = topLevel - 1; // levels are 0-indexed internally
let i = -1; // index in `keys`, -1 = head sentinel
path.push({ level: lvl, key: null });
while (lvl >= 0){
// advance to the next node present at this level while its key <= target
while (true){
let j = i + 1;
while (j < keys.length && height[keys[j]] - 1 < lvl) j++; // skip short nodes
if (j < keys.length && keys[j] <= target){ i = j; path.push({ level: lvl, key: keys[j] }); }
else break;
}
if (i >= 0 && keys[i] === target) return { path, found: true };
lvl--; // drop down a level
if (lvl >= 0) path.push({ level: lvl, key: i >= 0 ? keys[i] : null });
}
return { path, found: keys[i] === target };
}
// ---- rendering ----
const grid = document.getElementById('grid');
function nodeId(level, key){ return 'n_'+level+'_'+(key===null?'H':key); }
function render(){
grid.innerHTML = '';
for (let lvl = 0; lvl < topLevel; lvl++){
const lane = document.createElement('div'); lane.className = 'lane';
const lab = document.createElement('div'); lab.className = 'lane-label'; lab.textContent = fmt('L%lvl%', { lvl: lvl });
lane.appendChild(lab);
const head = document.createElement('div'); head.className = 'node head';
head.textContent = 'HEAD'; head.id = nodeId(lvl, null); lane.appendChild(head);
for (const k of keys){
const cell = document.createElement('div');
if (height[k] - 1 >= lvl){ cell.className = 'node'; cell.textContent = k; cell.id = nodeId(lvl, k); }
else { cell.className = 'node gap'; cell.textContent = k; }
lane.appendChild(cell);
}
grid.appendChild(lane);
}
}
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t; s.className = 'status '+(cls||''); }
async function animateSearch(target){
const { path, found } = searchPath(target);
render();
let hops = 0;
for (const step of path){
const el = document.getElementById(nodeId(step.level, step.key));
if (el){ el.classList.add('cursor'); }
hops++;
await new Promise(r=>setTimeout(r, 380));
if (el && !(found && step === path[path.length-1])) el.classList.remove('cursor');
}
const last = path[path.length-1];
if (found){
// highlight the found node on its top lane
const lvl = height[target]-1;
const el = document.getElementById(nodeId(lvl, target));
if (el){ el.classList.remove('cursor'); el.classList.add('found'); }
setStatus(fmt('Found %target% in %hops% hops (a flat list would need up to %indexOf%%v%).', { target: target, hops: hops, indexOf: keys.indexOf(target), v: 1 }), 'ok');
} else {
setStatus(fmt('%target% is not in the list — search ended after %hops% hops.', { target: target, hops: hops }), 'bad');
}
}
document.getElementById('insert').addEventListener('click', ()=>{
const k = parseInt(document.getElementById('key').value, 10);
if (isNaN(k)) return;
if (insert(k)){ render(); setStatus(fmt('Inserted %k% with tower height %v%.', { k: k, v: height[k] }), ''); }
else setStatus(fmt('%k% is already in the list.', { k: k }), 'bad');
});
document.getElementById('rand').addEventListener('click', ()=>{
let k, tries=0; do { k = 1 + Math.floor(Math.random()*99); tries++; } while (height[k] && tries<200);
if (insert(k)){ document.getElementById('key').value = k; render(); setStatus(fmt('Inserted %k% with tower height %v%.', { k: k, v: height[k] }), ''); }
});
document.getElementById('search').addEventListener('click', ()=>{
const k = parseInt(document.getElementById('key').value, 10);
if (isNaN(k)) return;
if (keys.length === 0){ setStatus('Insert some keys first.', 'bad'); return; }
setStatus(fmt('Searching for %k%…', { k: k }), ''); animateSearch(k);
});
document.getElementById('reset').addEventListener('click', ()=>{
keys = []; height = {}; topLevel = 1; render(); setStatus('Insert a few keys, then search for one.', '');
});
// seed a small list so the structure is visible immediately
[12, 25, 37, 42, 58, 71, 90].forEach(insert);
render();
<p class="hint">Insert keys to build the list — each node flips a coin for its <b>tower height</b>, so a lucky few rise into the express lanes. Then <b>search</b> and watch the cursor hop along the high lanes, dropping down only when the next node would overshoot.</p>
<div class="controls">
<input id="key" type="number" value="42" min="1" max="99" />
<button id="insert" type="button">Insert</button>
<button id="rand" type="button">Insert random</button>
<button id="search" type="button">Search</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="grid" class="grid"></div>
<div class="status" id="status">Insert a few keys, then search for one.</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; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
input { width: 64px; font: 600 14px system-ui, sans-serif; padding: .4rem .5rem;
border: 1px solid #adb1b8; border-radius: 8px; }
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; }
.grid { display: flex; flex-direction: column-reverse; gap: 4px; overflow-x: auto;
padding: .6rem .4rem; background: #f4f7fa; border-radius: 10px; }
.lane { display: flex; gap: 4px; align-items: center; min-height: 38px; }
.lane-label { flex: 0 0 34px; font: 700 11px ui-monospace, monospace; color: #6b7785; text-align: right; padding-right: 4px; }
.node { flex: 0 0 38px; height: 32px; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; border-radius: 8px; background: #e8eef3;
color: #1d3557; border: 1px solid #cdd9e3; transition: all .12s; }
.node.head { background: #1d3557; color: #fff; border-color: #1d3557; font-size: 11px; }
.node.gap { background: transparent; border: 1px dashed #d6dee6; color: transparent; }
.node.cursor { background: #f4a900; border-color: #c98700; color: #1d3557; transform: scale(1.12); }
.node.found { background: #0a7d33; border-color: #086628; color: #fff; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A skip list: an ordered linked list with random "express lanes". Each inserted
// node gets a random TOWER HEIGHT (flip a coin: heads => grow taller, P=1/2).
// Search starts on the top lane, moves right while the next key <= target, and
// drops a level otherwise — so high lanes skip over many nodes at once.
// Expected search/insert/delete: O(log n). (William Pugh, 1990.)
const P = 0.5, MAXLVL = 5;
let keys = []; // sorted unique keys
let height = {}; // key -> tower height (1..MAXLVL)
let topLevel = 1;
function randomHeight(){ let h = 1; while (Math.random() < P && h < MAXLVL) h++; return h; }
function insert(k){
if (height[k]) return false; // no duplicates
keys.push(k); keys.sort((a,b)=>a-b);
height[k] = randomHeight();
topLevel = Math.max(1, ...keys.map(x=>height[x]));
return true;
}
// Returns the ordered list of nodes visited during the search (for animation),
// and whether the key was found. This is the real skip-list search: at each
// level the "next" pointer jumps to the next node tall enough to reach this
// level, skipping over the short ones — that is what makes it O(log n).
function searchPath(target){
const path = []; // {level, key} key=null means HEAD
let lvl = topLevel - 1; // levels are 0-indexed internally
let i = -1; // index in `keys`, -1 = head sentinel
path.push({ level: lvl, key: null });
while (lvl >= 0){
// advance to the next node present at this level while its key <= target
while (true){
let j = i + 1;
while (j < keys.length && height[keys[j]] - 1 < lvl) j++; // skip short nodes
if (j < keys.length && keys[j] <= target){ i = j; path.push({ level: lvl, key: keys[j] }); }
else break;
}
if (i >= 0 && keys[i] === target) return { path, found: true };
lvl--; // drop down a level
if (lvl >= 0) path.push({ level: lvl, key: i >= 0 ? keys[i] : null });
}
return { path, found: keys[i] === target };
}
// ---- rendering ----
const grid = document.getElementById('grid');
function nodeId(level, key){ return 'n_'+level+'_'+(key===null?'H':key); }
function render(){
grid.innerHTML = '';
for (let lvl = 0; lvl < topLevel; lvl++){
const lane = document.createElement('div'); lane.className = 'lane';
const lab = document.createElement('div'); lab.className = 'lane-label'; lab.textContent = fmt('L%lvl%', { lvl: lvl });
lane.appendChild(lab);
const head = document.createElement('div'); head.className = 'node head';
head.textContent = 'HEAD'; head.id = nodeId(lvl, null); lane.appendChild(head);
for (const k of keys){
const cell = document.createElement('div');
if (height[k] - 1 >= lvl){ cell.className = 'node'; cell.textContent = k; cell.id = nodeId(lvl, k); }
else { cell.className = 'node gap'; cell.textContent = k; }
lane.appendChild(cell);
}
grid.appendChild(lane);
}
}
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t; s.className = 'status '+(cls||''); }
async function animateSearch(target){
const { path, found } = searchPath(target);
render();
let hops = 0;
for (const step of path){
const el = document.getElementById(nodeId(step.level, step.key));
if (el){ el.classList.add('cursor'); }
hops++;
await new Promise(r=>setTimeout(r, 380));
if (el && !(found && step === path[path.length-1])) el.classList.remove('cursor');
}
const last = path[path.length-1];
if (found){
// highlight the found node on its top lane
const lvl = height[target]-1;
const el = document.getElementById(nodeId(lvl, target));
if (el){ el.classList.remove('cursor'); el.classList.add('found'); }
setStatus(fmt('Found %target% in %hops% hops (a flat list would need up to %indexOf%%v%).', { target: target, hops: hops, indexOf: keys.indexOf(target), v: 1 }), 'ok');
} else {
setStatus(fmt('%target% is not in the list — search ended after %hops% hops.', { target: target, hops: hops }), 'bad');
}
}
document.getElementById('insert').addEventListener('click', ()=>{
const k = parseInt(document.getElementById('key').value, 10);
if (isNaN(k)) return;
if (insert(k)){ render(); setStatus(fmt('Inserted %k% with tower height %v%.', { k: k, v: height[k] }), ''); }
else setStatus(fmt('%k% is already in the list.', { k: k }), 'bad');
});
document.getElementById('rand').addEventListener('click', ()=>{
let k, tries=0; do { k = 1 + Math.floor(Math.random()*99); tries++; } while (height[k] && tries<200);
if (insert(k)){ document.getElementById('key').value = k; render(); setStatus(fmt('Inserted %k% with tower height %v%.', { k: k, v: height[k] }), ''); }
});
document.getElementById('search').addEventListener('click', ()=>{
const k = parseInt(document.getElementById('key').value, 10);
if (isNaN(k)) return;
if (keys.length === 0){ setStatus('Insert some keys first.', 'bad'); return; }
setStatus(fmt('Searching for %k%…', { k: k }), ''); animateSearch(k);
});
document.getElementById('reset').addEventListener('click', ()=>{
keys = []; height = {}; topLevel = 1; render(); setStatus('Insert a few keys, then search for one.', '');
});
// seed a small list so the structure is visible immediately
[12, 25, 37, 42, 58, 71, 90].forEach(insert);
render();
Comments
Loading comments...