diff --git a/public/Tree-Traversal.png b/public/Tree-Traversal.png new file mode 100644 index 0000000..73e2993 Binary files /dev/null and b/public/Tree-Traversal.png differ diff --git a/src/App.jsx b/src/App.jsx index 68f06cb..5cbaaa8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,7 @@ import DSPage from "./pages/dataStructure/datastructurePage.jsx" import DynamicProgrammingPage from "./pages/dynamic-programming/DyanmicProgrammingPage.jsx"; import Searchingpage from "./pages/searching/searchingPage"; import RecursionPage from "./pages/Recursion/RecursionPage"; +import Treepage from "./pages/Tree/Treepage"; import SlidingWindowPage from "./pages/sliding-window/SlidingWindowPage"; function App() { return ( @@ -21,6 +22,7 @@ function App() { } /> } /> }/> + } /> }/> diff --git a/src/algorithms/Tree/treeTraversal.js b/src/algorithms/Tree/treeTraversal.js new file mode 100644 index 0000000..8a2ebfb --- /dev/null +++ b/src/algorithms/Tree/treeTraversal.js @@ -0,0 +1,278 @@ +function buildTreeFromLevelOrder(arr) { + if (!arr || arr.length === 0 || arr[0] === null) return null; + + const nodes = arr.map((val, idx) => + val === null || val === -1 + ? null + : { id: `n${idx}`, value: val, left: null, right: null } + ); + const root = nodes[0]; + if (!root) return null; + + const queue = [root]; + let i = 1; + + while (queue.length > 0 && i < nodes.length) { + const current = queue.shift(); + if (i < nodes.length && nodes[i] !== null) { + current.left = nodes[i]; + queue.push(nodes[i]); + } + i++; + + if (i < nodes.length && nodes[i] !== null) { + current.right = nodes[i]; + queue.push(nodes[i]); + } + i++; + } + return root; +} +function cloneTree(node) { + if (!node) return null; + return { + id: node.id, + value: node.value, + left: cloneTree(node.left), + right: cloneTree(node.right) + }; +} +function getAllNodeIds(node) { + if (!node) return []; + return [node.id, ...getAllNodeIds(node.left), ...getAllNodeIds(node.right)]; +} + +//inorder +export function* inorderTraversal(root) { + if (!root) { + yield {type: "error", message: "Tree is empty", current: null, visited: [], path: [], subtree: "root" }; + return; + } + + const visited = []; + const path = []; + function* traverse(node) { + if (!node) return; + if (node.left) { + path.push(node.value); + yield { + type: "traverse_left", + message: `Moving to left subtree of ${node.value}`, + current: node.value, visited: [...visited], path: [...path], subtree: "left" + }; + yield* traverse(node.left); + path.pop(); + } + path.push(node.value); + visited.push(node.value); + yield { + type: "visit", + message: `Visiting ${node.value} (Inorder: Left → Root → Right)`, + current: node.value, visited: [...visited], path: [...path], subtree: "root" + }; + if (node.right) { + yield { + type: "traverse_right", + message: `Moving to right subtree of ${node.value}`, + current: node.value, visited: [...visited], path: [...path], subtree: "root" + }; + yield* traverse(node.right); + } + + path.pop(); + } + yield* traverse(root); + + yield { + type: "complete", + message: `Inorder traversal complete: ${visited.join(" → ")}`, + current: null, visited: visited, path: [] + }; +} + +//preorder +export function* preorderTraversal(root) { + if (!root) { + yield { type: "error", message: "Tree is empty", current: null, visited: [], path: [] }; + return; + } + const visited = []; + const path = []; + + function* traverse(node) { + if (!node) return; + path.push(node.value); + visited.push(node.value); + yield { + type: "visit", + message: `Visiting ${node.value} (Preorder: Root → Left → Right)`, + current: node.value, visited: [...visited], path: [...path], subtree: "root" + }; + if (node.left) { + yield { + type: "traverse_left", + message: `Moving to left subtree of ${node.value}`, + current: node.value, visited: [...visited],path: [...path] + }; + yield* traverse(node.left); + } + + if (node.right) { + yield { + type: "traverse_right", + message: `Moving to right subtree of ${node.value}`, + current: node.value, visited: [...visited], path: [...path] }; + yield* traverse(node.right); + } + path.pop(); + } + + yield* traverse(root); + + yield { type: "complete", message: `Preorder traversal complete: ${visited.join(" → ")}`, current: null, visited: visited, path: []}; +} + +//postorder +export function* postorderTraversal(root) { + if (!root) { + yield { type: "error", message: "Tree is empty", current: null, visited: [], path: [] }; + return; + } + const visited = []; + const path = []; + + function* traverse(node) { + if (!node) return; + path.push(node.value); + if (node.left) { + yield { + type: "traverse_left", + message: `Moving to left subtree of ${node.value}`, + current: node.value, + visited: [...visited], + path: [...path] + }; + yield* traverse(node.left); + } + + if (node.right) { + yield { + type: "traverse_right", + message: `Moving to right subtree of ${node.value}`, + current: node.value, + visited: [...visited], + path: [...path] + }; + yield* traverse(node.right); + } + visited.push(node.value); + yield { + type: "visit", + message: `Visiting ${node.value} (Postorder: Left → Right → Root)`, + current: node.value, + visited: [...visited], + path: [...path], + subtree: "root" + }; + path.pop(); + } + yield* traverse(root); + yield { + type: "complete", + message: `Postorder traversal complete: ${visited.join(" → ")}`, + current: null, + visited: visited, + path: [] + }; +} +export function* treeTraversalGenerator(treeData, traversalType = "all") { + const root = buildTreeFromLevelOrder(treeData); + if (!root) { + yield { + type: "error", + message: "Invalid tree data", + tree: null, + step: null + }; + return; + } + if (traversalType === "all") { + const traversals = [ + { name: "inorder", generator: inorderTraversal }, + { name: "preorder", generator: preorderTraversal }, + { name: "postorder", generator: postorderTraversal } + ]; + const results = { + inorder: { visited: [], steps: [] }, + preorder: { visited: [], steps: [] }, + postorder: { visited: [], steps: [] } + }; + for (const { name, generator } of traversals) { + for (const step of generator(cloneTree(root))) { + results[name].steps.push(step); + if (step.type === "complete") { + results[name].visited = step.visited; + } + } + } + yield { + type: "sync_step", + tree: cloneTree(root), + stepIndex: -1, + inorder: { type: "start", message: "Starting Inorder traversal", visited: [], path: [] }, + preorder: { type: "start", message: "Starting Preorder traversal", visited: [], path: [] }, + postorder: { type: "start", message: "Starting Postorder traversal", visited: [], path: [] } + }; + const maxSteps = Math.max( + results.inorder.steps.length, + results.preorder.steps.length, + results.postorder.steps.length + ); + for (let i = 0; i < maxSteps; i++) { + const step = { + type: "sync_step", + tree: cloneTree(root), + stepIndex: i, + inorder: results.inorder.steps[i] || results.inorder.steps[results.inorder.steps.length - 1] || null, + preorder: results.preorder.steps[i] || results.preorder.steps[results.preorder.steps.length - 1] || null, + postorder: results.postorder.steps[i] || results.postorder.steps[results.postorder.steps.length - 1] || null + }; + yield step; + } + yield { + type: "complete", + tree: cloneTree(root), + inorder: results.inorder.visited, + preorder: results.preorder.visited, + postorder: results.postorder.visited, + message: `All traversals complete. Inorder: [${results.inorder.visited.join(", ")}], Preorder: [${results.preorder.visited.join(", ")}], Postorder: [${results.postorder.visited.join(", ")}]` + }; + } else { + let generator; + if (traversalType === "inorder") { + generator = inorderTraversal; + } else if (traversalType === "preorder") { + generator = preorderTraversal; + } else if (traversalType === "postorder") { + generator = postorderTraversal; + } else { + yield { + type: "error", + message: "Invalid traversal type", + tree: null, + step: null + }; + return; + } + for (const step of generator(cloneTree(root))) { + yield { + type: "single_step", + tree: cloneTree(root), + traversal: traversalType, + step: step + }; + } + } +} +export { buildTreeFromLevelOrder, cloneTree, getAllNodeIds }; + diff --git a/src/components/Tree/TreeTraversalVisualizer.jsx b/src/components/Tree/TreeTraversalVisualizer.jsx new file mode 100644 index 0000000..6440e78 --- /dev/null +++ b/src/components/Tree/TreeTraversalVisualizer.jsx @@ -0,0 +1,296 @@ +import React, { useEffect, useRef, useState, useMemo } from "react"; + +export default function TreeTraversalVisualizer({ + tree = null, + currentStep = null, + traversalType = "all", + nodeSize = 60, + gapY = 100, + gapX = 80 +}) { + const containerRef = useRef(null); + const [layoutNodes, setLayoutNodes] = useState([]); + const [containerWidth, setContainerWidth] = useState(1000); + const [containerHeight, setContainerHeight] = useState(600); + const nodeIdMapRef = useRef(new WeakMap()); + const nextIdRef = useRef(1); + const getNodeVizId = (node) => { + if (!node) return null; + if (node.id !== undefined && node.id !== null) return String(node.id); + const map = nodeIdMapRef.current; + if (map.has(node)) return map.get(node); + const id = `viz_${nextIdRef.current++}`; + map.set(node, id); + return id; + }; + useEffect(() => { + if (!tree) { + setLayoutNodes([]); + return; + } + + const rect = containerRef.current?.getBoundingClientRect(); + if (rect) { + setContainerWidth(rect.width || 1000); + setContainerHeight(rect.height || 600); + } + + //tree height + function getTreeHeight(node) { + if (!node) return 0; + return 1 + Math.max( + getTreeHeight(node.left), + getTreeHeight(node.right) + ); + } + + const treeHeight = getTreeHeight(tree); + setContainerHeight(Math.max(treeHeight * gapY + 200, 400)); + const layoutMap = new Map(); + const usableWidth = Math.max(containerWidth - 100, 800); + const maxDepth = treeHeight - 1; + const baseSpacing = Math.max(80, gapX); + const inorderPositions = new Map(); + let inorderIndex = 0; + + function assignInorderPositions(node) { + if (!node) return; + assignInorderPositions(node.left); + inorderPositions.set(node, inorderIndex++); + assignInorderPositions(node.right); + } + assignInorderPositions(tree); + + function assignPositions(node, depth) { + if (!node) return; + + const id = getNodeVizId(node); + const inorderPos = inorderPositions.get(node); + const totalNodes = inorderPositions.size; + + const spacing = usableWidth / (totalNodes + 1); + const x = 50 + spacing * (inorderPos + 1); + const y = depth * gapY + 80; + + layoutMap.set(id, { id, value: node.value, rawNode: node, x, y, depth }); + assignPositions(node.left, depth + 1); + assignPositions(node.right, depth + 1); + } + + assignPositions(tree, 0); + + const layoutArray = Array.from(layoutMap.values()); + setLayoutNodes(layoutArray); + }, [tree, containerWidth, gapY, gapX, nodeSize]); + + useEffect(() => { + const onResize = () => { + const rect = containerRef.current?.getBoundingClientRect(); + if (rect) { + setContainerWidth(rect.width || 1000); + } + }; + window.addEventListener("resize", onResize); + onResize(); + return () => window.removeEventListener("resize", onResize); + }, []); + + // Build edges + const edges = useMemo(() => { + if (!tree || !layoutNodes.length) return []; + const edgesArray = []; + const nodeMap = new Map(layoutNodes.map(n => [n.id, n])); + + function buildEdges(node) { + if (!node) return; + const fromId = getNodeVizId(node); + const fromNode = nodeMap.get(fromId); + + if (!fromNode) return; + if (node.left) { + const toId = getNodeVizId(node.left); + const toNode = nodeMap.get(toId); + if (toNode) { + edgesArray.push({ from: fromNode, to: toNode, side: "left" }); + } + buildEdges(node.left); + } + if (node.right) { + const toId = getNodeVizId(node.right); + const toNode = nodeMap.get(toId); + if (toNode) { + edgesArray.push({ from: fromNode, to: toNode, side: "right" }); + } + buildEdges(node.right); + } + } + buildEdges(tree); + return edgesArray; + }, [tree, layoutNodes]); + + const getNodeClass = (nodeLayout) => { + if (!currentStep) { + return "bg-gray-700 text-white border-2 border-gray-600"; + } + let step = null; + if (currentStep.type === "single_step" && currentStep.step) { + step = currentStep.step; + } else if (currentStep.step) { + step = currentStep.step; + } else { + step = currentStep; + } + if (!step) { + return "bg-gray-700 text-white border-2 border-gray-600"; + } + + const stepType = step.type; + const currentId = step.current; + const path = step.path || []; + const visited = step.visited || []; + const nodeValue = nodeLayout.rawNode?.value ?? nodeLayout.value; + const nodeValueStr = String(nodeValue); + const currentIdStr = currentId ? String(currentId) : null; + + if (currentIdStr && currentIdStr === nodeValueStr) { + if (stepType === "visit") { + return "bg-emerald-500 text-white border-4 border-emerald-300 ring-4 ring-emerald-400 ring-opacity-70 shadow-xl transform scale-110 z-20"; + } else if (stepType === "traverse_left" || stepType === "traverse_right") { + return "bg-blue-500 text-white border-4 border-blue-300 ring-2 ring-blue-400 ring-opacity-60 z-10"; + } + } + const pathMatches = path.some(p => String(p) === nodeValueStr); + if (pathMatches && stepType !== "visit" && (!currentIdStr || currentIdStr !== nodeValueStr)) { + return "bg-indigo-600 text-white border-2 border-indigo-400"; + } + if (stepType === "visit" && visited && visited.length > 0 && visited.includes(nodeValue)) { + if (!currentIdStr || currentIdStr !== nodeValueStr) { + return "bg-violet-600 text-white border-2 border-violet-400 opacity-90"; + } + } + return "bg-gray-700 text-white border-2 border-gray-600"; + }; + + // Render node + const Node = ({ n }) => { + const cls = getNodeClass(n); + const radius = nodeSize / 2; + + return ( +
+ {String(n.value)} +
+ ); + }; + const SvgEdges = () => { + if (!layoutNodes.length) return null; + return ( + + + + + + + + + + + + {edges.map((e, i) => { + const x1 = e.from.x; + const y1 = e.from.y + nodeSize * 0.45; + const x2 = e.to.x; + const y2 = e.to.y - nodeSize * 0.45; + + let step = null; + if (currentStep?.type === "single_step" && currentStep.step) step = currentStep.step; + else if (currentStep?.step) step = currentStep.step; + else step = currentStep; + + const path = step?.path || []; + const fromValue = e.from.rawNode?.value ?? e.from.value; + const toValue = e.to.rawNode?.value ?? e.to.value; + const isInPath = path.some(p => String(p) === String(fromValue)) && + path.some(p => String(p) === String(toValue)); + const midX = (x1 + x2) / 2; + const controlY = Math.min(y1, y2) - 30; + const d = `M ${x1} ${y1} Q ${midX} ${controlY} ${x2} ${y2}`; + + return ( + + ); + })} + + ); + }; + + if (!tree) { + return ( +
+
+
🌳
+
No tree data to visualize
+
+
+ ); + } + const getVisitedArray = () => { + if (!currentStep) return []; + let step = null; + if (currentStep.type === "single_step" && currentStep.step) step = currentStep.step; + else if (currentStep.step) step = currentStep.step; + else step = currentStep; + return step?.visited || []; + }; + const visitedArray = getVisitedArray(); + return ( +
+
+ + {layoutNodes.map((n) => ( + + ))} + {visitedArray.length > 0 && ( +
+
Traversal Sequence:
+
+ {visitedArray.map((val, idx) => { + const isLast = idx === visitedArray.length - 1; + return ( + + + {val} + {idx < visitedArray.length - 1 && ( + )} + + ); + })} +
+
+ )} +
+
+ ); +} + diff --git a/src/pages/Homepage.jsx b/src/pages/Homepage.jsx index 318180d..f643a23 100644 --- a/src/pages/Homepage.jsx +++ b/src/pages/Homepage.jsx @@ -80,6 +80,15 @@ const sections = [ link: "/dynamic-programming", flag: false, }, + { + title: "Tree Traversal", + description: + "Visualize inorder, preorder, and postorder tree traversals step by step.", + phase: "Phase 2", + img: "/Tree-Traversal.png", + link: "/tree", + flag: false, + }, ]; const Homepage = () => { diff --git a/src/pages/Tree/TreeTraversal.jsx b/src/pages/Tree/TreeTraversal.jsx new file mode 100644 index 0000000..e3b2052 --- /dev/null +++ b/src/pages/Tree/TreeTraversal.jsx @@ -0,0 +1,303 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Toaster, toast } from "react-hot-toast"; +import { ArrowLeft, Play, Pause, StepForward, RotateCcw, TreePine } from "lucide-react"; +import TreeTraversalVisualizer from "../../components/Tree/TreeTraversalVisualizer"; +import { treeTraversalGenerator, buildTreeFromLevelOrder } from "../../algorithms/Tree/treeTraversal"; + +export default function TreeTraversal() { + const [selectedTraversal, setSelectedTraversal] = useState(""); + const [treeData, setTreeData] = useState([1, 2, 3, 4, 5, null, 6, 7]); + const [inputString, setInputString] = useState("1,2,3,4,5,-1,6,7"); + const [speed, setSpeed] = useState(800); + const [isPlaying, setIsPlaying] = useState(false); + const [currentStep, setCurrentStep] = useState(null); + const [steps, setSteps] = useState([]); + const [stepIndex, setStepIndex] = useState(-1); + const [tree, setTree] = useState(null); + const [finalResult, setFinalResult] = useState([]); + const generatorRef = useRef(null); + const timerRef = useRef(null); + const stepsRef = useRef([]); + + useEffect(() => { + const root = buildTreeFromLevelOrder(treeData); + setTree(root); + if (root && selectedTraversal) { + setSteps([]); + setStepIndex(-1); + setCurrentStep(null); + setIsPlaying(false); + } + }, [treeData, selectedTraversal]); + + const handleInputChange = (e) => { + const value = e.target.value; + setInputString(value); + try { + const parsed = value + .split(",") + .map((p) => + ["-1", "", "null"].includes(p.trim().toLowerCase()) + ? null + : Number(p.trim()) + ); + setTreeData(parsed); + setSteps([]); + setStepIndex(-1); + setCurrentStep(null); + setIsPlaying(false); + } catch (err) { + console.error("Error parsing input:", err); + } + }; + + const loadDemo = () => { + const demo = [1, 2, 3, 4, 5, null, 6, 7, null, null, null, null, null, 8, 9]; + setTreeData(demo); + setInputString("1,2,3,4,5,-1,6,7,-1,-1,-1,-1,-1,8,9"); + setSteps([]); + setStepIndex(-1); + setCurrentStep(null); + setIsPlaying(false); + }; + + useEffect(() => { + if (!selectedTraversal || !tree) return; + setIsPlaying(false); + clearTimeout(timerRef.current); + + const newSteps = []; + generatorRef.current = treeTraversalGenerator(treeData, selectedTraversal); + try { + for (const step of generatorRef.current) newSteps.push(step); + stepsRef.current = newSteps; + setSteps(newSteps); + setStepIndex(0); + setCurrentStep(newSteps[0] || null); + + const lastStep = newSteps.at(-1); + const visited = lastStep?.step?.visited ?? []; + setFinalResult(visited); + } catch (error) { + console.error("Error generating steps:", error); + toast.error("Error generating traversal steps"); + } + }, [selectedTraversal, treeData, tree]); + + useEffect(() => { + if (isPlaying && stepIndex < steps.length - 1) { + timerRef.current = setTimeout(() => { + setStepIndex((prev) => + prev + 1 < steps.length ? prev + 1 : steps.length - 1 + ); + }, speed); + } else if (stepIndex >= steps.length - 1) { + setIsPlaying(false); + } + return () => clearTimeout(timerRef.current); + }, [isPlaying, stepIndex, steps.length, speed]); + + useEffect(() => { + if (stepIndex >= 0 && stepIndex < steps.length) + setCurrentStep(steps[stepIndex]); + }, [stepIndex, steps]); + + const togglePlay = () => { + if (steps.length === 0) return; + if (stepIndex >= steps.length - 1) { + setStepIndex(0); + setIsPlaying(true); + } else setIsPlaying(!isPlaying); + }; + const stepForward = () => { + if (stepIndex < steps.length - 1) { + setStepIndex((p) => p + 1); + setIsPlaying(false); + } + }; + const stepBackward = () => { + if (stepIndex > 0) { + setStepIndex((p) => p - 1); + setIsPlaying(false); + } + }; + const reset = () => { + setIsPlaying(false); + clearTimeout(timerRef.current); + setSteps([]); + setStepIndex(-1); + setCurrentStep(null); + setFinalResult([]); + setSelectedTraversal(""); + }; + + const getTraversalDescription = (type) => + type === "inorder" + ? "Left → Root → Right" + : type === "preorder" + ? "Root → Left → Right" + : "Left → Right → Root"; + + if (!selectedTraversal) { + return ( +
+ +

+ Tree Traversal Algorithms +

+

+ Select a traversal method to visualize how it explores tree nodes — + simple, elegant, and interactive. +

+ +
+ {[ + { + id: "inorder", + title: "Inorder Traversal", + desc: "Left → Root → Right", + }, + { + id: "preorder", + title: "Preorder Traversal", + desc: "Root → Left → Right", + }, + { + id: "postorder", + title: "Postorder Traversal", + desc: "Left → Right → Root", + }, + ].map((t) => ( +
setSelectedTraversal(t.id)} + className={`relative bg-gray-700/80 backdrop-blur-md rounded-2xl p-6 text-center border border-gray-800 cursor-pointer transition-all duration-300 hover:scale-105`} + > +
🌳
+

{t.title}

+

{t.desc}

+
+ ))} +
+
+ ); + } + return ( +
+ + {/* Control Bar */} +
+
+ + + +
+ +
+ {["inorder", "preorder", "postorder"].map((t) => ( + + ))} +
+
+ + setSpeed(Number(e.target.value))} className="accent-blue-500 w-24" /> + {speed}ms +
+
+ + + + +
+
+ +
+
+

+ {selectedTraversal.charAt(0).toUpperCase() + + selectedTraversal.slice(1)}{" "} + Traversal +

+

+ {getTraversalDescription(selectedTraversal)} +

+
+ {finalResult.length > 0 && ( +
+

+ Final Sequence +

+
+ {finalResult.map((v, i) => ( + + + {v} + + {i < finalResult.length - 1 && ( + + )} + + ))} +
+
+ )} +
+
+ {tree ? ( + + ) : ( +
Loading Tree...
+ )} +
+
+ ); +} diff --git a/src/pages/Tree/Treepage.jsx b/src/pages/Tree/Treepage.jsx new file mode 100644 index 0000000..9df62e8 --- /dev/null +++ b/src/pages/Tree/Treepage.jsx @@ -0,0 +1,67 @@ +import React, { useState } from "react"; +import { TreePine } from "lucide-react"; +import TreeTraversal from "./TreeTraversal"; + +export default function Treepage() { + const [selectedAlgo, setSelectedAlgo] = useState(""); + + const renderAlgorithm = () => { + switch (selectedAlgo) { + case "tree-traversal": + return ; + default: + return ( +
+
+
+
+ +
+

+ Tree Algorithm Visualizer +

+

+ Select a tree algorithm from the sidebar to begin + visualization. Watch how nodes are traversed step by step! +

+
+
+
+ ); + } + }; + + return ( +
+ {/* Sidebar */} +
+

+ Tree Panel +

+ + + + + ← Back to Home + +
+
+
+ {renderAlgorithm()} +
+
+
+ ); +}