Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/images/linked-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/app/components/algorithm-cards.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ const algorithms = [
title: 'Game of Life',
description: "Visualize the Game of Life cellular automaton",
image: '/AlgorithmVisualizer/images/game-of-life.png?height=200&width=300'
},{
id: 'linked-list',
title: 'Linked List',
description: "Visualize insertion, deletion, search, and reversal on singly and doubly linked lists",
image: '/AlgorithmVisualizer/images/linked-list.png?height=200&width=300'
},
// {
// id: '15-puzzle',
Expand Down
9 changes: 9 additions & 0 deletions src/app/linked-list/arrow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Pointer arrow between two points. Uses the shared <marker id="arrow">
// defined in the canvas <defs>.

export default function Arrow({ x1, y1, x2, y2, color = '#64748b' }) {
return (
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke={color} strokeWidth="0.6"
markerEnd="url(#arrow)" />
);
}
107 changes: 107 additions & 0 deletions src/app/linked-list/canvas.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Node, { NODE_W, NODE_H } from './node';
import Arrow from './arrow';

const VB_W = 240;
const VB_H = 90;
const PAD = 16;
const Y = 56; // resting row
const LIFT = 26; // how high a staged node floats above the row

export default function Canvas({ nodes, nextOf, prevOf, listType, nodeState = {}, pointers = [], liftedId = null }) {
const count = nodes.length;
const spacing = count > 1
? Math.min(46, (VB_W - 2 * PAD - NODE_W) / (count - 1))
: 0;
const totalW = (count - 1) * spacing + NODE_W;
const startX = Math.max(PAD, (VB_W - totalW) / 2);

const posById = {};
nodes.forEach((n, i) => {
posById[n.id] = { x: startX + i * spacing, y: n.id === liftedId ? Y - LIFT : Y, i };
});

const doubly = listType === 1;
const tail = nodes[count - 1];

return (
<svg viewBox={`0 0 ${VB_W} ${VB_H}`} xmlns="http://www.w3.org/2000/svg" className="w-full">
<defs>
<filter id="nodeShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0.3" dy="0.4" stdDeviation="0.5" floodOpacity="0.25" />
</filter>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5"
markerWidth="4" markerHeight="4" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#64748b" />
</marker>
</defs>

{/* next pointers */}
{nodes.map((n) => {
const target = nextOf[n.id];
if (target == null) return null;
const src = posById[n.id];
const dst = posById[target];
if (!dst) return null;
const y1 = src.y + (doubly ? -3 : 0);
const y2 = dst.y + (doubly ? -3 : 0);
// forward link exits the next-cell to the target's left edge;
// a flipped (leftward) link during reverse points the other way.
if (dst.i > src.i) {
return <Arrow key={'nx' + n.id} x1={src.x + NODE_W} y1={y1} x2={dst.x} y2={y2} />;
}
return <Arrow key={'nx' + n.id} x1={src.x} y1={y1} x2={dst.x + NODE_W} y2={y2} />;
})}

{/* prev pointers (doubly only) */}
{doubly && nodes.map((n) => {
const target = prevOf[n.id];
if (target == null) return null;
const src = posById[n.id];
const dst = posById[target];
if (!dst) return null;
return <Arrow key={'pv' + n.id} x1={src.x} y1={src.y + 3} x2={dst.x + NODE_W} y2={dst.y + 3} color="#94a3b8" />;
})}

{/* null terminator after the tail */}
{tail && nextOf[tail.id] == null && (
<text x={posById[tail.id].x + NODE_W + 9} y={posById[tail.id].y} textAnchor="middle" dominantBaseline="central"
style={{ font: '4px sans-serif' }} fill="#94a3b8">null</text>
)}

{/* null terminator before the head (doubly: head.prev = null) */}
{doubly && nodes[0] && prevOf[nodes[0].id] == null && (
<text x={posById[nodes[0].id].x - 9} y={posById[nodes[0].id].y} textAnchor="middle" dominantBaseline="central"
style={{ font: '4px sans-serif' }} fill="#94a3b8">null</text>
)}

{/* nodes */}
{nodes.map((n) => (
<Node key={'nd' + n.id} x={posById[n.id].x} y={posById[n.id].y} value={n.value}
listType={listType} state={nodeState[n.id]}
isHead={prevOf[n.id] == null} isTail={nextOf[n.id] == null} />
))}

{/* head / tail labels (skip while a node is lifted to avoid clutter) */}
{nodes[0] && nodes[0].id !== liftedId && (
<text x={posById[nodes[0].id].x + NODE_W / 2} y={Y - NODE_H / 2 - 4}
textAnchor="middle" style={{ font: '3.5px sans-serif', fontWeight: 600 }} fill="#475569">head</text>
)}
{tail && count > 1 && tail.id !== liftedId && (
<text x={posById[tail.id].x + NODE_W / 2} y={Y - NODE_H / 2 - 4}
textAnchor="middle" style={{ font: '3.5px sans-serif', fontWeight: 600 }} fill="#475569">tail</text>
)}

{/* operation pointer captions (curr / prev / next) */}
{pointers.map((p, idx) => {
if (p.nodeId == null || !posById[p.nodeId]) return null;
const px = posById[p.nodeId].x + NODE_W / 2;
return (
<text key={'pt' + idx} x={px} y={Y + NODE_H / 2 + 7} textAnchor="middle"
style={{ font: '4px sans-serif', fontWeight: 700 }} fill="#0f172a">
▲ {p.label}
</text>
);
})}
</svg>
);
}
115 changes: 115 additions & 0 deletions src/app/linked-list/menu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useState } from 'react';
import { CustomSelect } from '@/components/custom-select';
import { CustomSlider } from '@/components/custom-slider';
import { CustomToggle } from '@/components/custom-toggle';
import { Button } from '@/components/ui/button';
import { Play, Shuffle, RotateCcw } from 'lucide-react';

const OPERATIONS = [
'Insert at head',
'Insert at tail',
'Insert at index',
'Delete by value',
'Delete at index',
'Search',
'Reverse',
];

const NEEDS_VALUE = new Set([0, 1, 2, 3, 5]);
const NEEDS_INDEX = new Set([2, 4]);

export default function Menu({
disabled,
onListTypeChange,
onOperationChange,
onValueChange,
onIndexChange,
onSpeedChange,
onVisualize,
onRandomize,
onReset,
}) {
const [operation, setOperation] = useState(0);

const handleOperation = (op) => {
setOperation(op);
onOperationChange(op);
};

return (
<div className="w-64 bg-gray-100 p-4 space-y-6">
<h2 className="text-lg font-semibold">Linked List</h2>

<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-gray-300" />
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Config</span>
<div className="h-px flex-1 bg-gray-300" />
</div>
<CustomToggle
title="Doubly linked"
onCheckedChange={(checked) => onListTypeChange(checked ? 1 : 0)}
disabled={disabled}
/>
<CustomSelect
title="Operation"
options={OPERATIONS}
onChange={handleOperation}
disabled={disabled}
/>
{NEEDS_VALUE.has(operation) && (
<div className="space-y-2">
<label className="text-sm font-medium whitespace-nowrap">Value</label>
<input
type="number"
defaultValue={42}
onChange={(e) => onValueChange(e.target.value)}
disabled={disabled}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
/>
</div>
)}
{NEEDS_INDEX.has(operation) && (
<div className="space-y-2">
<label className="text-sm font-medium whitespace-nowrap">Index</label>
<input
type="number"
defaultValue={1}
min={0}
onChange={(e) => onIndexChange(e.target.value)}
disabled={disabled}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
/>
</div>
)}
<CustomSlider
title="Speed"
defaultValue={50}
min={10}
max={100}
step={1}
onChange={onSpeedChange}
/>
</div>

<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-gray-300" />
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</span>
<div className="h-px flex-1 bg-gray-300" />
</div>
<Button className="w-full" onClick={onVisualize} disabled={disabled}>
<Play /> Visualize
</Button>
<div className="flex gap-2">
<Button className="flex-1" variant="outline" onClick={onRandomize} disabled={disabled}>
<Shuffle /> Random
</Button>
<Button className="flex-1" variant="outline" onClick={onReset} disabled={disabled}>
<RotateCcw /> Reset
</Button>
</div>
</div>
</div>
);
}
48 changes: 48 additions & 0 deletions src/app/linked-list/node.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Two-cell box node. Movement is a CSS transform transition (animates whenever
// x changes); the mount fade-in is a one-shot SMIL <animate begin="0s"> that
// fires only when a freshly-inserted node is added to the DOM.

const COLORS = {
normal: { fill: '#0d9488', stroke: '#0f766e' },
active: { fill: '#f59e0b', stroke: '#d97706' },
found: { fill: '#16a34a', stroke: '#15803d' },
remove: { fill: '#e11d48', stroke: '#be123c' },
done: { fill: '#334155', stroke: '#475569' },
};

const W = 24;
const H = 14;

export default function Node({ x, y, value, listType, state = 'normal', isHead, isTail }) {
const c = COLORS[state] || COLORS.normal;
const doubly = listType === 1;
const dataMid = doubly ? 12 : 9;

return (
<g style={{ transform: `translate(${x}px, ${y}px)`, transition: 'transform 0.4s ease' }}>
<animate attributeName="opacity" from="0" to="1" dur="0.4s" begin="0s" />

<rect x="0" y={-H / 2} width={W} height={H} rx="1.5"
fill={c.fill} stroke={c.stroke} strokeWidth="0.5" filter="url(#nodeShadow)" />

{/* prev cell (doubly): slash when head (prev = null), else a pointer dot */}
{doubly && <line x1="6" y1={-H / 2} x2="6" y2={H / 2} stroke={c.stroke} strokeWidth="0.4" />}
{doubly && (isHead
? <line x1="1" y1={H / 2 - 1.5} x2="5" y2={-H / 2 + 1.5} stroke="#f8fafc" strokeWidth="0.5" />
: <circle cx="3" cy="0" r="1.1" fill="#0f172a" />)}

<line x1="18" y1={-H / 2} x2="18" y2={H / 2} stroke={c.stroke} strokeWidth="0.4" />

<text x={dataMid} y="0" textAnchor="middle" dominantBaseline="central"
style={{ font: '5px sans-serif', fontWeight: 600 }} fill="#f8fafc">{value}</text>

{/* next cell: slash when tail (next = null), else a pointer dot */}
{isTail
? <line x1="19" y1={H / 2 - 1.5} x2="23" y2={-H / 2 + 1.5} stroke="#f8fafc" strokeWidth="0.5" />
: <circle cx="21" cy="0" r="1.1" fill="#0f172a" />}
</g>
);
}

export const NODE_W = W;
export const NODE_H = H;
Loading
Loading