diff --git a/src/algorithms/dataStructure/binarytree.js b/src/algorithms/dataStructure/binarytree.js new file mode 100644 index 0000000..1ca74dd --- /dev/null +++ b/src/algorithms/dataStructure/binarytree.js @@ -0,0 +1,533 @@ +// src/algorithms/dataStructure/binarytree.js +// Generator that yields step objects for visualization for Binary Tree and BST. +// Each yielded step: +// { +// tree: , +// nodesById: { id: { value, id, leftId, rightId } }, +// highlight: { type, id?, message?, path? }, +// message?: string, +// done?: boolean +// } + +let GLOBAL_ID = 1; +function nextId() { + return `n${GLOBAL_ID++}`; +} + +export function resetIdCounter() { + GLOBAL_ID = 1; +} + +function cloneNode(node) { + if (!node) return null; + const cloned = { id: node.id, value: node.value, left: null, right: null }; + cloned.left = cloneNode(node.left); + cloned.right = cloneNode(node.right); + return cloned; +} + +function treeToMap(root) { + const map = {}; + if (!root) return map; + const q = [root]; + while (q.length) { + const n = q.shift(); + map[n.id] = { id: n.id, value: n.value, leftId: n.left ? n.left.id : null, rightId: n.right ? n.right.id : null }; + if (n.left) q.push(n.left); + if (n.right) q.push(n.right); + } + return map; +} + +// Build from level order array where null (or "-1") means no node +function buildTreeFromLevelOrderArr(arr) { + if (!arr || arr.length === 0) return null; + const nodes = arr.map((v) => (v === null ? null : { id: nextId(), value: v, left: null, right: null })); + if (!nodes[0]) return null; + let root = nodes[0]; + const q = [root]; + let i = 1; + while (q.length && i < nodes.length) { + const cur = q.shift(); + // left + if (i < nodes.length) { + cur.left = nodes[i] || null; + if (cur.left) q.push(cur.left); + i++; + } + // right + if (i < nodes.length) { + cur.right = nodes[i] || null; + if (cur.right) q.push(cur.right); + i++; + } + } + return root; +} + +// Helper: BFS find node by value (first match). Returns { node, parent, dir, path } +function findNodeByValue(root, value) { + if (!root) return { node: null, parent: null, dir: null, path: [] }; + const q = [{ node: root, parent: null, dir: null, path: [root.id] }]; + while (q.length) { + const { node, parent, dir, path } = q.shift(); + if (String(node.value) === String(value)) return { node, parent, dir, path: [...path] }; + if (node.left) q.push({ node: node.left, parent: node, dir: "left", path: path.concat(node.left.id) }); + if (node.right) q.push({ node: node.right, parent: node, dir: "right", path: path.concat(node.right.id) }); + } + return { node: null, parent: null, dir: null, path: [] }; +} + +// Helper: find deepest node and its parent +function findDeepestNode(root) { + if (!root) return { deepest: null, parent: null, dir: null, path: [] }; + const queue = [{ node: root, parent: null, dir: null, path: [root.id] }]; + let last = null; + while (queue.length) { + last = queue.shift(); + if (last.node.left) queue.push({ node: last.node.left, parent: last.node, dir: "left", path: last.path.concat(last.node.left.id) }); + if (last.node.right) queue.push({ node: last.node.right, parent: last.node, dir: "right", path: last.path.concat(last.node.right.id) }); + } + return { deepest: last.node, parent: last.parent, dir: last.dir, path: last.path || [last.node.id] }; +} + +// Helper: find node value by id (search tree) +function findValueById(root, id) { + if (!root || !id) return null; + const q = [root]; + while (q.length) { + const n = q.shift(); + if (String(n.id) === String(id)) return n.value; + if (n.left) q.push(n.left); + if (n.right) q.push(n.right); + } + return null; +} + +// BST insert with callback to record steps (cb(type, id, message, path)) +// returns newRoot (root may be replaced if previously null) +function bstInsert(root, value, cb) { + const newNode = { id: nextId(), value, left: null, right: null }; + if (!root) { + // do not call cb with a snapshot that expects "root" to be set (outer code will push snapshot) + // simply return new node so caller can set root and emit snapshot + return newNode; + } + let cur = root; + const pathIds = []; + while (true) { + pathIds.push(cur.id); + if (String(value) === String(cur.value)) { + if (cb) cb("compare", cur.id, `Duplicate ${value} found — ignoring`, [...pathIds]); + return root; + } else if ((!isNaN(Number(value)) && !isNaN(Number(cur.value)) && Number(value) < Number(cur.value)) || (isNaN(Number(value)) || isNaN(Number(cur.value)) && String(value) < String(cur.value))) { + if (cb) cb("compare", cur.id, `Compare ${value} < ${cur.value} → go left`, [...pathIds]); + if (!cur.left) { + cur.left = newNode; + if (cb) cb("insert", newNode.id, `Inserted ${value} as left child of ${cur.value}`, pathIds.concat(newNode.id)); + return root; + } + cur = cur.left; + } else { + if (cb) cb("compare", cur.id, `Compare ${value} > ${cur.value} → go right`, [...pathIds]); + if (!cur.right) { + cur.right = newNode; + if (cb) cb("insert", newNode.id, `Inserted ${value} as right child of ${cur.value}`, pathIds.concat(newNode.id)); + return root; + } + cur = cur.right; + } + } +} + +// BST search: returns { node, pathIds } +function bstSearch(root, value, cb) { + let cur = root; + const path = []; + while (cur) { + path.push(cur.id); + if (cb) cb("compare", cur.id, `Compare with ${cur.value}`, [...path]); + if (String(cur.value) === String(value)) { + if (cb) cb("found", cur.id, `Found ${value}`, [...path]); + return { node: cur, path }; + } + if ((!isNaN(Number(value)) && !isNaN(Number(cur.value)) && Number(value) < Number(cur.value)) || (isNaN(Number(value)) || isNaN(Number(cur.value)) && String(value) < String(cur.value))) cur = cur.left; + else cur = cur.right; + } + if (cb) cb("notfound", null, `Value ${value} not found`, [...path]); + return { node: null, path }; +} + +// BST delete: returns new root +function bstDelete(root, value, cb) { + let parent = null; + let cur = root; + const path = []; + while (cur && String(cur.value) !== String(value)) { + path.push(cur.id); + if (cb) cb("compare", cur.id, `Compare ${value} with ${cur.value}`, [...path]); + parent = cur; + if ((!isNaN(Number(value)) && !isNaN(Number(cur.value)) && Number(value) < Number(cur.value)) || (isNaN(Number(value)) || isNaN(Number(cur.value)) && String(value) < String(cur.value))) cur = cur.left; + else cur = cur.right; + } + if (!cur) { + if (cb) cb("error", null, `Value ${value} not found for deletion`, [...path]); + return root; + } + if (cb) cb("delete", cur.id, `Deleting ${cur.value}`, [...path]); + + // 0 or 1 child + if (!cur.left || !cur.right) { + const child = cur.left ? cur.left : cur.right; + if (!parent) { + if (cb) cb("done", child ? child.id : null, `Replaced root with ${child ? child.value : 'null'}`, [...path]); + return child; + } else { + if (parent.left === cur) parent.left = child; + else parent.right = child; + if (cb) cb("done", null, `Deleted ${cur.value}`, [...path]); + return root; + } + } + + // two children: successor + let succParent = cur; + let succ = cur.right; + const succPath = [...path, succ.id]; + while (succ.left) { + succParent = succ; + succ = succ.left; + succPath.push(succ.id); + } + if (cb) cb("compare", succ.id, `Successor: ${succ.value}`, succPath.slice()); + cur.value = succ.value; + if (succParent.left === succ) succParent.left = succ.right; + else succParent.right = succ.right; + if (cb) cb("done", null, `Replaced and removed successor ${succ.value}`, succPath.slice()); + return root; +} + +// Binary tree delete - replace with deepest node +function binaryTreeDelete(root, value, cb) { + if (!root) { + if (cb) cb("error", null, "Tree empty", []); + return null; + } + const q = [{ node: root, parent: null, dir: null, path: [root.id] }]; + let target = null, targetParent = null, targetDir = null, targetPath = []; + while (q.length) { + const { node, parent, dir, path } = q.shift(); + if (String(node.value) === String(value)) { + target = node; targetParent = parent; targetDir = dir; targetPath = [...path]; + if (cb) cb("delete", node.id, `Will delete ${node.value}`, [...path]); + break; + } + if (node.left) q.push({ node: node.left, parent: node, dir: "left", path: path.concat(node.left.id) }); + if (node.right) q.push({ node: node.right, parent: node, dir: "right", path: path.concat(node.right.id) }); + } + if (!target) { + if (cb) cb("error", null, `Value ${value} not found`, []); + return root; + } + + const queue = [{ node: root, parent: null, dir: null, path: [root.id] }]; + let last = null; + while (queue.length) { + last = queue.shift(); + if (last.node.left) queue.push({ node: last.node.left, parent: last.node, dir: "left", path: last.path.concat(last.node.left.id) }); + if (last.node.right) queue.push({ node: last.node.right, parent: last.node, dir: "right", path: last.path.concat(last.node.right.id) }); + } + const deepest = last.node; + const deepestParent = last.parent; + + if (cb) cb("compare", deepest.id, `Deepest node: ${deepest.value}`, [deepest.id]); + + if (target === deepest) { + if (!targetParent) { + if (cb) cb("done", null, `Removed root ${target.value}`, []); + return null; + } else { + if (targetParent.left === target) targetParent.left = null; + else targetParent.right = null; + if (cb) cb("done", null, `Deleted ${target.value}`, []); + return root; + } + } + + const replacedValue = deepest.value; + if (cb) cb("compare", deepest.id, `Replacing ${target.value} with ${deepest.value}`, [target.id, deepest.id]); + target.value = deepest.value; + if (deepestParent) { + if (deepestParent.left === deepest) deepestParent.left = null; + else deepestParent.right = null; + } + if (cb) cb("done", null, `Replaced ${value} with ${replacedValue} and removed deepest node`, []); + return root; +} + +// Traversals +function traverseInorder(node, cb, path = []) { + if (!node) return; + traverseInorder(node.left, cb, path); + path.push(node.id); + if (cb) cb("traverse", node.id, `Visit ${node.value}`, [...path]); + traverseInorder(node.right, cb, path); +} +function traversePreorder(node, cb, path = []) { + if (!node) return; + path.push(node.id); + if (cb) cb("traverse", node.id, `Visit ${node.value}`, [...path]); + traversePreorder(node.left, cb, path); + traversePreorder(node.right, cb, path); +} +function traversePostorder(node, cb, path = []) { + if (!node) return; + traversePostorder(node.left, cb, path); + traversePostorder(node.right, cb, path); + path.push(node.id); + if (cb) cb("traverse", node.id, `Visit ${node.value}`, [...path]); +} +function traverseLevelOrder(root, cb) { + if (!root) return; + const q = [root]; + const path = []; + while (q.length) { + const n = q.shift(); + path.push(n.id); + if (cb) cb("traverse", n.id, `Visit ${n.value}`, [...path]); + if (n.left) q.push(n.left); + if (n.right) q.push(n.right); + } +} + +// Generator export +export function* binaryTreeOp(startRoot = null, action = {}) { + // clone startRoot to work on + let root = startRoot ? cloneNode(startRoot) : null; + + const pushSnapshot = (type, id = null, message = null, path = null, done = false) => { + const snapshot = cloneNode(root); + const nodesById = treeToMap(snapshot); + const step = { tree: snapshot, nodesById, highlight: { type, id, message, path: path || (id ? [id] : []) }, message }; + if (done) step.done = true; + return step; + }; + + let pending = []; + const cb = (type, id, message, path) => { + pending.push(pushSnapshot(type, id, message, path, false)); + }; + function* flushPending() { + while (pending.length) yield pending.shift(); + } + + const type = action.type; + + switch (type) { + case "buildTree": { + root = null; + yield pushSnapshot("clear", null, "Clearing existing tree before build", null, false); + + const list = (action.list || []).map((v) => { + if (v === null) return null; + if (typeof v === "string") { + const s = v.trim(); + if (s === "-1" || s === "") return null; + return isNaN(s) ? s : Number(s); + } + return v; + }); + + for (let i = 0; i < list.length; i++) { + const partial = list.slice(0, i + 1); + root = buildTreeFromLevelOrderArr(partial); + const v = list[i]; + if (v === null) yield pushSnapshot("insert", null, `Skipped null at step ${i}`, null, false); + else { + const found = findNodeByValue(root, v); + yield pushSnapshot("insert", found.node?.id ?? null, `Inserted ${v} (build step ${i})`, found.path ?? null, false); + } + } + yield pushSnapshot("done", null, "Build complete", null, true); + break; + } + + case "insert": { + const value = action.value; + const treeType = action.treeType || "binary"; + if (value === undefined || value === null || String(value).trim() === "") { + yield pushSnapshot("error", null, "No value provided for insert", null, true); + return; + } + pending = []; + if (treeType === "bst") { + const hadRoot = !!root; + root = bstInsert(root, value, cb); + // If tree was empty before insert, bstInsert returned the new node as root + if (!hadRoot && root) { + // push a proper snapshot showing the new root inserted + pending.push(pushSnapshot("insert", root.id, `Inserted ${value} as root`, [root.id], false)); + } + for (const s of yield* flushPending()) yield s; + yield pushSnapshot("done", null, `Inserted ${value} into BST`, null, true); + } else { + // Binary tree insert level-order + if (!root) { + root = { id: nextId(), value, left: null, right: null }; + yield pushSnapshot("insert", root.id, `Inserted ${value} as root`, [root.id], false); + yield pushSnapshot("done", root.id, `Inserted ${value}`, null, true); + } else { + // BFS to find vacancy and yield compare steps along the way + const q = [root]; + let inserted = false; + while (q.length) { + const node = q.shift(); + // yield compare for node + pending.push(pushSnapshot("compare", node.id, `Checking ${node.value} for vacancy`, [node.id])); + if (!node.left) { + const newNode = { id: nextId(), value, left: null, right: null }; + node.left = newNode; + pending.push(pushSnapshot("insert", newNode.id, `Inserted ${value} as left child of ${node.value}`, [node.id, newNode.id])); + inserted = true; + break; + } else q.push(node.left); + + if (!node.right) { + const newNode = { id: nextId(), value, left: null, right: null }; + node.right = newNode; + pending.push(pushSnapshot("insert", newNode.id, `Inserted ${value} as right child of ${node.value}`, [node.id, newNode.id])); + inserted = true; + break; + } else q.push(node.right); + } + if (!inserted) { + // fallback (rare) + root = { id: nextId(), value, left: root, right: null }; + pending.push(pushSnapshot("insert", root.id, `Inserted ${value} as fallback root`, [root.id])); + } + for (const s of yield* flushPending()) yield s; + yield pushSnapshot("done", null, `Inserted ${value}`, null, true); + } + } + break; + } + + case "search": { + const value = action.value; + const treeType = action.treeType || "binary"; + if (value === undefined || value === null || String(value).trim() === "") { yield pushSnapshot("error", null, "No value provided for search", null, true); return; } + if (!root) { yield pushSnapshot("error", null, "Tree empty", null, true); return; } + + pending = []; + if (treeType === "bst") { + const res = bstSearch(root, value, cb); // returns { node, path } + for (const s of yield* flushPending()) yield s; + + // Compose traversal values from path + const visitedValues = res.path.map((id) => findValueById(root, id)).filter((v) => v !== undefined && v !== null); + if (res.node) { + // found + yield pushSnapshot("found", res.node.id, `Found ${value}. Visited: ${visitedValues.join(", ")}`, res.path, true); + } else { + yield pushSnapshot("notfound", null, `Not found ${value}. Visited: ${visitedValues.join(", ")}`, res.path, true); + } + } else { + const q = [{ node: root, path: [root.id] }]; + const visitedIds = []; + let found = null; + while (q.length) { + const { node, path } = q.shift(); + visitedIds.push(node.id); + yield pushSnapshot("compare", node.id, `Comparing ${value} with ${node.value}`, [...path]); + if (String(node.value) === String(value)) { + found = { node, path }; + yield pushSnapshot("found", node.id, `Found ${value}`, [...path]); + break; + } + if (node.left) q.push({ node: node.left, path: path.concat(node.left.id) }); + if (node.right) q.push({ node: node.right, path: path.concat(node.right.id) }); + } + const visitedValues = visitedIds.map((id) => findValueById(root, id)); + if (found) { + yield pushSnapshot("done", found.node.id, `Found ${value}. Visited: ${visitedValues.join(", ")}`, found.path, true); + } else { + yield pushSnapshot("notfound", null, `Not found ${value}. Visited: ${visitedValues.join(", ")}`, visitedIds, true); + } + } + break; + } + + case "delete": { + const value = action.value; + const treeType = action.treeType || "binary"; + if (value === undefined || value === null || String(value).trim() === "") { yield pushSnapshot("error", null, "No value provided for delete", null, true); return; } + if (!root) { yield pushSnapshot("error", null, "Tree empty", null, true); return; } + pending = []; + if (treeType === "bst") { + root = bstDelete(root, value, cb); + for (const s of yield* flushPending()) yield s; + yield pushSnapshot("done", null, `Delete complete`, null, true); + } else { + root = binaryTreeDelete(root, value, cb); + for (const s of yield* flushPending()) yield s; + yield pushSnapshot("done", null, `Delete complete`, null, true); + } + break; + } + + case "traverse": { + const order = action.order || "inorder"; + if (!root) { yield pushSnapshot("error", null, "Tree empty", null, true); return; } + yield pushSnapshot("message", null, `Starting ${order} traversal`, null, false); + pending = []; + if (order === "inorder") traverseInorder(root, cb, []); + else if (order === "preorder") traversePreorder(root, cb, []); + else if (order === "postorder") traversePostorder(root, cb, []); + else traverseLevelOrder(root, cb); + for (const s of yield* flushPending()) yield s; + + // produce final traversal values to show as message + let finalVisited = []; + if (order === "inorder") { + const arr = []; function collectIn(n){ if(!n) return; collectIn(n.left); arr.push(n.value); collectIn(n.right); } collectIn(root); finalVisited = arr; + } else if (order === "preorder") { + const arr = []; function collectPre(n){ if(!n) return; arr.push(n.value); collectPre(n.left); collectPre(n.right);} collectPre(root); finalVisited = arr; + } else if (order === "postorder") { + const arr = []; function collectPost(n){ if(!n) return; collectPost(n.left); collectPost(n.right); arr.push(n.value);} collectPost(root); finalVisited = arr; + } else { + const arr = []; const q = [root]; while(q.length){ const n=q.shift(); arr.push(n.value); if(n.left) q.push(n.left); if(n.right) q.push(n.right);} finalVisited = arr; + } + + yield pushSnapshot("done", null, `${order} traversal complete: ${finalVisited.join(", ")}`, null, true); + break; + } + + case "clear": { + root = null; + yield pushSnapshot("clear", null, "Cleared tree", null, true); + break; + } + + case "loadDemo": { + root = null; + yield pushSnapshot("clear", null, "Clearing existing tree before loading demo", null, false); + const demo = action.demo || ["10", "5", "15", "3", "7", "-1", "20"]; + const normalized = demo.map((v) => (String(v).trim() === "-1" ? null : (isNaN(v) ? v : Number(v)))); + for (let i = 0; i < normalized.length; i++) { + const arrPartial = normalized.slice(0, i + 1); + root = buildTreeFromLevelOrderArr(arrPartial); + const v = normalized[i]; + if (v !== null) { + const found = findNodeByValue(root, v); + yield pushSnapshot("insert", found.node?.id ?? null, `Loaded ${v} (demo step ${i})`, found.path ?? null, false); + } else yield pushSnapshot("insert", null, `Skipped null at step ${i}`, null, false); + } + yield pushSnapshot("done", null, "Demo load complete", null, true); + break; + } + + default: + yield pushSnapshot("done", null, "No-op", null, true); + break; + } +} diff --git a/src/components/dataStructure/BinaryTreeVisualizer.jsx b/src/components/dataStructure/BinaryTreeVisualizer.jsx new file mode 100644 index 0000000..a11aa5f --- /dev/null +++ b/src/components/dataStructure/BinaryTreeVisualizer.jsx @@ -0,0 +1,229 @@ +// src/components/dataStructure/BinaryTreeVisualizer.jsx +import React, { useEffect, useRef, useState } from "react"; + +/* +Props: + - tree: root node (object with { value, left, right } or with ids) + - nodesById: (optional) map id => { id, value, leftId, rightId } (not required) + - highlight: { type, id, message, path } (ids may be generated by this visualizer) + - nodeSize, gapY optional +*/ + +export default function BinaryTreeVisualizer({ + tree = null, + nodesById = {}, + highlight = null, + nodeSize = 64, + gapY = 120, +}) { + const containerRef = useRef(null); + const [layoutNodes, setLayoutNodes] = useState([]); // { id, value, depth, x, y, rawNode } + const [containerWidth, setContainerWidth] = useState(1000); + + // WeakMap to assign stable ids to nodes (won't prevent GC) + const nodeIdMapRef = useRef(new WeakMap()); + const nextIdRef = useRef(1); + + const getNodeVizId = (node) => { + if (!node) return null; + // Prefer existing id if present and unique-ish + 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; + }; + + // Build inorder layout positions and depths; uses tree object structure + useEffect(() => { + const rect = containerRef.current?.getBoundingClientRect(); + if (rect) setContainerWidth(rect.width || 1000); + + // Reset id generator only when a completely different tree object reference is provided + // (we keep WeakMap so stable ids persist across updates for same node objects) + // If your tree implementation replaces nodes with brand new objects often, this still works. + + const nodesInOrder = []; + // Track depth for each node object (we'll push { node, depth } in inorder) + function inorder(node, depth = 0) { + if (!node) return; + inorder(node.left, depth + 1); + nodesInOrder.push({ node, depth }); + inorder(node.right, depth + 1); + } + + inorder(tree, 0); + + if (!nodesInOrder.length) { + setLayoutNodes([]); + return; + } + + const total = nodesInOrder.length; + const usableWidth = Math.max(containerWidth - 120, 300); + + const computed = nodesInOrder.map((entry, idx) => { + const x = (idx / Math.max(total - 1, 1)) * usableWidth + 60; + const y = entry.depth * gapY + 40; + const id = getNodeVizId(entry.node); + return { id, value: entry.node.value, depth: entry.depth, x, y, rawNode: entry.node }; + }); + + setLayoutNodes(computed); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tree, containerRef.current, containerWidth, nodeSize, gapY, highlight]); + + // Update container width on resize + useEffect(() => { + const onResize = () => { + const rect = containerRef.current?.getBoundingClientRect(); + if (rect) setContainerWidth(rect.width || 1000); + setLayoutNodes((s) => [...s]); + }; + window.addEventListener("resize", onResize); + onResize(); + return () => window.removeEventListener("resize", onResize); + }, []); + + // helper + const nodeById = (id) => layoutNodes.find((n) => n.id === id); + + // Build edges by traversing the original tree object, but mapping to viz ids/positions + const edges = []; + (function buildEdges(node) { + if (!node) return; + const fromId = getNodeVizId(node); + const left = node.left; + const right = node.right; + if (left) { + const toId = getNodeVizId(left); + const p = nodeById(fromId); + const c = nodeById(toId); + if (p && c) edges.push({ from: p, to: c }); + buildEdges(left); + } + if (right) { + const toId = getNodeVizId(right); + const p = nodeById(fromId); + const c = nodeById(toId); + if (p && c) edges.push({ from: p, to: c }); + buildEdges(right); + } + })(tree); + + // Highlight/class logic — allow highlight IDs to be either original ids or generated viz ids. + const getNodeClass = (nodeLayout) => { + const t = highlight?.type; + const hid = highlight?.id !== undefined && highlight?.id !== null ? String(highlight.id) : null; + const path = (highlight?.path || []).map(String); + const isActive = hid && (hid === nodeLayout.id || hid === String(nodeLayout.rawNode?.id) || hid === String(nodeLayout.value)); + const isInPath = path.includes(nodeLayout.id) || path.includes(String(nodeLayout.rawNode?.id)) || path.includes(String(nodeLayout.value)); + + if (t === "insert" && isActive) return "bg-emerald-600 text-white ring-4 ring-emerald-400"; + if (t === "delete" && isActive) return "bg-rose-600 text-white ring-4 ring-rose-400"; + if ((t === "found" || t === "notfound") && isActive) return "bg-yellow-300 text-black ring-4 ring-yellow-400"; + if (t === "traverse" && isInPath) return "bg-violet-700 text-white ring-4 ring-violet-400"; + if (t === "compare" && isInPath) return "bg-sky-500 text-white ring-2 ring-sky-300"; + if (isInPath) return "bg-slate-700 text-white/90"; + return "bg-slate-800 text-white"; + }; + + const Node = ({ n }) => { + const cls = getNodeClass(n); + const radius = nodeSize / 2; + return ( +
+
{String(n.value)}
+
+ ); + }; + + // Render edges as smooth paths + const SvgEdges = () => { + if (!layoutNodes.length) return null; + const rect = containerRef.current?.getBoundingClientRect() || { width: containerWidth, height: Math.max(...layoutNodes.map((n) => n.y)) + nodeSize + 60 }; + const width = rect.width; + const height = rect.height + 40; + + 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) - 20; + const d = `M ${x1} ${y1} Q ${midX} ${controlY} ${x2} ${y2}`; + const pathVals = (highlight?.path || []).map(String); + const fromIn = pathVals.includes(String(e.from.id)) || pathVals.includes(String(e.from.rawNode?.id)) || pathVals.includes(String(e.from.value)); + const toIn = pathVals.includes(String(e.to.id)) || pathVals.includes(String(e.to.rawNode?.id)) || pathVals.includes(String(e.to.value)); + return ; + })} + + ); + }; + + const computeStats = () => { + if (!tree) return { nodes: 0, height: 0, root: null }; + const q = [tree]; + let nodes = 0; + while (q.length) { + const n = q.shift(); + nodes++; + if (n.left) q.push(n.left); + if (n.right) q.push(n.right); + } + function height(n) { + if (!n) return 0; + return 1 + Math.max(height(n.left), height(n.right)); + } + const rootId = tree ? (getNodeVizId(tree) || (tree.id ?? String(tree.value))) : null; + return { nodes, height: height(tree), root: rootId }; + }; + + const stats = computeStats(); + + return ( +
+
+
Insert
+
Delete
+
Found
+
Traverse
+
Compare / Path
+
+ + + + {layoutNodes.map((n) => ( + + ))} + +
+ {highlight?.message ?? ""} +
Nodes: {stats.nodes} • Height: {stats.height} • Root: {stats.root ?? "—"}
+
+
+ ); +} diff --git a/src/pages/dataStructure/BinaryTreePage.jsx b/src/pages/dataStructure/BinaryTreePage.jsx new file mode 100644 index 0000000..8a69b9a --- /dev/null +++ b/src/pages/dataStructure/BinaryTreePage.jsx @@ -0,0 +1,319 @@ +// src/pages/dataStructure/BinaryTreePage.jsx +import React, { useEffect, useRef, useState } from "react"; +import { Toaster, toast } from "react-hot-toast"; +import BinaryTreeVisualizer from "../../components/dataStructure/BinaryTreeVisualizer.jsx"; +import { binaryTreeOp, resetIdCounter } from "../../algorithms/dataStructure/binarytree.js"; + +export default function BinaryTreePage() { + const [treeSnapshot, setTreeSnapshot] = useState(null); + const [nodesById, setNodesById] = useState({}); + const [value, setValue] = useState(""); + const [listInput, setListInput] = useState(""); + const [treeType, setTreeType] = useState("binary"); + const [traverseOrder, setTraverseOrder] = useState("inorder"); + const [highlight, setHighlight] = useState(null); + const [isPlaying, setIsPlaying] = useState(false); + const [isRunning, setIsRunning] = useState(false); + const [stepCountLabel, setStepCountLabel] = useState("No actions yet"); + + const genRef = useRef(null); + const historyRef = useRef([]); + const stepIndexRef = useRef(-1); + const playDelay = useRef(550); + const playLoop = useRef(false); + + const delay = (ms) => new Promise((res) => setTimeout(res, ms)); + + // Apply visualization step + const applyStep = (step) => { + if (!step) return; + setTreeSnapshot(step.tree ?? null); + setNodesById(step.nodesById ?? {}); + setHighlight(step.highlight ?? null); + const idx = stepIndexRef.current; + const len = historyRef.current.length; + setStepCountLabel(idx >= 0 ? `Step ${idx + 1} / ${len}` : `Step 0 / ${len}`); + }; + + const moveToIndex = (idx) => { + const h = historyRef.current; + if (idx < 0) { + stepIndexRef.current = -1; + setTreeSnapshot(null); + setNodesById({}); + setHighlight(null); + setStepCountLabel("No actions yet"); + return; + } + if (idx < h.length) { + stepIndexRef.current = idx; + applyStep(h[idx]); + } + }; + + const stepForward = async () => { + if (isRunning) return; + setIsRunning(true); + try { + const h = historyRef.current; + if (stepIndexRef.current < h.length - 1) { + stepIndexRef.current++; + applyStep(h[stepIndexRef.current]); + } else if (genRef.current) { + const { value, done } = genRef.current.next(); + if (!done && value) { + h.push(value); + stepIndexRef.current = h.length - 1; + applyStep(value); + } + if (done) genRef.current = null; + } + } finally { + setIsRunning(false); + } + }; + + const stepBack = () => { + if (isRunning) return; + if (stepIndexRef.current > 0) { + stepIndexRef.current--; + applyStep(historyRef.current[stepIndexRef.current]); + } + }; + + const startPlay = async () => { + if (isPlaying) return; + setIsPlaying(true); + playLoop.current = true; + + while (playLoop.current) { + await stepForward(); + await delay(playDelay.current); + if (!genRef.current && stepIndexRef.current >= historyRef.current.length - 1) { + playLoop.current = false; + } + } + setIsPlaying(false); + }; + + const pausePlay = () => { + playLoop.current = false; + setIsPlaying(false); + }; + + const resetAll = () => { + pausePlay(); + genRef.current = null; + historyRef.current = []; + stepIndexRef.current = -1; + setTreeSnapshot(null); + setNodesById({}); + setHighlight(null); + setStepCountLabel("No actions yet"); + }; + + // 🔧 Main operation runner (fixed for immediate execution) + const runAction = async (action) => { + if (isRunning) return; + setIsRunning(true); + pausePlay(); + + try { + const startRoot = treeSnapshot || null; + genRef.current = binaryTreeOp(startRoot, action); + + // Consume first step immediately + const first = genRef.current.next(); + if (!first.done && first.value) { + historyRef.current.push(first.value); + stepIndexRef.current = historyRef.current.length - 1; + applyStep(first.value); + } + + // Consume rest automatically (no double-click) + while (true) { + const { value, done } = genRef.current.next(); + if (!done && value) { + historyRef.current.push(value); + stepIndexRef.current = historyRef.current.length - 1; + applyStep(value); + await delay(20); + } else break; + } + + } catch (err) { + console.error("runAction error:", err); + // Removed toast.error("Operation failed"); + } finally { + setIsRunning(false); + } + }; + + // --- Button Handlers --- + const handleInsert = async () => { + if (!value.trim()) return toast.error("Enter a value to insert"); + const v = isNaN(value) ? value : Number(value); + await runAction({ type: "insert", value: v, treeType }); + setValue(""); + }; + + const handleDelete = async () => { + if (!value.trim()) return toast.error("Enter value to delete"); + const v = isNaN(value) ? value : Number(value); + await runAction({ type: "delete", value: v, treeType }); + setValue(""); + }; + + const handleSearch = async () => { + if (!value.trim()) return toast.error("Enter value to search"); + const v = isNaN(value) ? value : Number(value); + await runAction({ type: "search", value: v, treeType }); + setValue(""); + }; + + const handleBuildTree = async () => { + if (!listInput.trim()) return toast.error("Enter level-order list (e.g. 1,2,3,-1,4)"); + const parts = listInput.split(",").map((s) => s.trim()); + const normalized = parts.map((p) => + p === "" || p === "-1" ? null : isNaN(p) ? p : Number(p) + ); + await runAction({ type: "buildTree", list: normalized, treeType }); + }; + + const handleTraverse = async () => { + await runAction({ type: "traverse", order: traverseOrder }); + }; + + const handleLoadDemo = async () => { + const demo = ["1","2","3","4","5","-1","6","7","-1","-1","-1"]; + const normalized = demo.map((v) => (v === "-1" ? null : (isNaN(v) ? v : Number(v)))); + await runAction({ type: "loadDemo", demo: normalized, treeType }); + }; + + useEffect(() => () => (playLoop.current = false), []); + + return ( +
+ +

+ Binary Tree / BST Visualizer +

+ +
+ {/* Left Panel */} +
+
+ + + + + setValue(e.target.value)} + placeholder="Value (text or number)" + className="p-3 rounded bg-[#0b1220] border border-indigo-600 text-indigo-200" + /> + +
+ + +
+ + + +
+ + setListInput(e.target.value)} + placeholder="1,2,3,4,-1,-1,5" + className="p-3 rounded bg-[#0b1220] border border-indigo-600 text-indigo-200" + /> +
+ + +
+
+ +
+ + + +
+ +
+ +
+ + + + + +
+
+ +
+ Note: Binary Tree builds / manual Add use level-order insertion. Use -1 for null nodes. +
+
+
+ + {/* Right visualizer */} +
+ +
{highlight?.message ?? stepCountLabel}
+
+
+
+ ); +} diff --git a/src/pages/dataStructure/datastructurePage.jsx b/src/pages/dataStructure/datastructurePage.jsx index f4a1462..e799083 100644 --- a/src/pages/dataStructure/datastructurePage.jsx +++ b/src/pages/dataStructure/datastructurePage.jsx @@ -3,6 +3,8 @@ import { Network, Compass, Rocket } from "lucide-react"; import StackPage from "./stack.jsx"; import LinkedListPage from "./linkedlist.jsx"; // ✅ Linked List page import import QueuePage from "./queue.jsx"; +import BinaryTreePage from "./BinaryTreePage.jsx"; + export default function DSPage() { const [selectedDS, setSelectedDS] = useState(""); @@ -29,6 +31,13 @@ export default function DSPage() { ); + case "binarytree": + return ( +
+ +
+ ); + default: return ( @@ -74,6 +83,8 @@ export default function DSPage() { + +