diff --git a/app/visualizer/page.jsx b/app/visualizer/page.jsx index 0dcace7..ef20c25 100755 --- a/app/visualizer/page.jsx +++ b/app/visualizer/page.jsx @@ -403,11 +403,11 @@ const sections = [ }, { name: "Tree Diameter", - path: "/visualizer/trees/algorithms/diameter", + path: "/visualizer/trees/algorithms/tree-diameter", }, { name: "Tree Isomorphism", - path: "/visualizer/trees/algorithms/isomorphism", + path: "/visualizer/trees/algorithms/tree-isomorphism", }, { name: "Serialize/Deserialize", diff --git a/app/visualizer/trees/algorithms/lowest-common-ancestor/page.jsx b/app/visualizer/trees/algorithms/lowest-common-ancestor/page.jsx index 44fa1c6..48a10f2 100755 --- a/app/visualizer/trees/algorithms/lowest-common-ancestor/page.jsx +++ b/app/visualizer/trees/algorithms/lowest-common-ancestor/page.jsx @@ -100,6 +100,8 @@ export default function Page() { { + if (!node) return new TreeNode(value); + if (value < node.value) node.left = insertNode(node.left, value); + else if (value > node.value) node.right = insertNode(node.right, value); + return node; +}; + +// Edge-height: number of nodes on the longest downward path from this node (0 for null). +const height = (node) => (node ? 1 + Math.max(height(node.left), height(node.right)) : 0); + +// Post-order pass: at every node, the longest path *through* it is +// leftHeight + rightHeight edges. The overall diameter is the max of that +// value across every node — not necessarily the one at the root. +const computeDiameter = (root) => { + const steps = []; + let best = { diameter: -1, node: null }; + + const visit = (node) => { + if (!node) return 0; + const leftHeight = visit(node.left); + const rightHeight = visit(node.right); + const h = 1 + Math.max(leftHeight, rightHeight); + const diameter = leftHeight + rightHeight; + steps.push({ value: node.value, leftHeight, rightHeight, h, diameter }); + if (diameter > best.diameter) best = { diameter, node }; + return h; + }; + + visit(root); + return { steps, best }; +}; + +// Walks from a node down to the deepest leaf, always following the taller side. +const deepPath = (node) => { + if (!node) return []; + const leftHeight = height(node.left); + const rightHeight = height(node.right); + if (leftHeight >= rightHeight) return [node.value, ...deepPath(node.left)]; + return [node.value, ...deepPath(node.right)]; +}; + +const buildDiameterPath = (pivot) => { + if (!pivot) return []; + return [...deepPath(pivot.left)].reverse().concat([pivot.value]).concat(deepPath(pivot.right)); +}; + +const NODE_RADIUS = 22; +const LEVEL_HEIGHT = 78; + +const layoutTree = (node, depth = 0, x = 320, y = 40, nodes = [], edges = []) => { + if (!node) return { nodes, edges }; + const isLeaf = !node.left && !node.right; + const xOffset = Math.max(24, 110 / (depth + 1)); + + nodes.push({ value: node.value, x, y, depth, isLeaf, isRoot: depth === 0 }); + + if (node.left) { + const leftX = x - xOffset; + const leftY = y + LEVEL_HEIGHT; + edges.push({ x1: x, y1: y + NODE_RADIUS - 2, x2: leftX, y2: leftY - NODE_RADIUS + 2, parentValue: node.value, childValue: node.left.value }); + layoutTree(node.left, depth + 1, leftX, leftY, nodes, edges); + } + if (node.right) { + const rightX = x + xOffset; + const rightY = y + LEVEL_HEIGHT; + edges.push({ x1: x, y1: y + NODE_RADIUS - 2, x2: rightX, y2: rightY - NODE_RADIUS + 2, parentValue: node.value, childValue: node.right.value }); + layoutTree(node.right, depth + 1, rightX, rightY, nodes, edges); + } + + return { nodes, edges }; +}; + +const STEP_DELAY = 450; + +const DiameterVisualizer = () => { + const [root, setRoot] = useState(null); + const [inputValue, setInputValue] = useState(""); + const [message, setMessage] = useState("Tree is empty"); + const [computedHeights, setComputedHeights] = useState({}); + const [diameterPath, setDiameterPath] = useState([]); + const [pivotValue, setPivotValue] = useState(null); + const [diameterEdges, setDiameterEdges] = useState(null); + const [busy, setBusy] = useState(false); + + const animateDropIn = (el) => { + if (!el || el.dataset.animated) return; + el.dataset.animated = "true"; + gsap.fromTo(el, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.45, ease: "back.out(1.7)" }); + }; + + const clearResult = () => { + setComputedHeights({}); + setDiameterPath([]); + setPivotValue(null); + setDiameterEdges(null); + }; + + const handleInsert = () => { + const value = parseInt(inputValue, 10); + if (Number.isNaN(value)) { + setMessage("Please enter a valid number"); + return; + } + if (busy) return; + setRoot((prev) => insertNode(prev ? structuredClone(prev) : null, value)); + setMessage(`Inserted ${value}`); + setInputValue(""); + clearResult(); + }; + + const handleFindDiameter = () => { + if (busy || !root) return; + + const { steps, best } = computeDiameter(root); + const path = buildDiameterPath(best.node); + + setBusy(true); + clearResult(); + + let step = 0; + const revealStep = () => { + const s = steps[step]; + setComputedHeights((prev) => ({ ...prev, [s.value]: s.h })); + setMessage(`height(${s.value}) = ${s.h} — longest path through ${s.value} spans ${s.diameter} edge${s.diameter === 1 ? "" : "s"}`); + step++; + + if (step < steps.length) { + setTimeout(revealStep, STEP_DELAY); + } else { + setTimeout(() => { + setDiameterPath(path); + setPivotValue(best.node.value); + setDiameterEdges(best.diameter); + setMessage(`Diameter is ${best.diameter} edge${best.diameter === 1 ? "" : "s"} (${path.length} nodes), turning at ${best.node.value}`); + setBusy(false); + }, STEP_DELAY); + } + }; + revealStep(); + }; + + const generateRandomTree = () => { + if (busy) return; + const size = Math.floor(Math.random() * 5) + 7; + const values = Array.from({ length: size }, () => Math.floor(Math.random() * 100) + 1); + let newRoot = null; + values.forEach((v) => { + newRoot = insertNode(newRoot, v); + }); + setRoot(newRoot); + setMessage(`Generated a tree with ${size} random inserts`); + clearResult(); + }; + + const reset = () => { + if (busy) return; + setRoot(null); + setInputValue(""); + setMessage("Tree is empty"); + clearResult(); + }; + + const { nodes, edges } = root ? layoutTree(root) : { nodes: [], edges: [] }; + + const getSvgDimensions = () => { + if (nodes.length === 0) return { width: 600, height: 220 }; + const xValues = nodes.map((n) => n.x); + const yValues = nodes.map((n) => n.y); + const padding = 40; + return { + width: Math.max(600, Math.max(...xValues) - Math.min(...xValues) + padding * 2), + height: Math.max(220, Math.max(...yValues) + padding * 2), + }; + }; + const dims = getSvgDimensions(); + + const isDiameterEdge = (edge) => { + if (diameterPath.length < 2) return false; + for (let i = 0; i < diameterPath.length - 1; i++) { + const a = diameterPath[i]; + const b = diameterPath[i + 1]; + if ((edge.parentValue === a && edge.childValue === b) || (edge.parentValue === b && edge.childValue === a)) { + return true; + } + } + return false; + }; + + return ( +
+

+ Find the longest path between any two nodes in the tree +

+ +
+ {/* Controls */} +
+
+ setInputValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleInsert()} + placeholder="Enter a number" + disabled={busy} + className="flex-1 min-w-0 px-3 py-2 text-sm rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-neutral-900 text-gray-800 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 transition disabled:opacity-50" + /> + +
+ +
+ +
+ +
+ + +
+
+ + {/* Visualization */} +
+
+ {message} +
+ +
+ {nodes.length > 0 ? ( + + + + + + + + + + + + + + + + {edges.map((edge, i) => { + const midY = (edge.y1 + edge.y2) / 2; + const onDiameter = isDiameterEdge(edge); + return ( + + ); + })} + + {nodes.map((node, i) => { + const heightLabel = computedHeights[node.value]; + const isOnDiameter = diameterPath.includes(node.value); + const isPivot = node.value === pivotValue; + const isEndpoint = isOnDiameter && !isPivot && (node.value === diameterPath[0] || node.value === diameterPath[diameterPath.length - 1]); + return ( + + {node.isRoot && ( + + )} + {isOnDiameter && !isPivot && !isEndpoint && ( + + )} + {isEndpoint && ( + + )} + {isPivot && ( + + )} + + + {node.value} + + + {heightLabel !== undefined && ( + <> + + + h:{heightLabel} + + + )} + + ); + })} + + ) : ( +
+ No tree yet — insert a value or generate a random tree +
+ )} +
+ + {diameterEdges !== null && ( +
+ Diameter: {diameterEdges} edge{diameterEdges === 1 ? "" : "s"} ({diameterPath.join(" → ")}) +
+ )} + +
+ + + Internal node + + + + Leaf node + + + + Root + + + + Diameter path + + + + Path endpoints + + + + Turning point + +
+
+
+
+ ); +}; + +export default DiameterVisualizer; diff --git a/app/visualizer/trees/algorithms/tree-diameter/code.js b/app/visualizer/trees/algorithms/tree-diameter/code.js new file mode 100755 index 0000000..5f0d075 --- /dev/null +++ b/app/visualizer/trees/algorithms/tree-diameter/code.js @@ -0,0 +1,159 @@ +const codeExamples = { + javascript: `// Binary tree node +class TreeNode { + constructor(value) { + this.value = value; + this.left = null; + this.right = null; + } +} + +// Single post-order pass: each node's height is computed once and reused +// by its parent, while a running maximum tracks the widest "path-through" value. +function diameterOfTree(root) { + let diameter = 0; + + function height(node) { + if (node === null) return 0; + + const leftHeight = height(node.left); + const rightHeight = height(node.right); + + // Longest path that turns at this node + diameter = Math.max(diameter, leftHeight + rightHeight); + + // Height contributed upward to this node's parent + return 1 + Math.max(leftHeight, rightHeight); + } + + height(root); + return diameter; // number of edges +} + +// Usage example +let root = null; +[8, 3, 10, 1, 6, 14].forEach((v) => { + root = insert(root, v); // see BST Insertion for insert +}); + +diameterOfTree(root); // 4`, + + python: `# Binary tree node +class TreeNode: + def __init__(self, value): + self.value = value + self.left = None + self.right = None + +# Single post-order pass: each node's height is computed once and reused +# by its parent, while a running maximum tracks the widest "path-through" value. +def diameter_of_tree(root): + diameter = 0 + + def height(node): + nonlocal diameter + if node is None: + return 0 + + left_height = height(node.left) + right_height = height(node.right) + + # Longest path that turns at this node + diameter = max(diameter, left_height + right_height) + + # Height contributed upward to this node's parent + return 1 + max(left_height, right_height) + + height(root) + return diameter # number of edges + +# Usage example +root = None +for v in [8, 3, 10, 1, 6, 14]: + root = insert(root, v) # see BST Insertion for insert + +diameter_of_tree(root) # 4`, + + c: `#include + +typedef struct TreeNode { + int value; + struct TreeNode *left, *right; +} TreeNode; + +int diameter = 0; + +int max(int a, int b) { + return a > b ? a : b; +} + +// Single post-order pass: each node's height is computed once and reused +// by its parent, while diameter tracks the widest "path-through" value. +int height(TreeNode* node) { + if (node == NULL) return 0; + + int leftHeight = height(node->left); + int rightHeight = height(node->right); + + // Longest path that turns at this node + if (leftHeight + rightHeight > diameter) { + diameter = leftHeight + rightHeight; + } + + // Height contributed upward to this node's parent + return 1 + max(leftHeight, rightHeight); +} + +int diameterOfTree(TreeNode* root) { + diameter = 0; + height(root); + return diameter; // number of edges +} + +int main() { + TreeNode* root = NULL; // build with insert() from BST Insertion + printf("Diameter: %d\\n", diameterOfTree(root)); + return 0; +}`, + + java: `class TreeNode { + int value; + TreeNode left, right; + + TreeNode(int value) { + this.value = value; + } +} + +public class TreeDiameter { + private static int diameter; + + // Single post-order pass: each node's height is computed once and reused + // by its parent, while diameter tracks the widest "path-through" value. + private static int height(TreeNode node) { + if (node == null) return 0; + + int leftHeight = height(node.left); + int rightHeight = height(node.right); + + // Longest path that turns at this node + diameter = Math.max(diameter, leftHeight + rightHeight); + + // Height contributed upward to this node's parent + return 1 + Math.max(leftHeight, rightHeight); + } + + static int diameterOfTree(TreeNode root) { + diameter = 0; + height(root); + return diameter; // number of edges + } + + public static void main(String[] args) { + TreeNode root = null; // build with insert() from BST Insertion + System.out.println("Diameter: " + diameterOfTree(root)); + } +}`, +}; + +export default codeExamples; diff --git a/app/visualizer/trees/algorithms/tree-diameter/content.jsx b/app/visualizer/trees/algorithms/tree-diameter/content.jsx new file mode 100755 index 0000000..4af861d --- /dev/null +++ b/app/visualizer/trees/algorithms/tree-diameter/content.jsx @@ -0,0 +1,232 @@ +"use client"; +import ComplexityGraph from "@/app/components/ui/graph"; +import { useTheme } from "@/app/contexts/ThemeContext"; +import DailyDSAEmbed from "@/app/components/ui/DailyDSAEmbed"; +import NewsletterEmbed from "@/app/components/ui/NewsletterEmbed"; +import InContentAd from "@/app/components/ads/InContentAd"; +import { motion } from "framer-motion"; + +const WalkthroughDiagram = () => { + const nodes = [ + { id: "8", x: 110, y: 30 }, + { id: "3", x: 70, y: 80, isPivot: true }, + { id: "10", x: 150, y: 80 }, + { id: "1", x: 50, y: 130, isEndpoint: true }, + { id: "6", x: 90, y: 130 }, + { id: "14", x: 150, y: 130, isEndpoint: true }, + ]; + const edges = [ + ["8", "3"], + ["8", "10"], + ["3", "1"], + ["3", "6"], + ["10", "14"], + ]; + const diameterEdges = new Set(["3-1", "3-8", "8-10", "10-14"]); + const byId = Object.fromEntries(nodes.map((n) => [n.id, n])); + + return ( + + {edges.map(([from, to], i) => { + const onDiameter = diameterEdges.has(`${from}-${to}`) || diameterEdges.has(`${to}-${from}`); + return ( + + ); + })} + + {nodes.map((n, i) => ( + + {n.isPivot && ( + + )} + {n.isEndpoint && ( + + )} + + + {n.id} + + + ))} + + ); +}; + +const Content = () => { + const { theme } = useTheme(); + + const paragraphs = [ + `The diameter of a tree is the number of edges on the longest path between any two nodes. That path doesn't have to pass through the root — it can start and end anywhere, and in most trees it actually cuts through some node in the middle where two deep subtrees meet.`, + `The key insight: for any single node, the longest path that passes *through* it is the height of its left subtree plus the height of its right subtree — one leg going down each side. Checking every node this way and keeping the largest total automatically finds the true diameter, because whichever node happens to be the meeting point of the two longest branches will produce the biggest sum.`, + `This means diameter can be computed in a single post-order traversal: recursively find the height of the left and right subtrees first, use them to compute this node's own height (1 + the taller side) and its "path-through" value (left height + right height), then update a running maximum. No repeated re-traversal is needed — every node's height is computed exactly once and reused by its parent.`, + `Diameter shows up whenever "the two most distant points in a hierarchy" matters: the worst-case latency between two nodes in a network topology tree, the longest chain of dependencies in a build graph, or simply describing how "spread out" or "stringy" versus "bushy" a tree's shape is.`, + ]; + + const algorithm = [ + { points: "Run a post-order traversal — process both children before the current node" }, + { + points: "At each node, using the already-computed heights of its children:", + subpoints: [ + "This node's height = 1 + max(left child height, right child height)", + "The longest path through this node = left child height + right child height", + ], + }, + { points: "Track the maximum path-through value seen across every node — that maximum is the diameter" }, + ]; + + const complexity = [ + { points: "Time Complexity: O(n) — every node's height is computed exactly once in a single traversal." }, + { points: "Space Complexity: O(h) — recursion stack depth equals the tree's height (O(log n) balanced, O(n) skewed)." }, + ]; + + return ( +
+
+ + +
+
+ {/* What is Diameter */} +
+

+ + What is the Diameter of a Tree? +

+
+

+ {paragraphs[0]} +

+
+
+ + {/* How it works */} +
+

+ + How Does It Work? +

+
+

+ {paragraphs[1]} +

+
+
+

+ {paragraphs[2]} +

+
+ +
+ Diameter = 4 edges — the path 1 → 3 → 8 → 10 → 14 turns at node 3 +
+ + +
+ + + Path endpoints + + + + Turning point + +
+
+ + {/* Algorithm Steps */} +
+

+ + Algorithm Steps +

+
+
    + {algorithm.map((item, index) => ( +
  1. + {item.points} + {item.subpoints && ( +
      + {item.subpoints.map((subitem, subindex) => ( +
    • + {subitem} +
    • + ))} +
    + )} +
  2. + ))} +
+
+
+ + {/* Time Complexity */} +
+

+ + Time Complexity +

+
+
    + {complexity.map((item, index) => ( +
  • + + {item.points.split(":")[0]}: + + {item.points.split(":")[1]} +
  • + ))} +
+
+ +
+ n} + averageCase={(n) => n} + worstCase={(n) => n} + maxN={25} + /> +
+ + +
+ + {/* Additional Info */} +
+
+
+

+ {paragraphs[3]} +

+
+
+
+
+ + +
+ ); +}; + +export default Content; diff --git a/app/visualizer/trees/algorithms/tree-diameter/page.jsx b/app/visualizer/trees/algorithms/tree-diameter/page.jsx new file mode 100755 index 0000000..1cc855d --- /dev/null +++ b/app/visualizer/trees/algorithms/tree-diameter/page.jsx @@ -0,0 +1,115 @@ +import Animation from "@/app/visualizer/trees/algorithms/tree-diameter/animation"; +import Navbar from "@/app/components/navbarinner"; +import ModuleHeader from "@/app/components/modules/Header"; +import Footer from "@/app/components/footer"; +import BackToTop from "@/app/components/ui/backtotop"; +import ExploreOther from "@/app/components/ui/exploreOther"; +import CodeBlock from "@/app/components/modules/CodeBlock"; +import codeExamples from "./code"; +import Quiz from "@/app/visualizer/trees/algorithms/tree-diameter/quiz"; +import Content from "@/app/visualizer/trees/algorithms/tree-diameter/content"; +import ModuleCard from "@/app/components/ui/ModuleCard"; +import { MODULE_MAPS } from "@/lib/modulesMap"; + +export const metadata = { + title: "Tree Diameter | Animation and Explanation", + description: + "Learn how to find the diameter of a binary tree — the longest path between any two nodes — using a single post-order traversal, with an interactive visualizer, code examples in JavaScript, C, Python, and Java, and a quiz.", + keywords: [ + "Tree Diameter", + "Diameter of a Binary Tree", + "Tree Diameter Algorithm", + "Diameter of a Binary Tree Algorithm", + "Tree Diameter Visualization", + "Diameter of a Binary Tree Visualization", + "Longest Path in a Tree", + "Binary Tree Height", + "Tree Diameter in JavaScript", + "Diameter of a Binary Tree in JavaScript", + "Tree Diameter in C", + "Diameter of a Binary Tree in C", + "Tree Diameter in Python", + "Diameter of a Binary Tree in Python", + "Tree Diameter in Java", + "Diameter of a Binary Tree in Java", + "Tree Algorithms", + "DSA Trees", + "Learn Trees", + "Tree Quiz", + ], + robots: "index, follow", + openGraph: { + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Tree Diameter Visualization", + }, + ], + }, +}; + +export default function Page() { + const paths = [ + { name: "Home", href: "/" }, + { name: "Visualizer", href: "/visualizer" }, + { name: "Trees : Tree Diameter", href: "" }, + ]; + + return ( + <> +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+

+ Test Your Knowledge before moving forward! +

+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + +