diff --git a/app/components/ui/PushPop.jsx b/app/components/ui/PushPop.jsx index 2ef0a98..d6105a2 100755 --- a/app/components/ui/PushPop.jsx +++ b/app/components/ui/PushPop.jsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { Plus, ArrowUpFromLine, Eye, RotateCcw } from 'lucide-react'; const PushPop = ({ stack, setStack, isAnimating, setIsAnimating, setMessage, setOperation }) => { const [inputValue, setInputValue] = useState(''); @@ -61,44 +62,49 @@ const PushPop = ({ stack, setStack, isAnimating, setIsAnimating, setMessage, set }; return ( -
-
+
+
setInputValue(e.target.value)} - placeholder="Enter value" - className="flex-1 p-2 border rounded dark:bg-neutral-900 focus:ring-2 focus:ring-blue-500" + onKeyDown={(e) => e.key === 'Enter' && push()} + placeholder="Enter a value" + 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" disabled={isAnimating} />
-
+
diff --git a/app/visualizer/sorting/insertionsort/content.jsx b/app/visualizer/sorting/insertionsort/content.jsx index d867dcf..45e27b1 100755 --- a/app/visualizer/sorting/insertionsort/content.jsx +++ b/app/visualizer/sorting/insertionsort/content.jsx @@ -4,6 +4,137 @@ 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"; + +const InsertionStepDiagram = ({ + values, + sortedFrom, + keyIndex, + targetIndex, + keyPrefix, +}) => { + const boxSize = 40; + const gap = 8; + const paddingX = 8; + const topPadding = 42; + const width = values.length * (boxSize + gap) - gap + paddingX * 2; + const height = boxSize + topPadding + 22; + + const boxX = (idx) => paddingX + idx * (boxSize + gap); + const boxY = topPadding; + const cx = (idx) => boxX(idx) + boxSize / 2; + + const shifts = (idx) => + targetIndex !== null && + keyIndex !== targetIndex && + idx >= targetIndex && + idx < keyIndex; + + const fillFor = (idx) => { + if (idx === keyIndex) return "#f59e0b"; + if (shifts(idx)) return "#94a3b8"; + if (idx < sortedFrom) return "#10b981"; + return "#3b82f6"; + }; + + const opacityFor = (idx) => { + if (idx === keyIndex || idx < sortedFrom) return "0.9"; + if (shifts(idx)) return "0.5"; + return "0.25"; + }; + + const labelFor = (idx) => { + if (idx === keyIndex) return "key"; + if (targetIndex !== null && idx === targetIndex && idx !== keyIndex) + return "insert here"; + return null; + }; + + const moves = targetIndex !== null && keyIndex !== targetIndex; + + return ( + + + + + + + + {moves && ( + + )} + + {values.map((val, idx) => { + const label = labelFor(idx); + return ( + + {label && ( + + {label} + + )} + + + {val} + + + {idx} + + + ); + })} + + ); +}; + const Content = () => { const { theme } = useTheme(); @@ -18,22 +149,42 @@ const Content = () => { { points: "First Element (7):", subpoints: ['Already "sorted" as the first item', "→ [7, 3, 5, 2, 1]"], + array: [7, 3, 5, 2, 1], + sortedFrom: 0, + keyIndex: 0, + targetIndex: 0, }, { points: "Second Element (3):", subpoints: ["Insert before 7", "→ [3, 7, 5, 2, 1]"], + array: [7, 3, 5, 2, 1], + sortedFrom: 1, + keyIndex: 1, + targetIndex: 0, }, { points: "Third Element (5):", subpoints: ["Insert between 3 and 7", "→ [3, 5, 7, 2, 1]"], + array: [3, 7, 5, 2, 1], + sortedFrom: 2, + keyIndex: 2, + targetIndex: 1, }, { points: "Fourth Element (2):", subpoints: ["Insert at beginning", "→ [2, 3, 5, 7, 1]"], + array: [3, 5, 7, 2, 1], + sortedFrom: 3, + keyIndex: 3, + targetIndex: 0, }, { points: "Fifth Element (1):", subpoints: ["Insert at beginning", "→ [1, 2, 3, 5, 7]"], + array: [2, 3, 5, 7, 1], + sortedFrom: 4, + keyIndex: 4, + targetIndex: 0, }, ]; @@ -106,7 +257,7 @@ const Content = () => { Consider this unsorted array: [7, 3, 5, 2, 1]

-
    +
      {working.map((items, index) => (
    1. { ))} )} + {items.array && ( +
      + +
      + )}
    2. ))}
    +
    + + + Key (being inserted) + + + + Shifts right + + + + Sorted portion + +
    +

    {paragraph[1]}

    diff --git a/app/visualizer/sorting/quicksort/content.jsx b/app/visualizer/sorting/quicksort/content.jsx index a8b9afa..4d89699 100755 --- a/app/visualizer/sorting/quicksort/content.jsx +++ b/app/visualizer/sorting/quicksort/content.jsx @@ -4,6 +4,204 @@ 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"; + +const MiniArrayGroup = ({ values, keyPrefix, accent }) => { + const boxSize = 34; + const gap = 6; + const paddingX = 6; + const paddingY = 6; + const width = Math.max(values.length, 1) * (boxSize + gap) - gap + paddingX * 2; + const height = boxSize + paddingY * 2; + + const fill = + accent === "final" ? "#10b981" : accent === "pivot" ? "#f59e0b" : accent === "dim" ? "#94a3b8" : "#3b82f6"; + + if (values.length === 0) { + return ( + + empty + + ); + } + + const boxX = (idx) => paddingX + idx * (boxSize + gap); + const cx = (idx) => boxX(idx) + boxSize / 2; + + return ( + + {values.map((val, idx) => ( + + + + {val} + + + ))} + + ); +}; + +const PartitionFlowDiagram = ({ + beforeValues, + pivotIndex, + afterValues, + pivotFinalIndex, + keyPrefix, +}) => { + const boxSize = 36; + const gap = 6; + const paddingX = 8; + const row1Y = 24; + const rowGap = 46; + const row2Y = row1Y + boxSize + rowGap; + const pivotValue = beforeValues[pivotIndex]; + const width = beforeValues.length * (boxSize + gap) - gap + paddingX * 2; + const height = row2Y + boxSize + 4; + + const boxX = (idx) => paddingX + idx * (boxSize + gap); + const cx = (idx) => boxX(idx) + boxSize / 2; + + const beforeFill = (idx) => { + if (idx === pivotIndex) return "#f59e0b"; + return beforeValues[idx] < pivotValue ? "#3b82f6" : "#94a3b8"; + }; + + const afterFill = (idx) => { + if (idx === pivotFinalIndex) return "#10b981"; + return idx < pivotFinalIndex ? "#3b82f6" : "#94a3b8"; + }; + + const arrowStartX = cx(pivotIndex); + const arrowStartY = row1Y + boxSize + 4; + const arrowEndX = cx(pivotFinalIndex); + const arrowEndY = row2Y - 4; + const arrowControlY = (arrowStartY + arrowEndY) / 2; + + return ( + + + + + + + + + + {beforeValues.map((val, idx) => ( + + {idx === pivotIndex && ( + + pivot + + )} + + + {val} + + + ))} + + {afterValues.map((val, idx) => ( + + + + {val} + + + ))} + + ); +}; + +const StepArrow = ({ down }) => ( + + {down ? "↓" : "→"} + +); + const Content = () => { const { theme } = useTheme(); @@ -139,7 +337,7 @@ const Content = () => { Consider this unsorted array: [10, 80, 30, 90, 40, 50, 70]

    -
      +
        {working.map((item, index) => (
      • {item.steps} @@ -155,9 +353,87 @@ const Content = () => { ))}
)} + + {index === 0 && ( +
+ +
+ )} + + {index === 1 && ( +
+
+
+ Partition [10, 30, 40, 50]: +
+ +
+ +
+
+ Partition [80, 90]: +
+ +
+ +
+
+ Combine sorted left + pivot + sorted right: +
+
+ + + + + +
+
+
+ )} ))} + +
+ + + Pivot + + + + Less than pivot + + + + Greater than pivot + + + + Fully sorted + +
diff --git a/app/visualizer/sorting/selectionsort/content.jsx b/app/visualizer/sorting/selectionsort/content.jsx index d7acb49..7fb7325 100755 --- a/app/visualizer/sorting/selectionsort/content.jsx +++ b/app/visualizer/sorting/selectionsort/content.jsx @@ -4,6 +4,130 @@ 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"; + +const SelectionStepDiagram = ({ + values, + sortedFrom, + boundary, + minIndex, + swapped, + keyPrefix, +}) => { + const boxSize = 40; + const gap = 8; + const paddingX = 8; + const topPadding = 42; + const width = values.length * (boxSize + gap) - gap + paddingX * 2; + const height = boxSize + topPadding + 22; + + const boxX = (idx) => paddingX + idx * (boxSize + gap); + const boxY = topPadding; + const cx = (idx) => boxX(idx) + boxSize / 2; + + const fillFor = (idx) => { + if (idx === boundary && idx === minIndex) return "#f59e0b"; + if (idx === minIndex) return "#3b82f6"; + if (idx === boundary) return "#f59e0b"; + if (idx < sortedFrom) return "#10b981"; + return "#3b82f6"; + }; + + const opacityFor = (idx) => { + if (idx === boundary || idx === minIndex || idx < sortedFrom) return "0.9"; + return "0.25"; + }; + + const labelFor = (idx) => { + if (idx === boundary && idx === minIndex) return "pos"; + if (idx === minIndex) return "min"; + if (idx === boundary) return "pos"; + return null; + }; + + return ( + + + + + + + + {swapped && ( + + )} + + {values.map((val, idx) => { + const label = labelFor(idx); + return ( + + {label && ( + + {label} + + )} + + + {val} + + + {idx} + + + ); + })} + + ); +}; + const Content = () => { const { theme } = useTheme(); @@ -21,6 +145,11 @@ const Content = () => { "Find the minimum in [64, 25, 12, 22, 11] → 11 at index 4", "Swap with first element → [11, 25, 12, 22, 64]", ], + array: [64, 25, 12, 22, 11], + sortedFrom: 0, + boundary: 0, + minIndex: 4, + swapped: true, }, { pass: "Second Pass:", @@ -28,6 +157,11 @@ const Content = () => { "Find minimum in [25, 12, 22, 64] → 12 at index 2", "Swap with first element → [11, 12, 25, 22, 64]", ], + array: [11, 25, 12, 22, 64], + sortedFrom: 1, + boundary: 1, + minIndex: 2, + swapped: true, }, { pass: "Third Pass:", @@ -35,6 +169,11 @@ const Content = () => { "Find minimum in [25, 22, 64] → 22 at index 2", "Swap with first element → [11, 12, 22, 25, 64]", ], + array: [11, 12, 25, 22, 64], + sortedFrom: 2, + boundary: 2, + minIndex: 3, + swapped: true, }, { pass: "Fourth Pass:", @@ -42,8 +181,21 @@ const Content = () => { "Find minimum in [25, 64] → 25 at index 0", "No swap needed → [11, 12, 22, 25, 64]", ], + array: [11, 12, 22, 25, 64], + sortedFrom: 3, + boundary: 3, + minIndex: 3, + swapped: false, + }, + { + pass: "Result:", + points: ["[11, 12, 22, 25, 64]"], + array: [11, 12, 22, 25, 64], + sortedFrom: 5, + boundary: null, + minIndex: null, + swapped: false, }, - { pass: "Result:", points: ["[11, 12, 22, 25, 64]"] }, ]; const algorithm = [ @@ -108,7 +260,7 @@ const Content = () => { Consider this unsorted array: [64, 25, 12, 22, 11]

-
    +
      {working.map((items, index) => (
    1. { ))} )} + {items.array && ( +
      + +
      + )}
    2. ))}
    + +
    + + + Boundary position + + + + Smallest found so far + + + + Sorted portion + +
diff --git a/app/visualizer/stack/push-pop/animation.jsx b/app/visualizer/stack/push-pop/animation.jsx index ab6e116..652be02 100755 --- a/app/visualizer/stack/push-pop/animation.jsx +++ b/app/visualizer/stack/push-pop/animation.jsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState, useEffect, useLayoutEffect, useRef } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { gsap } from "gsap"; import PushPop from "@/app/components/ui/PushPop"; @@ -8,104 +8,135 @@ const StackVisualizer = () => { const [operation, setOperation] = useState(null); const [message, setMessage] = useState("Stack is empty"); const [isAnimating, setIsAnimating] = useState(false); - const stackRefs = useRef([]); + const topRef = useRef(null); - // Reset stack - const reset = () => { - setStack([]); - setMessage("Stack is empty"); - setOperation(null); + // Intercept state updates coming from PushPop so a single-item pop + // (stack shrinking by exactly one) gets a "fly away" exit animation + // before it's actually removed. Pushes and resets pass straight through. + const handleSetStack = (updater) => { + const next = typeof updater === "function" ? updater(stack) : updater; + + if (stack.length - next.length === 1 && topRef.current) { + const el = topRef.current; + gsap.to(el, { + y: -140, + x: 36, + rotate: 14, + opacity: 0, + duration: 0.4, + ease: "power2.in", + onComplete: () => setStack(next), + }); + return; + } + + setStack(next); }; - useEffect(() => { - if (isAnimating && stackRefs.current.length > 0) { - const el = stackRefs.current[0]; - if (operation?.includes("pushed")) { - gsap.fromTo( - el, - { y: -50, opacity: 0 }, - { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" } - ); - } else if (operation?.includes("popped")) { - gsap.to(el, { y: 50, opacity: 0, duration: 0.3, ease: "power1.in" }); - } else if (operation?.includes("Peek")) { - gsap.fromTo( - el, - { scale: 1 }, - { scale: 1.2, yoyo: true, repeat: 1, duration: 0.2 } - ); + // Runs the moment a new "top" box mounts in the DOM — a reliable + // drop-in entrance that doesn't depend on any surrounding state timing. + const animateDropIn = (el) => { + if (!el || el.dataset.dropped) return; + el.dataset.dropped = "true"; + gsap.fromTo( + el, + { y: -220, opacity: 0 }, + { + y: 0, + opacity: 1, + duration: 0.55, + ease: "bounce.out", + onComplete: () => { + gsap.fromTo( + el, + { scaleX: 1, scaleY: 1 }, + { + scaleX: 1.08, + scaleY: 0.85, + duration: 0.08, + yoyo: true, + repeat: 1, + ease: "power1.out", + } + ); + }, } + ); + }; + + useEffect(() => { + if (operation?.includes("Peek") && topRef.current) { + gsap.fromTo( + topRef.current, + { scale: 1 }, + { scale: 1.15, yoyo: true, repeat: 1, duration: 0.2, ease: "power1.inOut" } + ); } - }, [stack, operation, isAnimating]); + }, [operation]); + + // Oldest first so the container can be laid out column-reverse: the + // newest item always lands visually on top without existing items + // ever needing to move, matching how a physical stack behaves. + const displayStack = [...stack].reverse(); return ( -
+

Visualize the LIFO (Last In, First Out) principle

- {/* Use the PushPop component */} - {/* Stack Visualization */}
-

Stack Visualization

- - {/* Operation Status */} {operation && ( -
+
{operation}
)} - {/* Vertical Stack */} -
- {/* Top indicator */} -
- {stack.length > 0 ? "↑ Top" : ""} +
+
+ {stack.length > 0 && "↑ Top"}
- {/* Stack elements */} -
- {stack.length === 0 ? ( -
- Stack is empty -
- ) : ( -
- {stack.map((item, index) => ( -
(stackRefs.current[index] = el)} - className={`p-3 border-2 rounded text-center font-medium transition-all duration-300 ${ - index === 0 - ? "bg-blue-100 dark:bg-blue-900 border-blue-300 dark:border-blue-700" - : "bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600" - }`} - > -
{item}
- {index === 0 && ( -
- (Top) -
- )} -
- ))} +
+ {stack.length === 0 && ( +
+ Stack is empty
)} + + {displayStack.map((item, i) => { + const isTop = i === displayStack.length - 1; + return ( +
{ + if (isTop) topRef.current = el; + animateDropIn(el); + }} + className={`h-11 shrink-0 rounded-lg shadow-md flex items-center justify-center font-semibold text-sm text-white bg-gradient-to-br ${ + isTop + ? "from-blue-500 to-blue-600 ring-2 ring-blue-300 dark:ring-blue-700 ring-offset-2 ring-offset-white dark:ring-offset-neutral-950" + : "from-slate-400 to-slate-500 dark:from-slate-600 dark:to-slate-700" + }`} + > + {item} +
+ ); + })}
- {/* Bottom indicator */} -
- {stack.length > 0 ? "↓ Bottom" : ""} +
+ {stack.length > 0 && "Bottom"}
diff --git a/app/visualizer/trees/binaryTree/properties/animation.jsx b/app/visualizer/trees/binaryTree/properties/animation.jsx new file mode 100755 index 0000000..40c58b0 --- /dev/null +++ b/app/visualizer/trees/binaryTree/properties/animation.jsx @@ -0,0 +1,321 @@ +"use client"; +import React, { useState, useRef } from "react"; +import { gsap } from "gsap"; +import { Plus, Shuffle, RotateCcw } from "lucide-react"; + +class TreeNode { + constructor(value) { + this.value = value; + this.left = null; + this.right = null; + } +} + +const insertNode = (node, value) => { + 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; +}; + +const computeStats = (node) => { + if (!node) return { height: -1, total: 0, leaves: 0 }; + const left = computeStats(node.left); + const right = computeStats(node.right); + const isLeaf = !node.left && !node.right; + return { + height: 1 + Math.max(left.height, right.height), + total: 1 + left.total + right.total, + leaves: isLeaf ? 1 : left.leaves + right.leaves, + }; +}; + +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, + }); + 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, + }); + layoutTree(node.right, depth + 1, rightX, rightY, nodes, edges); + } + + return { nodes, edges }; +}; + +const TreePropertiesVisualizer = () => { + const [root, setRoot] = useState(null); + const [inputValue, setInputValue] = useState(""); + const [message, setMessage] = useState("Tree is empty"); + const lastInsertedRef = useRef(null); + + 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 handleInsert = () => { + const value = parseInt(inputValue, 10); + if (Number.isNaN(value)) { + setMessage("Please enter a valid number"); + return; + } + setRoot((prev) => { + const cloned = prev ? structuredClone(prev) : null; + const updated = insertNode(cloned, value); + return updated; + }); + lastInsertedRef.current = value; + setMessage(`Inserted ${value}`); + setInputValue(""); + }; + + const generateRandomTree = () => { + const size = Math.floor(Math.random() * 5) + 5; + 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`); + }; + + const reset = () => { + setRoot(null); + setInputValue(""); + setMessage("Tree is empty"); + }; + + const { nodes, edges } = root ? layoutTree(root) : { nodes: [], edges: [] }; + const stats = computeStats(root); + + 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(); + + return ( +
+

+ Build a tree and watch its height, depth, and node counts update live +

+ +
+ {/* Controls */} +
+
+ setInputValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleInsert()} + placeholder="Enter a number" + 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" + /> + +
+
+ + +
+
+ + {/* Visualization */} +
+
+ {message} +
+ +
+
+
Height
+
{root ? stats.height : "—"}
+
+
+
Total Nodes
+
{stats.total}
+
+
+
Leaf Nodes
+
{stats.leaves}
+
+
+ +
+ {nodes.length > 0 ? ( + + + + + + + + + + + + + + + + {edges.map((edge, i) => { + const midY = (edge.y1 + edge.y2) / 2; + return ( + + ); + })} + + {nodes.map((node, i) => ( + + {node.isRoot && ( + + )} + + + {node.value} + + + + + d{node.depth} + + + ))} + + ) : ( +
+ No tree yet — insert a value or generate a random tree +
+ )} +
+ +
+ + + Internal node + + + + Leaf node + + + + Root + + + d0, d1, d2… = depth from root + +
+
+
+
+ ); +}; + +export default TreePropertiesVisualizer; diff --git a/app/visualizer/trees/binaryTree/properties/code.js b/app/visualizer/trees/binaryTree/properties/code.js new file mode 100755 index 0000000..83e8468 --- /dev/null +++ b/app/visualizer/trees/binaryTree/properties/code.js @@ -0,0 +1,168 @@ +const codeExamples = { + javascript: `// Binary Tree node +class TreeNode { + constructor(value) { + this.value = value; + this.left = null; + this.right = null; + } +} + +// Height: number of edges on the longest root-to-leaf path +function height(node) { + if (node === null) return -1; + return 1 + Math.max(height(node.left), height(node.right)); +} + +// Total node count +function countNodes(node) { + if (node === null) return 0; + return 1 + countNodes(node.left) + countNodes(node.right); +} + +// Leaf node count +function countLeaves(node) { + if (node === null) return 0; + if (node.left === null && node.right === null) return 1; + return countLeaves(node.left) + countLeaves(node.right); +} + +// Usage example +const root = new TreeNode('A'); +root.left = new TreeNode('B'); +root.right = new TreeNode('C'); +root.left.left = new TreeNode('D'); + +console.log('Height:', height(root)); +console.log('Total nodes:', countNodes(root)); +console.log('Leaf nodes:', countLeaves(root));`, + + python: `# Binary Tree node +class TreeNode: + def __init__(self, value): + self.value = value + self.left = None + self.right = None + +# Height: number of edges on the longest root-to-leaf path +def height(node): + if node is None: + return -1 + return 1 + max(height(node.left), height(node.right)) + +# Total node count +def count_nodes(node): + if node is None: + return 0 + return 1 + count_nodes(node.left) + count_nodes(node.right) + +# Leaf node count +def count_leaves(node): + if node is None: + return 0 + if node.left is None and node.right is None: + return 1 + return count_leaves(node.left) + count_leaves(node.right) + +# Usage example +root = TreeNode('A') +root.left = TreeNode('B') +root.right = TreeNode('C') +root.left.left = TreeNode('D') + +print('Height:', height(root)) +print('Total nodes:', count_nodes(root)) +print('Leaf nodes:', count_leaves(root))`, + + c: `#include +#include + +typedef struct TreeNode { + char value; + struct TreeNode *left, *right; +} TreeNode; + +TreeNode* newNode(char value) { + TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode)); + node->value = value; + node->left = node->right = NULL; + return node; +} + +// Height: number of edges on the longest root-to-leaf path +int height(TreeNode* node) { + if (node == NULL) return -1; + int leftHeight = height(node->left); + int rightHeight = height(node->right); + return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight); +} + +// Total node count +int countNodes(TreeNode* node) { + if (node == NULL) return 0; + return 1 + countNodes(node->left) + countNodes(node->right); +} + +// Leaf node count +int countLeaves(TreeNode* node) { + if (node == NULL) return 0; + if (node->left == NULL && node->right == NULL) return 1; + return countLeaves(node->left) + countLeaves(node->right); +} + +int main() { + TreeNode* root = newNode('A'); + root->left = newNode('B'); + root->right = newNode('C'); + root->left->left = newNode('D'); + + printf("Height: %d\\n", height(root)); + printf("Total nodes: %d\\n", countNodes(root)); + printf("Leaf nodes: %d\\n", countLeaves(root)); + return 0; +}`, + + java: `class TreeNode { + char value; + TreeNode left, right; + + TreeNode(char value) { + this.value = value; + } +} + +public class BinaryTreeProperties { + + // Height: number of edges on the longest root-to-leaf path + static int height(TreeNode node) { + if (node == null) return -1; + return 1 + Math.max(height(node.left), height(node.right)); + } + + // Total node count + static int countNodes(TreeNode node) { + if (node == null) return 0; + return 1 + countNodes(node.left) + countNodes(node.right); + } + + // Leaf node count + static int countLeaves(TreeNode node) { + if (node == null) return 0; + if (node.left == null && node.right == null) return 1; + return countLeaves(node.left) + countLeaves(node.right); + } + + public static void main(String[] args) { + TreeNode root = new TreeNode('A'); + root.left = new TreeNode('B'); + root.right = new TreeNode('C'); + root.left.left = new TreeNode('D'); + + System.out.println("Height: " + height(root)); + System.out.println("Total nodes: " + countNodes(root)); + System.out.println("Leaf nodes: " + countLeaves(root)); + } +}`, +}; + +export default codeExamples; diff --git a/app/visualizer/trees/binaryTree/properties/content.jsx b/app/visualizer/trees/binaryTree/properties/content.jsx new file mode 100755 index 0000000..5a9ebdc --- /dev/null +++ b/app/visualizer/trees/binaryTree/properties/content.jsx @@ -0,0 +1,323 @@ +"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 LabeledTreeDiagram = () => { + const nodes = [ + { id: "A", x: 110, y: 30, depth: 0, leaf: false }, + { id: "B", x: 80, y: 80, depth: 1, leaf: false }, + { id: "C", x: 140, y: 80, depth: 1, leaf: false }, + { id: "D", x: 65, y: 130, depth: 2, leaf: true }, + { id: "E", x: 95, y: 130, depth: 2, leaf: true }, + { id: "F", x: 125, y: 130, depth: 2, leaf: true }, + { id: "G", x: 155, y: 130, depth: 2, leaf: true }, + ]; + const edges = [ + ["A", "B"], + ["A", "C"], + ["B", "D"], + ["B", "E"], + ["C", "F"], + ["C", "G"], + ]; + const byId = Object.fromEntries(nodes.map((n) => [n.id, n])); + const levelRows = [ + { y: 30, label: "Depth 0" }, + { y: 80, label: "Depth 1" }, + { y: 130, label: "Depth 2" }, + ]; + + return ( + + {levelRows.map((row) => ( + + + + {row.label} + + + ))} + + {edges.map(([from, to], i) => ( + + ))} + + {nodes.map((n, i) => ( + + + + {n.id} + + + ))} + + ); +}; + +const Content = () => { + const { theme } = useTheme(); + + const paragraphs = [ + `A Binary Tree is a hierarchical structure built from nodes, where every node has at most two children, conventionally called the left and right child. That two-child limit is what separates it from a general tree, and it's what makes every property below meaningful and calculable.`, + `Height and depth are the two measurements that come up constantly when reasoning about a tree's performance — most tree operations run in time proportional to the height, not the number of nodes, which is exactly why keeping a tree balanced matters so much.`, + `Space Complexity for storing a binary tree with n nodes is O(n), since each node needs a fixed amount of memory (its value plus two child pointers) regardless of the tree's shape.`, + `These structural properties aren't just theory — they're what a balancing algorithm (like in AVL or Red-Black trees) is actively trying to protect. A tree that's allowed to grow unchecked can degrade to the same O(n) worst case as a linked list.`, + ]; + + const terminology = [ + { term: "Root", def: "The single node at the top of the tree with no parent." }, + { term: "Parent / Child", def: "A node directly connected one level above/below another." }, + { term: "Sibling", def: "Nodes that share the same parent." }, + { term: "Leaf", def: "A node with no children (both left and right are null)." }, + { term: "Internal Node", def: "Any node with at least one child (includes the root)." }, + { term: "Edge", def: "The connection/link between a parent and its child." }, + { term: "Depth of a node", def: "Number of edges from the root down to that node." }, + { term: "Height of a node", def: "Number of edges on the longest path from that node down to a leaf." }, + { term: "Height of the tree", def: "The height of the root node — the longest root-to-leaf path." }, + ]; + + const nodeCountFormulas = [ + { label: "Max nodes at depth d", formula: "2^d" }, + { label: "Max total nodes for height h", formula: "2^(h+1) − 1" }, + { label: "Min total nodes for height h", formula: "h + 1" }, + { label: "Min possible height for n nodes", formula: "⌊log₂ n⌋" }, + ]; + + const balanceComparison = [ + { points: "Balanced tree, n = 7 nodes → height = 2 (as close to log₂ 7 as possible)" }, + { points: "Skewed tree, n = 7 nodes → height = 6 (every node has exactly one child)" }, + { points: "Same node count, wildly different performance — height is what actually matters" }, + ]; + + return ( +
+
+ + +
+
+ {/* What is a Binary Tree */} +
+

+ + What is a Binary Tree? +

+
+

+ {paragraphs[0]} +

+
+
+ + {/* Terminology */} +
+

+ + Key Terminology +

+
+ {terminology.map((item) => ( +
+ + {item.term}: + {" "} + + {item.def} + +
+ ))} +
+
+ + {/* Height, Depth & Level */} +
+

+ + Height, Depth & Level +

+
+

+ {paragraphs[1]} +

+
+ + + +
+ + + Internal node + + + + Leaf node + +
+ +

+ In this tree, node A sits at depth 0 (the root), B and C sit at depth 1, and + D, E, F, G all sit at depth 2. Since the deepest node is at depth 2, the + tree's height is 2. +

+
+ + {/* Node Count Formulas */} +
+

+ + Node Count Formulas +

+
+ + + + + + + + + {nodeCountFormulas.map((row, index) => ( + + + + + ))} + +
+ Property + + Formula +
+ {row.label} + + {row.formula} +
+
+
+ + {/* Balanced vs Skewed */} +
+

+ + Why Balance Matters +

+
+
    + {balanceComparison.map((item, index) => ( +
  • + {item.points} +
  • + ))} +
+
+
+ + {/* Complexity */} +
+

+ + Complexity Implications +

+
+

+ Since most tree operations (search, insert, delete) walk from the root + down to a leaf, their time complexity is O(height) — O(log n) for a + balanced tree, degrading to O(n) for a skewed one. +

+
+ +
+ Math.log2(n)} + averageCase={(n) => Math.log2(n)} + worstCase={(n) => n} + maxN={25} + /> +
+ + + +

+ {paragraphs[2]} +

+
+ + {/* Additional Info */} +
+
+
+

+ {paragraphs[3]} +

+
+
+
+
+ + +
+ ); +}; + +export default Content; diff --git a/app/visualizer/trees/binaryTree/properties/page.jsx b/app/visualizer/trees/binaryTree/properties/page.jsx new file mode 100755 index 0000000..ed0330f --- /dev/null +++ b/app/visualizer/trees/binaryTree/properties/page.jsx @@ -0,0 +1,104 @@ +import Animation from "@/app/visualizer/trees/binaryTree/properties/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/binaryTree/properties/quiz"; +import Content from "@/app/visualizer/trees/binaryTree/properties/content"; +import ModuleCard from "@/app/components/ui/ModuleCard"; +import { MODULE_MAPS } from "@/lib/modulesMap"; + +export const metadata = { + title: "Binary Tree Properties | Height, Depth, Node Count & Balance Explained", + description: + "Learn the core structural properties of Binary Trees — height, depth, level, node count formulas, leaf vs internal nodes, and why balance matters — with an interactive tree builder, code examples in JavaScript, C, Python, and Java, and a quiz.", + keywords: [ + "Binary Tree Properties", + "Binary Tree Height", + "Binary Tree Depth", + "Binary Tree Level", + "Leaf Node vs Internal Node", + "Binary Tree Node Count Formula", + "Balanced Binary Tree", + "Skewed Binary Tree", + "Binary Tree Visualization", + "DSA Binary Trees", + "Binary Tree Height in JavaScript", + "Binary Tree Height in C", + "Binary Tree Height in Python", + "Binary Tree Height in Java", + "Learn Binary Trees DSA", + "Binary Tree Quiz", + ], + robots: "index, follow", + openGraph: { + images: [ + { + url: "/og.png", + width: 1200, + height: 630, + alt: "Binary Tree Properties Visualization", + }, + ], + }, +}; + +export default function Page() { + const paths = [ + { name: "Home", href: "/" }, + { name: "Visualizer", href: "/visualizer" }, + { name: "Trees : Binary Tree Properties", href: "" }, + ]; + + return ( + <> +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+

+ Test Your Knowledge before moving forward! +

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