From c9a0c1255ccd498cf994a2c07ac8b596b00eb98c Mon Sep 17 00:00:00 2001 From: indar suthar Date: Sat, 15 Nov 2025 16:06:06 +0530 Subject: [PATCH] feat: build tree from preorder and inorder --- .../Tree/buildTreeFromPreorderInorder.js | 131 ++++++++ src/components/Tree/BuildTreeVisualizer.jsx | 228 ++++++++++++++ .../Tree/BuildTreeFromPreorderInorder.jsx | 282 ++++++++++++++++++ src/pages/Tree/Treepage.jsx | 4 + 4 files changed, 645 insertions(+) create mode 100644 src/algorithms/Tree/buildTreeFromPreorderInorder.js create mode 100644 src/components/Tree/BuildTreeVisualizer.jsx create mode 100644 src/pages/Tree/BuildTreeFromPreorderInorder.jsx diff --git a/src/algorithms/Tree/buildTreeFromPreorderInorder.js b/src/algorithms/Tree/buildTreeFromPreorderInorder.js new file mode 100644 index 0000000..1b90e66 --- /dev/null +++ b/src/algorithms/Tree/buildTreeFromPreorderInorder.js @@ -0,0 +1,131 @@ +export function* buildTreeFromPreorderInorder(preorder, inorder) { + const steps = []; + let stepIndex = 0; + + function* buildTree(preStart, preEnd, inStart, inEnd, parent = null) { + if (preStart > preEnd || inStart > inEnd) { + yield { + type: "base_case", + message: "Base case: Empty subtree", + preorder: preorder ,inorder: inorder ,preStart, inEnd, inStart, preEnd, root: null, tree: null, parent + }; + return null; + } + + const rootValue = preorder[preStart]; + + yield { + type: "select_root", + message: `Selecting root: ${rootValue} (first element in preorder)`, + preorder: preorder ,inorder: inorder ,preStart, inEnd, inStart, preEnd, rootValue, tree: null, parent + }; + const rootIndexInInorder = inorder.indexOf(rootValue); + + yield { + type: "find_in_inorder", + message: `Finding ${rootValue} in inorder at index ${rootIndexInInorder}`, + preorder: preorder ,inorder: inorder ,preStart, inEnd, inStart, preEnd, rootValue, rootIndexInInorder, tree: null, parent + }; + + const leftSize = rootIndexInInorder - inStart; + const rightSize = inEnd - rootIndexInInorder; + + yield { + type: "calculate_sizes", + message: `Left subtree size: ${leftSize}, Right subtree size: ${rightSize}`, + preorder: preorder,inorder: inorder, + preStart, inEnd, inStart, preEnd, rootValue, rootIndexInInorder, leftSize, rightSize, tree: null, parent + }; + + const root = { + value: rootValue, + left: null, right: null, + id: `node_${rootValue}_${Date.now()}_${Math.random()}` + }; + + yield { + type: "create_node", + message: `Creating node with value ${rootValue}`, + preorder: preorder ,inorder: inorder ,preStart, inEnd, inStart, preEnd, rootValue, rootIndexInInorder, leftSize, rightSize, tree: root, parent + }; + + if (leftSize > 0) { + yield { + type: "build_left", + message: `Building left subtree: preorder[${preStart + 1}..${preStart + leftSize}], inorder[${inStart}..${rootIndexInInorder - 1}]`, + preorder: preorder, inorder: inorder, + preStart, preEnd, inStart, inEnd, root: rootValue, + rootIndexInInorder, leftSize, rightSize, tree: root, parent + }; + + const leftSubtree = yield* buildTree( + preStart + 1, + preStart + leftSize, inStart, + rootIndexInInorder - 1, root + ); + root.left = leftSubtree; + + yield { + type: "attach_left", + message: `Attached left subtree to ${rootValue}`, + preorder: preorder, inorder: inorder, + preStart, preEnd, inStart, inEnd, + root: rootValue, tree: root, parent + }; + } + if (rightSize > 0) { + yield { + type: "build_right", + message: `Building right subtree: preorder[${preStart + leftSize + 1}..${preEnd}], inorder[${rootIndexInInorder + 1}..${inEnd}]`, + preorder: preorder, inorder: inorder, + preStart, preEnd, inStart, inEnd, root: rootValue, + rootIndexInInorder, leftSize, rightSize, tree: root, parent + }; + + const rightSubtree = yield* buildTree( + preStart + leftSize + 1, preEnd, + rootIndexInInorder + 1, inEnd, root + ); + root.right = rightSubtree; + + yield { + type: "attach_right", + message: `Attached right subtree to ${rootValue}`, + preorder: preorder, + inorder: inorder, + preStart, preEnd, inStart, inEnd, root: rootValue, tree: root, parent + }; + } + + yield { + type: "complete_subtree", + message: `Completed subtree rooted at ${rootValue}`, + preorder: preorder, + inorder: inorder, preStart, preEnd, inStart, inEnd, root: rootValue, tree: root, parent + }; + return root; + } + + yield { + type: "start", + message: `Starting to build tree from preorder: [${preorder.join(", ")}] and inorder: [${inorder.join(", ")}]`, + preorder: preorder, inorder: inorder, tree: null + }; + const root = yield* buildTree(0, preorder.length - 1, 0, inorder.length - 1); + + yield { + type: "complete", + message: "Tree construction complete!", + preorder: preorder, inorder: inorder, tree: root + }; + return root; +} +export function cloneTree(node) { + if (!node) return null; + return { + value: node.value, + left: cloneTree(node.left), + right: cloneTree(node.right), + id: node.id + }; +} diff --git a/src/components/Tree/BuildTreeVisualizer.jsx b/src/components/Tree/BuildTreeVisualizer.jsx new file mode 100644 index 0000000..2147d44 --- /dev/null +++ b/src/components/Tree/BuildTreeVisualizer.jsx @@ -0,0 +1,228 @@ +import React, { useEffect, useRef, useState, useMemo } from "react"; + +export default function BuildTreeVisualizer({ + tree = null, currentStep = null, preorder = [], inorder = [], + nodeSize = 60, gapY = 100, gapX = 80 +}) { + const containerRef = useRef(null); + const [layoutNodes, setLayoutNodes] = useState([]); + const [containerWidth, setContainerWidth] = useState(1000); + const [containerHeight, setContainerHeight] = useState(600); + + useEffect(() => { + if (!tree) { + setLayoutNodes([]); + return; + } + const rect = containerRef.current?.getBoundingClientRect(); + if (rect) { + setContainerWidth(rect.width || 1000); + setContainerHeight(rect.height || 600); + } + 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 inorderPositions = new Map(); + let inorderIndex = 0; + + function assignInorderPositions(node) { + if (!node) return; + assignInorderPositions(node.left); + inorderPositions.set(node, inorderIndex++); + assignInorderPositions(node.right); + } + assignInorderPositions(tree); + + const totalNodes = inorderPositions.size; + function assignPositions(node, depth) { + if (!node) return; + + const inorderPos = inorderPositions.get(node) ?? 0; + const spacing = usableWidth / (totalNodes + 1); + const x = 50 + spacing * (inorderPos + 1); + const y = depth * gapY + 80; + + const nodeId = node.id || `node_${node.value}_${depth}_${inorderPos}`; + layoutMap.set(nodeId, { value: node.value, node, x, y, depth, id: nodeId }); + 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]); + + useEffect(() => { + const onResize = () => { + const rect = containerRef.current?.getBoundingClientRect(); + if (rect) { + setContainerWidth(rect.width || 1000); + } + }; + window.addEventListener("resize", onResize); + onResize(); + return () => window.removeEventListener("resize", onResize); + }, []); + + const edges = useMemo(() => { + if (!tree || !layoutNodes.length) return []; + const edgesArray = []; + const nodeMap = new Map(layoutNodes.map(n => [n.node, n])); + + function buildEdges(node) { + if (!node) return; + const fromNode = nodeMap.get(node); + + if (!fromNode) return; + if (node.left) { + const toNode = nodeMap.get(node.left); + if (toNode) { + edgesArray.push({ from: fromNode, to: toNode, side: "left" }); + } + buildEdges(node.left); + } + if (node.right) { + const toNode = nodeMap.get(node.right); + if (toNode) { + edgesArray.push({ from: fromNode, to: toNode, side: "right" }); + } + buildEdges(node.right); + } + } + buildEdges(tree); + return edgesArray; + }, [tree, layoutNodes]); + + const getNodeClass = (nodeLayout) => { + if (!currentStep || !currentStep.tree) { + return "bg-gray-700 text-white border-2 border-gray-600"; + } + + const step = currentStep; + const stepType = step.type; + const rootValue = step.root; + const nodeValue = nodeLayout.value; + + if (rootValue === nodeValue) { + if (stepType === "select_root" || stepType === "create_node") { + return "bg-blue-500 text-white border-4 border-blue-300 ring-4 ring-blue-400 ring-opacity-70 shadow-xl transform scale-110 z-20"; + } else if (stepType === "find_in_inorder" || stepType === "calculate_sizes") { + return "bg-yellow-500 text-white border-4 border-yellow-300 ring-2 ring-yellow-400 ring-opacity-60 z-10"; + } else if (stepType === "attach_left" || stepType === "attach_right") { + return "bg-green-500 text-white border-4 border-green-300 ring-2 ring-green-400 ring-opacity-60 z-10"; + } else if (stepType === "complete_subtree") { + return "bg-emerald-500 text-white border-4 border-emerald-300 ring-2 ring-emerald-400 ring-opacity-60 z-10"; + } + } + if (step.tree) { + const isInTree = (node, targetTree) => { + if (!targetTree) return false; + if (targetTree.value === node.value) return true; + return isInTree(node, targetTree.left) || isInTree(node, targetTree.right); + }; + + if (isInTree(nodeLayout.node, step.tree) && rootValue !== nodeValue) { + return "bg-indigo-600 text-white border-2 border-indigo-400 opacity-90"; + } + } + return "bg-gray-700 text-white border-2 border-gray-600"; + }; + + 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; + 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 to visualize
+
+
+ ); + } + return ( +
+
+ + {layoutNodes.map((n) => ( + + ))} +
+
+ ); +} + diff --git a/src/pages/Tree/BuildTreeFromPreorderInorder.jsx b/src/pages/Tree/BuildTreeFromPreorderInorder.jsx new file mode 100644 index 0000000..13b9d82 --- /dev/null +++ b/src/pages/Tree/BuildTreeFromPreorderInorder.jsx @@ -0,0 +1,282 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Toaster, toast } from "react-hot-toast"; +import { ArrowLeft, Play, Pause, StepForward, RotateCcw } from "lucide-react"; +import BuildTreeVisualizer from "../../components/Tree/BuildTreeVisualizer"; +import { buildTreeFromPreorderInorder, cloneTree } from "../../algorithms/Tree/buildTreeFromPreorderInorder"; + +export default function BuildTreeFromPreorderInorder() { + const [preorder, setPreorder] = useState([3, 9, 20, 15, 7]); + const [inorder, setInorder] = useState([9, 3, 15, 20, 7]); + const [preorderInput, setPreorderInput] = useState("3,9,20,15,7"); + const [inorderInput, setInorderInput] = useState("9,3,15,20,7"); + const [speed, setSpeed] = useState(1000); + 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 generatorRef = useRef(null); + const timerRef = useRef(null); + + useEffect(() => { + if (preorder.length === 0 || inorder.length === 0) { + setTree(null); + setSteps([]); + setStepIndex(-1); + setCurrentStep(null); + return; + } + if (preorder.length !== inorder.length) { + toast.error("Preorder and Inorder arrays must have the same length!"); + return; + } + const preorderSorted = [...preorder].sort((a, b) => a - b); + const inorderSorted = [...inorder].sort((a, b) => a - b); + if (JSON.stringify(preorderSorted) !== JSON.stringify(inorderSorted)) { + toast.error("Preorder and Inorder arrays must contain the same elements!"); + return; + } + setIsPlaying(false); + clearTimeout(timerRef.current); + const newSteps = []; + generatorRef.current = buildTreeFromPreorderInorder(preorder, inorder); + try { + for (const step of generatorRef.current) { + newSteps.push(step); + } + setSteps(newSteps); + setStepIndex(0); + setCurrentStep(newSteps[0] || null); + + const lastStep = newSteps[newSteps.length - 1]; + if (lastStep && lastStep.tree) { + setTree(cloneTree(lastStep.tree)); + } + } catch (error) { + console.error("Error generating steps:", error); + toast.error("Error generating construction steps"); + } + }, [preorder, inorder]); + + 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]); + if (steps[stepIndex].tree) { + setTree(cloneTree(steps[stepIndex].tree)); + } + } + }, [stepIndex, steps]); + const handlePreorderChange = (e) => { + const value = e.target.value; + setPreorderInput(value); + try { + const parsed = value + .split(",") + .map((p) => p.trim()) + .filter((p) => p !== "") + .map((p) => Number(p)); + if (parsed.length > 0) { + setPreorder(parsed); + } + } catch (err) { + console.error("Error parsing preorder:", err); + } + }; + + const handleInorderChange = (e) => { + const value = e.target.value; + setInorderInput(value); + try { + const parsed = value + .split(",") + .map((p) => p.trim()) + .filter((p) => p !== "") + .map((p) => Number(p)); + if (parsed.length > 0) { + setInorder(parsed); + } + } catch (err) { + console.error("Error parsing inorder:", err); + } + }; + const loadDemo = () => { + setPreorder([3, 9, 20, 15, 7]); + setInorder([9, 3, 15, 20, 7]); + setPreorderInput("3,9,20,15,7"); + setInorderInput("9,3,15,20,7"); + }; + const togglePlay = () => { + if (steps.length === 0) return; + if (stepIndex >= steps.length - 1) { + setStepIndex(0); + setIsPlaying(true); + } else { + setIsPlaying(!isPlaying); + } + }; + const reset = () => { + setIsPlaying(false); + clearTimeout(timerRef.current); + setSteps([]); + setStepIndex(-1); + setCurrentStep(null); + setTree(null); + }; + return ( +
+ +
+
+ +
+ + +
+ +
+ + +
+
+
+ + setSpeed(Number(e.target.value))} className="accent-blue-500 w-24"/> + {speed}ms +
+
+ + +
+
+ +
+
+

+ Construct Binary Tree from Preorder & Inorder +

+
+ {currentStep && ( +
+
+ Step {stepIndex + 1} / {steps.length} +
+
+ {currentStep.type?.replace(/_/g, " ").toUpperCase()} +
+
+ )} +
+ {currentStep && ( +
+
+
+
Preorder Array:
+
+ {currentStep.preorder?.map((val, idx) => { + const isInRange = + idx >= (currentStep.preStart || 0) && + idx <= (currentStep.preEnd || currentStep.preorder.length - 1); + const isRoot = idx === (currentStep.preStart || 0); + return ( + + {val} + + ); + })} +
+
+
+
Inorder Array:
+
+ {currentStep.inorder?.map((val, idx) => { + const isInRange = + idx >= (currentStep.inStart || 0) && + idx <= (currentStep.inEnd || currentStep.inorder.length - 1); + const isRoot = val === currentStep.root; + return ( + + {val} + + ); + })} +
+
+
+
+ )} + {currentStep && currentStep.message && ( +
+
+ {currentStep.message} +
+
+ )} +
+ {tree ? ( + + ) : ( +
+ {preorder.length === 0 || inorder.length === 0 + ? "Enter preorder and inorder arrays to start" + : "Loading tree construction..."} +
+ )} +
+
+ ); +} diff --git a/src/pages/Tree/Treepage.jsx b/src/pages/Tree/Treepage.jsx index 9df62e8..02063e1 100644 --- a/src/pages/Tree/Treepage.jsx +++ b/src/pages/Tree/Treepage.jsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { TreePine } from "lucide-react"; import TreeTraversal from "./TreeTraversal"; +import BuildTreeFromPreorderInorder from "./BuildTreeFromPreorderInorder"; export default function Treepage() { const [selectedAlgo, setSelectedAlgo] = useState(""); @@ -9,6 +10,8 @@ export default function Treepage() { switch (selectedAlgo) { case "tree-traversal": return ; + case "build-tree": + return ; default: return (
@@ -46,6 +49,7 @@ export default function Treepage() { > +