diff --git a/src/components/tree/tree-node.jsx b/src/components/tree/tree-node.jsx
index f6d771b..69e4966 100644
--- a/src/components/tree/tree-node.jsx
+++ b/src/components/tree/tree-node.jsx
@@ -11,11 +11,21 @@ const FILL = {
remove: ['#f43f5e', '#be123c'],
};
+// red/black node fills (used when a `color` is given, e.g. Red-Black trees)
+const RB_FILL = {
+ red: ['#dc2626', '#b91c1c'],
+ black: ['#1f2937', '#0f172a'],
+};
+// when a node has a fixed red/black fill, the operation highlight is drawn as
+// an outer ring instead of recoloring the fill
+const RING = { current: '#f59e0b', found: '#10b981', visited: '#94a3b8', remove: '#f43f5e' };
+
const R = 16;
-export default function TreeNode({ x, y, value, secondary, state }) {
+export default function TreeNode({ x, y, value, secondary, state, color }) {
if (state === 'hidden') return null;
- const [bg, border] = FILL[state] || FILL.normal;
+ const [bg, border] = color ? RB_FILL[color] || RB_FILL.black : FILL[state] || FILL.normal;
+ const ring = color ? RING[state] : null; // operation highlight for colored nodes
// shrink the primary label so longer values (e.g. recursion call labels
// like "(4,3)") still fit inside the circle
const len = String(value ?? '').length;
@@ -24,7 +34,8 @@ export default function TreeNode({ x, y, value, secondary, state }) {
return (
-
+ {ring && }
+
({ tree: treeRef.current });
const clear = () => { if (isRunningRef.current) return; applyTree(null); clearMarks(); };
+ const reset = (t) => { if (isRunningRef.current) return; applyTree(t); clearMarks(); };
const setSpeed = (s) => { speedRef.current = toDelay(s); };
- return { tree, nodeState, edgeState, labels, status, isRunning, run, getContext, clear, setSpeed };
+ return { tree, nodeState, edgeState, labels, status, isRunning, run, getContext, clear, reset, setSpeed };
}
function sleep(ms) {
diff --git a/src/lib/algorithms/heap.js b/src/lib/algorithms/heap.js
new file mode 100644
index 0000000..1fe9a1d
--- /dev/null
+++ b/src/lib/algorithms/heap.js
@@ -0,0 +1,146 @@
+// Binary Heap (min-heap) — action-log planners for insert / extract-min /
+// build-heap / heapsort, rendered on the reusable tree component.
+//
+// A heap is a complete binary tree stored as an array: node i's children are
+// 2i+1 and 2i+2. We keep the heap as an array of { id, value } and rebuild a
+// logical tree from it for each `setTree`. A sift swaps two array entries
+// *including their ids*, so the two nodes' target positions swap and the canvas
+// tween slides them past each other.
+
+let counter = 0;
+const mkNode = (value) => ({ id: 'h' + ++counter, value });
+
+// array of { id, value } -> logical tree { id, value, left, right }
+export function buildTree(arr) {
+ const make = (i) => {
+ if (i >= arr.length) return null;
+ return { id: arr[i].id, value: arr[i].value, left: make(2 * i + 1), right: make(2 * i + 2) };
+ };
+ return make(0);
+}
+
+// logical complete tree -> level-order array of { id, value }
+export function toArray(tree) {
+ const out = [];
+ const q = tree ? [tree] : [];
+ while (q.length) {
+ const n = q.shift();
+ out.push({ id: n.id, value: n.value });
+ if (n.left) q.push(n.left);
+ if (n.right) q.push(n.right);
+ }
+ return out;
+}
+
+export function fromValues(values) {
+ const a = values.map(mkNode);
+ heapify(a);
+ return buildTree(a);
+}
+
+// in-place Floyd build-heap (no animation) — used for seeding
+function heapify(a) {
+ for (let i = Math.floor(a.length / 2) - 1; i >= 0; i--) siftDown(a, i, a.length);
+}
+
+function siftDown(a, i, size) {
+ for (;;) {
+ let small = i;
+ const l = 2 * i + 1;
+ const r = 2 * i + 2;
+ if (l < size && a[l].value < a[small].value) small = l;
+ if (r < size && a[r].value < a[small].value) small = r;
+ if (small === i) break;
+ [a[i], a[small]] = [a[small], a[i]];
+ i = small;
+ }
+}
+
+const snap = (actions, a) => actions.push({ type: 'setTree', tree: buildTree(a) });
+const mark = (actions, a, idxs, state) => {
+ for (const i of idxs) if (a[i]) actions.push({ type: 'markNode', id: a[i].id, state });
+};
+
+export function insertActions(tree, value) {
+ const actions = [];
+ const a = toArray(tree);
+ a.push(mkNode(value));
+ snap(actions, a); // appears at the end
+ let i = a.length - 1;
+ while (i > 0) {
+ const p = Math.floor((i - 1) / 2);
+ mark(actions, a, [i, p], 'current');
+ if (a[i].value >= a[p].value) break;
+ [a[i], a[p]] = [a[p], a[i]]; // ids travel with values
+ snap(actions, a);
+ i = p;
+ }
+ actions.push({ type: 'status', text: `Inserted ${value}` });
+ return actions;
+}
+
+export function extractActions(tree) {
+ const actions = [];
+ const a = toArray(tree);
+ if (!a.length) {
+ actions.push({ type: 'status', text: 'Heap is empty' });
+ actions.push({ type: 'clear' });
+ return actions;
+ }
+ const min = a[0].value;
+ mark(actions, a, [0], 'current');
+ const last = a.length - 1;
+ if (last > 0) {
+ [a[0], a[last]] = [a[last], a[0]];
+ snap(actions, a); // min swapped to the end
+ }
+ a.pop(); // remove the min
+ snap(actions, a);
+ siftDownActions(actions, a, 0, a.length);
+ actions.push({ type: 'status', text: `Extracted min ${min}` });
+ return actions;
+}
+
+// animated sift-down: emit a snapshot per swap within a[0..size)
+function siftDownActions(actions, a, i, size) {
+ for (;;) {
+ let small = i;
+ const l = 2 * i + 1;
+ const r = 2 * i + 2;
+ if (l < size && a[l].value < a[small].value) small = l;
+ if (r < size && a[r].value < a[small].value) small = r;
+ mark(actions, a, [i, l, r].filter((k) => k < size), 'current');
+ if (small === i) break;
+ [a[i], a[small]] = [a[small], a[i]];
+ snap(actions, a); // render the full array; `size` only bounds the sift
+ i = small;
+ }
+}
+
+export function buildActions(values) {
+ const actions = [];
+ const a = values.map(mkNode);
+ snap(actions, a); // raw, unheapified
+ for (let i = Math.floor(a.length / 2) - 1; i >= 0; i--) {
+ siftDownActions(actions, a, i, a.length);
+ }
+ actions.push({ type: 'status', text: 'Built min-heap' });
+ return actions;
+}
+
+export function heapsortActions(tree) {
+ const actions = [];
+ const a = toArray(tree);
+ let size = a.length;
+ while (size > 1) {
+ const last = size - 1;
+ [a[0], a[last]] = [a[last], a[0]];
+ snap(actions, a); // current min parked at the end of the active region
+ actions.push({ type: 'markNode', id: a[last].id, state: 'found' }); // sorted
+ size--;
+ siftDownActions(actions, a, 0, size);
+ }
+ if (a.length) actions.push({ type: 'markNode', id: a[0].id, state: 'found' });
+ actions.push({ type: 'status', text: 'Heapsort complete (descending)' });
+ return actions;
+}
diff --git a/src/app/recursion-tree/fib.jsx b/src/lib/algorithms/recursion.js
similarity index 100%
rename from src/app/recursion-tree/fib.jsx
rename to src/lib/algorithms/recursion.js
diff --git a/src/lib/algorithms/redBlack.js b/src/lib/algorithms/redBlack.js
new file mode 100644
index 0000000..cccee93
--- /dev/null
+++ b/src/lib/algorithms/redBlack.js
@@ -0,0 +1,295 @@
+// Red-Black tree — action-log planners for insert / delete / search.
+//
+// Node (rendered): { id, value, left, right, color: 'red' | 'black' }.
+//
+// Immutable RB delete with stable ids is painful, so each planner builds a
+// MUTABLE working copy (parent pointers + a nil sentinel, ids preserved), runs
+// the standard CLRS algorithm on it, and emits an immutable snapshot() as a
+// `setTree` action after every rotation / recolor. The tree component then
+// animates each step (nodes slide on rotations; fills fade on recolor).
+
+import { searchActions as bstSearchActions } from './bst';
+
+let counter = 0;
+
+// A fresh context (working tree + nil sentinel) per operation, so the shared
+// nil's transient parent pointers never leak across calls.
+function rbContext(immutableRoot) {
+ const nil = { id: null, value: null, color: 'black' };
+ nil.left = nil.right = nil.parent = nil;
+ const T = { root: nil };
+
+ const build = (r) => {
+ if (!r) return nil;
+ const n = { id: r.id, value: r.value, color: r.color || 'black', left: nil, right: nil, parent: nil };
+ n.left = build(r.left);
+ if (n.left !== nil) n.left.parent = n;
+ n.right = build(r.right);
+ if (n.right !== nil) n.right.parent = n;
+ return n;
+ };
+ T.root = build(immutableRoot);
+
+ const snap = (n) => (n === nil ? null : { id: n.id, value: n.value, color: n.color, left: snap(n.left), right: snap(n.right) });
+
+ const emit = (actions, activeId) => {
+ if (!actions) return;
+ actions.push({ type: 'setTree', tree: snap(T.root) });
+ if (activeId) actions.push({ type: 'markNode', id: activeId, state: 'current' });
+ };
+
+ const minimum = (n) => {
+ while (n.left !== nil) n = n.left;
+ return n;
+ };
+
+ const leftRotate = (x) => {
+ const y = x.right;
+ x.right = y.left;
+ if (y.left !== nil) y.left.parent = x;
+ y.parent = x.parent;
+ if (x.parent === nil) T.root = y;
+ else if (x === x.parent.left) x.parent.left = y;
+ else x.parent.right = y;
+ y.left = x;
+ x.parent = y;
+ };
+
+ const rightRotate = (x) => {
+ const y = x.left;
+ x.left = y.right;
+ if (y.right !== nil) y.right.parent = x;
+ y.parent = x.parent;
+ if (x.parent === nil) T.root = y;
+ else if (x === x.parent.right) x.parent.right = y;
+ else x.parent.left = y;
+ y.right = x;
+ x.parent = y;
+ };
+
+ const transplant = (u, v) => {
+ if (u.parent === nil) T.root = v;
+ else if (u === u.parent.left) u.parent.left = v;
+ else u.parent.right = v;
+ v.parent = u.parent;
+ };
+
+ const insertFixup = (z, actions) => {
+ while (z.parent.color === 'red') {
+ if (z.parent === z.parent.parent.left) {
+ const y = z.parent.parent.right; // uncle
+ if (y.color === 'red') {
+ z.parent.color = 'black';
+ y.color = 'black';
+ z.parent.parent.color = 'red';
+ z = z.parent.parent;
+ emit(actions, z.id);
+ } else {
+ if (z === z.parent.right) {
+ z = z.parent;
+ leftRotate(z);
+ emit(actions, z.id);
+ }
+ z.parent.color = 'black';
+ z.parent.parent.color = 'red';
+ rightRotate(z.parent.parent);
+ emit(actions, z.parent.id);
+ }
+ } else {
+ const y = z.parent.parent.left; // uncle
+ if (y.color === 'red') {
+ z.parent.color = 'black';
+ y.color = 'black';
+ z.parent.parent.color = 'red';
+ z = z.parent.parent;
+ emit(actions, z.id);
+ } else {
+ if (z === z.parent.left) {
+ z = z.parent;
+ rightRotate(z);
+ emit(actions, z.id);
+ }
+ z.parent.color = 'black';
+ z.parent.parent.color = 'red';
+ leftRotate(z.parent.parent);
+ emit(actions, z.parent.id);
+ }
+ }
+ }
+ if (T.root.color !== 'black') {
+ T.root.color = 'black';
+ emit(actions, T.root.id);
+ }
+ };
+
+ const insert = (value, actions) => {
+ const z = { id: 'rb' + ++counter, value, color: 'red', left: nil, right: nil, parent: nil };
+ let y = nil;
+ let x = T.root;
+ while (x !== nil) {
+ y = x;
+ x = value < x.value ? x.left : x.right;
+ }
+ z.parent = y;
+ if (y === nil) T.root = z;
+ else if (value < y.value) y.left = z;
+ else y.right = z;
+ emit(actions, z.id); // new red node slides in
+ insertFixup(z, actions);
+ };
+
+ const deleteFixup = (x, actions) => {
+ while (x !== T.root && x.color === 'black') {
+ if (x === x.parent.left) {
+ let w = x.parent.right;
+ if (w.color === 'red') {
+ w.color = 'black';
+ x.parent.color = 'red';
+ leftRotate(x.parent);
+ emit(actions, w.id);
+ w = x.parent.right;
+ }
+ if (w.left.color === 'black' && w.right.color === 'black') {
+ w.color = 'red';
+ emit(actions, w.id);
+ x = x.parent;
+ } else {
+ if (w.right.color === 'black') {
+ w.left.color = 'black';
+ w.color = 'red';
+ rightRotate(w);
+ emit(actions, w.id);
+ w = x.parent.right;
+ }
+ w.color = x.parent.color;
+ x.parent.color = 'black';
+ w.right.color = 'black';
+ leftRotate(x.parent);
+ emit(actions, w.id);
+ x = T.root;
+ }
+ } else {
+ let w = x.parent.left;
+ if (w.color === 'red') {
+ w.color = 'black';
+ x.parent.color = 'red';
+ rightRotate(x.parent);
+ emit(actions, w.id);
+ w = x.parent.left;
+ }
+ if (w.right.color === 'black' && w.left.color === 'black') {
+ w.color = 'red';
+ emit(actions, w.id);
+ x = x.parent;
+ } else {
+ if (w.left.color === 'black') {
+ w.right.color = 'black';
+ w.color = 'red';
+ leftRotate(w);
+ emit(actions, w.id);
+ w = x.parent.left;
+ }
+ w.color = x.parent.color;
+ x.parent.color = 'black';
+ w.left.color = 'black';
+ rightRotate(x.parent);
+ emit(actions, w.id);
+ x = T.root;
+ }
+ }
+ }
+ x.color = 'black';
+ emit(actions, x === nil ? null : x.id);
+ };
+
+ const findNode = (value) => {
+ let cur = T.root;
+ while (cur !== nil && cur.value !== value) cur = value < cur.value ? cur.left : cur.right;
+ return cur === nil ? null : cur;
+ };
+
+ const remove = (value, actions) => {
+ let z = findNode(value);
+ if (!z) return false;
+ // two children: copy in-order successor's value into z (id preserved),
+ // then physically remove the successor (which has at most one child)
+ if (z.left !== nil && z.right !== nil) {
+ const s = minimum(z.right);
+ z.value = s.value;
+ emit(actions, z.id); // value copied up
+ z = s;
+ }
+ const child = z.left !== nil ? z.left : z.right;
+ const removedColor = z.color;
+ transplant(z, child);
+ emit(actions, child === nil ? null : child.id);
+ if (removedColor === 'black') deleteFixup(child, actions);
+ return true;
+ };
+
+ return { T, snap, insert, remove, findNode };
+}
+
+export function fromValues(values) {
+ let root = null;
+ for (const v of values) {
+ const ctx = rbContext(root);
+ if (!ctx.findNode(v)) ctx.insert(v, null);
+ root = ctx.snap(ctx.T.root);
+ }
+ return root;
+}
+
+function contains(root, value) {
+ let cur = root;
+ while (cur) {
+ if (value === cur.value) return true;
+ cur = value < cur.value ? cur.left : cur.right;
+ }
+ return false;
+}
+
+function pathTo(root, value) {
+ const path = [];
+ let cur = root;
+ while (cur) {
+ path.push(cur);
+ if (value === cur.value) break;
+ cur = value < cur.value ? cur.left : cur.right;
+ }
+ return path;
+}
+
+export function insertActions(tree, value) {
+ const actions = [];
+ for (const n of pathTo(tree, value)) actions.push({ type: 'markNode', id: n.id, state: 'current' });
+ if (contains(tree, value)) {
+ actions.push({ type: 'status', text: `${value} is already in the tree` });
+ actions.push({ type: 'clear' });
+ return actions;
+ }
+ const ctx = rbContext(tree);
+ ctx.insert(value, actions);
+ actions.push({ type: 'status', text: `Inserted ${value}` });
+ return actions;
+}
+
+export function deleteActions(tree, value) {
+ const actions = [];
+ const path = pathTo(tree, value);
+ for (const n of path) actions.push({ type: 'markNode', id: n.id, state: 'current' });
+ const target = path[path.length - 1];
+ if (!target || target.value !== value) {
+ actions.push({ type: 'status', text: `${value} not found` });
+ actions.push({ type: 'clear' });
+ return actions;
+ }
+ actions.push({ type: 'markNode', id: target.id, state: 'remove' });
+ const ctx = rbContext(tree);
+ ctx.remove(value, actions);
+ actions.push({ type: 'status', text: `Deleted ${value}` });
+ return actions;
+}
+
+// search is the plain BST walk (RB nodes carry id/value/left/right)
+export const searchActions = bstSearchActions;