Skip to content

Commit 7231650

Browse files
author
TamimEhsan
committed
feat: Add a Binary Heap visualizer
New /binary-heap segment on the reusable tree component: a min-heap with insert (sift-up), extract-min (sift-down), build-heap from a typed list (Floyd O(n) heapify), and heapsort (descending, since it's a min-heap). src/lib/algorithms/heap.js keeps the heap as an array of {id,value} and rebuilds a complete binary tree per setTree; sifts swap entries with their ids so nodes slide past each other on the rAF tween. Reuses binaryLayout and the existing node states (amber = active pair, green = sorted). Adds a heap-specific menu and a home card (placeholder image).
1 parent 20fac04 commit 7231650

5 files changed

Lines changed: 282 additions & 0 deletions

File tree

public/images/binary-heap.png

279 KB
Loading

src/app/binary-heap/menu.jsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useState } from 'react';
2+
import { CustomSlider } from '@/components/custom-slider';
3+
import { Button } from '@/components/ui/button';
4+
import { Plus, ArrowUpFromLine, Wand2, ArrowDownWideNarrow, RotateCcw } from 'lucide-react';
5+
6+
// Sidebar for the binary heap (min-heap): insert a value, extract the min,
7+
// build a heap from a typed list, and run heapsort.
8+
9+
export default function HeapMenu({ disabled, onInsert, onExtract, onBuild, onHeapsort, onClear, onSpeedChange }) {
10+
const [value, setValue] = useState('');
11+
const [list, setList] = useState('5, 3, 8, 1, 9, 2, 7');
12+
13+
const num = () => Number(value);
14+
const valid = value.trim() !== '' && Number.isFinite(num());
15+
const parsedList = () => list.split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n));
16+
17+
return (
18+
<div className="w-64 bg-gray-100 p-4 space-y-6 overflow-auto">
19+
<h2 className="text-lg font-semibold">Binary Heap (min)</h2>
20+
21+
<div className="space-y-3">
22+
<div className="flex items-center gap-2">
23+
<div className="h-px flex-1 bg-gray-300" />
24+
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Operation</span>
25+
<div className="h-px flex-1 bg-gray-300" />
26+
</div>
27+
<div className="space-y-2">
28+
<label className="text-sm font-medium whitespace-nowrap">Value</label>
29+
<input
30+
type="number"
31+
value={value}
32+
onChange={(e) => setValue(e.target.value)}
33+
disabled={disabled}
34+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
35+
/>
36+
</div>
37+
<div className="flex gap-2">
38+
<Button className="flex-1" onClick={() => valid && onInsert(num())} disabled={disabled || !valid}>
39+
<Plus /> Insert
40+
</Button>
41+
<Button className="flex-1" variant="outline" onClick={onExtract} disabled={disabled}>
42+
<ArrowUpFromLine /> Extract
43+
</Button>
44+
</div>
45+
</div>
46+
47+
<div className="space-y-3">
48+
<div className="flex items-center gap-2">
49+
<div className="h-px flex-1 bg-gray-300" />
50+
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Build</span>
51+
<div className="h-px flex-1 bg-gray-300" />
52+
</div>
53+
<div className="space-y-2">
54+
<label className="text-sm font-medium whitespace-nowrap">List</label>
55+
<input
56+
type="text"
57+
value={list}
58+
onChange={(e) => setList(e.target.value)}
59+
disabled={disabled}
60+
placeholder="e.g. 5, 3, 8, 1"
61+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
62+
/>
63+
</div>
64+
<Button className="w-full" variant="outline" onClick={() => onBuild(parsedList())} disabled={disabled || parsedList().length === 0}>
65+
<Wand2 /> Build heap
66+
</Button>
67+
</div>
68+
69+
<div className="space-y-3">
70+
<div className="flex items-center gap-2">
71+
<div className="h-px flex-1 bg-gray-300" />
72+
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</span>
73+
<div className="h-px flex-1 bg-gray-300" />
74+
</div>
75+
<Button className="w-full" variant="outline" onClick={onHeapsort} disabled={disabled}>
76+
<ArrowDownWideNarrow /> Heapsort
77+
</Button>
78+
<p className="text-xs text-gray-500">Min-heap heapsort sorts in descending order.</p>
79+
<CustomSlider title="Speed" defaultValue={50} min={10} max={100} step={1} onChange={onSpeedChange} />
80+
<Button className="w-full" variant="outline" onClick={onClear} disabled={disabled}>
81+
<RotateCcw /> Clear
82+
</Button>
83+
</div>
84+
</div>
85+
);
86+
}

src/app/binary-heap/page.jsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
3+
import { useState } from 'react';
4+
import Navbar from '@/components/navbar';
5+
import { fromValues, insertActions, extractActions, buildActions, heapsortActions } from '@/lib/algorithms/heap';
6+
import { binaryLayout } from '@/components/tree/layout';
7+
import { useTreeEditor } from '@/components/tree/use-tree-editor';
8+
import TreeCanvas from '@/components/tree/tree-canvas';
9+
import HeapMenu from './menu';
10+
11+
export default function BinaryHeap() {
12+
const [initialTree] = useState(() => fromValues([50, 30, 70, 20, 40, 60, 80]));
13+
const g = useTreeEditor({ initialTree });
14+
15+
return (
16+
<div className="flex flex-col h-screen">
17+
<Navbar />
18+
<div className="flex flex-1 overflow-hidden">
19+
<HeapMenu
20+
disabled={g.isRunning}
21+
onInsert={(v) => g.run(insertActions(g.getContext().tree, v))}
22+
onExtract={() => g.run(extractActions(g.getContext().tree))}
23+
onBuild={(values) => g.run(buildActions(values))}
24+
onHeapsort={() => g.run(heapsortActions(g.getContext().tree))}
25+
onClear={g.clear}
26+
onSpeedChange={g.setSpeed}
27+
/>
28+
<div className="relative flex-1 p-6">
29+
{g.status && (
30+
<div className="absolute top-3 left-3 z-10 rounded-md bg-slate-800 px-3 py-1.5 text-xs font-semibold text-white shadow">
31+
{g.status}
32+
</div>
33+
)}
34+
<TreeCanvas
35+
tree={g.tree}
36+
layout={binaryLayout}
37+
nodeState={g.nodeState}
38+
edgeState={g.edgeState}
39+
labels={g.labels}
40+
/>
41+
</div>
42+
</div>
43+
</div>
44+
);
45+
}

src/app/components/algorithm-cards.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ const algorithms = [
4040
title: 'Binary Search Trees',
4141
description: "Insert, delete, and search on a plain BST or a Red-Black tree, with animated rotations and recoloring",
4242
image: '/AlgorithmVisualizer/images/bst.png?height=200&width=300'
43+
},{
44+
id: 'binary-heap',
45+
title: 'Binary Heap',
46+
description: "Min-heap: insert, extract-min, build-heap from a list, and heapsort with animated sift up and down",
47+
image: '/AlgorithmVisualizer/images/binary-heap.png?height=200&width=300'
4348
},
4449
{
4550
id: 'recursion-tree',

src/lib/algorithms/heap.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Binary Heap (min-heap) — action-log planners for insert / extract-min /
2+
// build-heap / heapsort, rendered on the reusable tree component.
3+
//
4+
// A heap is a complete binary tree stored as an array: node i's children are
5+
// 2i+1 and 2i+2. We keep the heap as an array of { id, value } and rebuild a
6+
// logical tree from it for each `setTree`. A sift swaps two array entries
7+
// *including their ids*, so the two nodes' target positions swap and the canvas
8+
// tween slides them past each other.
9+
10+
let counter = 0;
11+
const mkNode = (value) => ({ id: 'h' + ++counter, value });
12+
13+
// array of { id, value } -> logical tree { id, value, left, right }
14+
export function buildTree(arr) {
15+
const make = (i) => {
16+
if (i >= arr.length) return null;
17+
return { id: arr[i].id, value: arr[i].value, left: make(2 * i + 1), right: make(2 * i + 2) };
18+
};
19+
return make(0);
20+
}
21+
22+
// logical complete tree -> level-order array of { id, value }
23+
export function toArray(tree) {
24+
const out = [];
25+
const q = tree ? [tree] : [];
26+
while (q.length) {
27+
const n = q.shift();
28+
out.push({ id: n.id, value: n.value });
29+
if (n.left) q.push(n.left);
30+
if (n.right) q.push(n.right);
31+
}
32+
return out;
33+
}
34+
35+
export function fromValues(values) {
36+
const a = values.map(mkNode);
37+
heapify(a);
38+
return buildTree(a);
39+
}
40+
41+
// in-place Floyd build-heap (no animation) — used for seeding
42+
function heapify(a) {
43+
for (let i = Math.floor(a.length / 2) - 1; i >= 0; i--) siftDown(a, i, a.length);
44+
}
45+
46+
function siftDown(a, i, size) {
47+
for (;;) {
48+
let small = i;
49+
const l = 2 * i + 1;
50+
const r = 2 * i + 2;
51+
if (l < size && a[l].value < a[small].value) small = l;
52+
if (r < size && a[r].value < a[small].value) small = r;
53+
if (small === i) break;
54+
[a[i], a[small]] = [a[small], a[i]];
55+
i = small;
56+
}
57+
}
58+
59+
const snap = (actions, a) => actions.push({ type: 'setTree', tree: buildTree(a) });
60+
const mark = (actions, a, idxs, state) => {
61+
for (const i of idxs) if (a[i]) actions.push({ type: 'markNode', id: a[i].id, state });
62+
};
63+
64+
export function insertActions(tree, value) {
65+
const actions = [];
66+
const a = toArray(tree);
67+
a.push(mkNode(value));
68+
snap(actions, a); // appears at the end
69+
let i = a.length - 1;
70+
while (i > 0) {
71+
const p = Math.floor((i - 1) / 2);
72+
mark(actions, a, [i, p], 'current');
73+
if (a[i].value >= a[p].value) break;
74+
[a[i], a[p]] = [a[p], a[i]]; // ids travel with values
75+
snap(actions, a);
76+
i = p;
77+
}
78+
actions.push({ type: 'status', text: `Inserted ${value}` });
79+
return actions;
80+
}
81+
82+
export function extractActions(tree) {
83+
const actions = [];
84+
const a = toArray(tree);
85+
if (!a.length) {
86+
actions.push({ type: 'status', text: 'Heap is empty' });
87+
actions.push({ type: 'clear' });
88+
return actions;
89+
}
90+
const min = a[0].value;
91+
mark(actions, a, [0], 'current');
92+
const last = a.length - 1;
93+
if (last > 0) {
94+
[a[0], a[last]] = [a[last], a[0]];
95+
snap(actions, a); // min swapped to the end
96+
}
97+
a.pop(); // remove the min
98+
snap(actions, a);
99+
siftDownActions(actions, a, 0, a.length);
100+
actions.push({ type: 'status', text: `Extracted min ${min}` });
101+
return actions;
102+
}
103+
104+
// animated sift-down: emit a snapshot per swap within a[0..size)
105+
function siftDownActions(actions, a, i, size) {
106+
for (;;) {
107+
let small = i;
108+
const l = 2 * i + 1;
109+
const r = 2 * i + 2;
110+
if (l < size && a[l].value < a[small].value) small = l;
111+
if (r < size && a[r].value < a[small].value) small = r;
112+
mark(actions, a, [i, l, r].filter((k) => k < size), 'current');
113+
if (small === i) break;
114+
[a[i], a[small]] = [a[small], a[i]];
115+
snap(actions, a); // render the full array; `size` only bounds the sift
116+
i = small;
117+
}
118+
}
119+
120+
export function buildActions(values) {
121+
const actions = [];
122+
const a = values.map(mkNode);
123+
snap(actions, a); // raw, unheapified
124+
for (let i = Math.floor(a.length / 2) - 1; i >= 0; i--) {
125+
siftDownActions(actions, a, i, a.length);
126+
}
127+
actions.push({ type: 'status', text: 'Built min-heap' });
128+
return actions;
129+
}
130+
131+
export function heapsortActions(tree) {
132+
const actions = [];
133+
const a = toArray(tree);
134+
let size = a.length;
135+
while (size > 1) {
136+
const last = size - 1;
137+
[a[0], a[last]] = [a[last], a[0]];
138+
snap(actions, a); // current min parked at the end of the active region
139+
actions.push({ type: 'markNode', id: a[last].id, state: 'found' }); // sorted
140+
size--;
141+
siftDownActions(actions, a, 0, size);
142+
}
143+
if (a.length) actions.push({ type: 'markNode', id: a[0].id, state: 'found' });
144+
actions.push({ type: 'status', text: 'Heapsort complete (descending)' });
145+
return actions;
146+
}

0 commit comments

Comments
 (0)