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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions src/algorithms/dataStructure/linkedlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// src/algorithms/dataStructure/linkedlist.js
// Generator that yields step objects for visualization.
// Each yield: { array, highlight: { type, index }, message?, done?:boolean }

export function* linkedListOp(startArray = [], action = {}) {
// Work on a shallow copy
const arr = [...startArray];
const variant = action.variant || "singly"; // 'singly' | 'doubly' | 'circular'
const direction = action.direction || "forward"; // for traverse in doubly

const pushStep = (type, index = null, message = null) => {
return { array: [...arr], highlight: index !== null ? { type, index } : { type }, message };
};

// small helper to clamp
const validIndex = (i) => i >= 0 && i <= arr.length;

switch (action.type) {
case "insertHead": {
arr.unshift(action.value);
yield pushStep("insert", 0, `Inserted ${action.value} at head`);
break;
}

case "insertTail": {
arr.push(action.value);
yield pushStep("insert", arr.length - 1, `Inserted ${action.value} at tail`);
break;
}

case "insertAt": {
const idx = Number.isFinite(action.index) ? action.index : arr.length;
if (!validIndex(idx)) {
yield { array: [...arr], highlight: { type: "error" }, message: "Index out of bounds" };
return;
}
arr.splice(idx, 0, action.value);
yield pushStep("insert", idx, `Inserted ${action.value} at index ${idx}`);
break;
}

case "deleteHead": {
if (arr.length === 0) {
yield { array: [...arr], highlight: { type: "error" }, message: "List is empty" };
return;
}
yield pushStep("delete", 0, `Deleting head (${arr[0]})`);
arr.shift();
yield pushStep("done", null, "Deleted head");
break;
}

case "deleteTail": {
if (arr.length === 0) {
yield { array: [...arr], highlight: { type: "error" }, message: "List is empty" };
return;
}
yield pushStep("delete", arr.length - 1, `Deleting tail (${arr[arr.length - 1]})`);
arr.pop();
yield pushStep("done", null, "Deleted tail");
break;
}

case "deleteValue": {
const idx = arr.indexOf(action.value);
if (idx === -1) {
yield { array: [...arr], highlight: { type: "error" }, message: `Value ${action.value} not found` };
return;
}
yield pushStep("delete", idx, `Deleting value ${action.value} at index ${idx}`);
arr.splice(idx, 1);
yield pushStep("done", null, `Deleted ${action.value}`);
break;
}

case "traverse": {
if (arr.length === 0) {
yield { array: [...arr], highlight: { type: "error" }, message: "Nothing to traverse" };
return;
}

// For circular: traverse exactly arr.length nodes (one loop)
// For doubly: support forward/backward via action.direction
if (variant === "doubly" && direction === "backward") {
// traverse from tail to head
for (let i = arr.length - 1; i >= 0; i--) {
yield pushStep("traverse", i, `Visiting index ${i}: ${arr[i]}`);
}
} else {
// forward
for (let i = 0; i < arr.length; i++) {
yield pushStep("traverse", i, `Visiting index ${i}: ${arr[i]}`);
}
}

// For circular, show extra step showing loop back to head
if (variant === "circular" && arr.length > 0) {
yield { array: [...arr], highlight: { type: "loop" }, message: "Looped back to head (circular)" };
}

yield { array: [...arr], highlight: { type: "done" }, done: true };
break;
}

case "clear": {
yield { array: [...arr], highlight: { type: "clear" }, message: "Clearing list..." };
arr.length = 0;
yield { array: [], highlight: { type: "done" }, done: true };
break;
}

case "loadDemo": {
const demo = action.demo || ["A", "B", "C"];
arr.length = 0;
for (let i = 0; i < demo.length; i++) {
arr.push(demo[i]);
yield pushStep("insert", i, `Loaded ${demo[i]}`);
}
yield { array: [...arr], highlight: { type: "done" }, done: true };
break;
}

default:
yield { array: [...arr], highlight: null, done: true };
break;
}
}
192 changes: 192 additions & 0 deletions src/components/dataStructure/linkedlist.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import React, { useEffect, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

export default function LinkedListVisualizer({
array = [],
highlight = null,
variant = "singly",
}) {
const containerRef = useRef(null);
const [positions, setPositions] = useState([]);
const [containerBounds, setContainerBounds] = useState(null);

// Recalculate node positions when array changes
useEffect(() => {
if (!containerRef.current || array.length === 0) {
setPositions([]);
return;
}

// Wait briefly for DOM to update
const timer = setTimeout(() => {
const container = containerRef.current?.getBoundingClientRect();
const nodes = Array.from(
containerRef.current.querySelectorAll(".ll-node")
);

if (nodes.length > 0 && container) {
const rects = nodes.map((n) => n.getBoundingClientRect());
setPositions(rects);
setContainerBounds(container);
}
}, 60);

return () => clearTimeout(timer);
}, [array.length]); // triggers on insert/delete

if (!array || array.length === 0)
return (
<div className="flex items-center justify-center text-slate-400 text-lg h-40">
List is empty
</div>
);

const isCircular = variant === "circular";
const isDoubly = variant === "doubly";

// Color logic for different highlight actions
const getNodeColor = (idx) => {
const isActive = highlight && highlight.index === idx;
const type = highlight?.type;
if (type === "insert" && isActive) return "bg-emerald-700 text-white";
if (type === "delete" && isActive) return "bg-rose-600 text-white";
if (type === "traverse" && isActive) return "bg-yellow-500 text-black";
return "bg-slate-800 text-white";
};

// ✅ Improved circular path calculation (perfectly connects)
const getCircularPath = () => {
if (!isCircular || positions.length < 2 || !containerBounds) return null;

const first = positions[0];
const last = positions[positions.length - 1];

// Calculate path start/end near bottom centers
const startX =
last.right - containerBounds.left - last.width * 0.2; // tail bottom-right inward
const startY = last.bottom - containerBounds.top - 5;

const endX = first.left - containerBounds.left + first.width * 0.2; // head bottom-left inward
const endY = first.bottom - containerBounds.top - 5;

// Control point for smooth curve below nodes
const controlY = Math.max(startY, endY) + 60;
const midX = (startX + endX) / 2;

return {
path: `M ${startX} ${startY} Q ${midX} ${controlY} ${endX} ${endY}`,
startX,
startY,
endX,
endY,
};
};

const circularPath = getCircularPath();

return (
<div
ref={containerRef}
className="relative flex items-center justify-center gap-6 p-6 min-h-[240px]"
>
{/* --- SVG Circular Connection --- */}
{isCircular && circularPath && (
<svg
className="absolute inset-0 w-full h-full pointer-events-none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id="circular-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#22c55e" />
<stop offset="100%" stopColor="#0ea5e9" />
</linearGradient>
</defs>

<motion.path
d={circularPath.path}
stroke="url(#circular-gradient)"
strokeWidth="2.5"
fill="none"
strokeDasharray="8 5"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1.2, ease: "easeInOut" }}
/>

{/* Label on the path */}
<text
x={(circularPath.startX + circularPath.endX) / 2}
y={Math.max(circularPath.startY, circularPath.endY) + 50}
textAnchor="middle"
className="fill-slate-300 text-xs"
>
Tail → Head
</text>
</svg>
)}

{/* --- Nodes --- */}
<AnimatePresence mode="popLayout">
{array.map((val, idx) => (
<motion.div
key={`${val}-${idx}`}
layout
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.6, opacity: 0 }}
transition={{ duration: 0.3 }}
className={`relative ll-node flex flex-col items-center justify-center w-16 h-16 rounded-lg shadow-md ${getNodeColor(
idx
)}`}
>
{/* Index label */}
<div className="absolute -top-5 text-xs text-slate-400">
[{idx}]
</div>

{/* Node value */}
<div className="text-base font-semibold">{val}</div>

{/* HEAD label */}
{idx === 0 && (
<span className="absolute -bottom-6 text-xs text-emerald-400 font-semibold">
HEAD
</span>
)}

{/* TAIL label */}
{idx === array.length - 1 && (
<span
className={`absolute text-xs font-semibold ${
isCircular
? "top-[70px] text-cyan-400"
: "-bottom-6 text-rose-400"
}`}
>
TAIL
</span>
)}

{/* Doubly linked backward pointer */}
{isDoubly && idx > 0 && (
<span className="absolute -left-5 text-lg text-slate-500">←</span>
)}

{/* Forward pointer */}
{(idx < array.length - 1 || isCircular) && (
<span className="absolute -right-5 text-lg text-slate-500">→</span>
)}
</motion.div>
))}
</AnimatePresence>

{/* --- Info Label --- */}
{isCircular && (
<div className="absolute bottom-2 text-slate-400 text-sm italic">
↻ Circular Linked List — Tail connects back to Head
</div>
)}
</div>
);
}

12 changes: 10 additions & 2 deletions src/pages/dataStructure/datastructurePage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Network, Compass, Rocket } from "lucide-react";
import StackPage from "./stack.jsx";
import LinkedListPage from "./linkedlist.jsx"; // ✅ Linked List page import

export default function DSPage() {
const [selectedDS, setSelectedDS] = useState("");
Expand All @@ -14,6 +15,13 @@ export default function DSPage() {
</div>
);

case "linkedlist":
return (
<div className="w-full h-full overflow-auto">
<LinkedListPage />
</div>
);

default:
return (
<div className="flex flex-col items-center justify-center text-center p-6">
Expand All @@ -27,7 +35,7 @@ export default function DSPage() {
</h2>
<p className="text-gray-400 mb-6 max-w-sm">
Select a data structure from the sidebar to begin visualization.
Watch how elements, stacks, and queues transform step by step! 🧠✨
Watch how stacks, queues, and linked lists transform step by step! 🧠✨
</p>
<div className="flex items-center justify-center gap-6">
<Compass className="w-8 h-8 text-blue-400 animate-pulse" />
Expand Down Expand Up @@ -57,7 +65,7 @@ export default function DSPage() {
<option value="">Select Data Structure</option>
<option value="stack">Stack</option>
<option value="queue">Queue</option>
{/* <option value="linkedlist">Linked List</option> */}
<option value="linkedlist">Linked List</option>
</select>

<button
Expand Down
Loading
Loading