Show code
```html
<!DOCTYPE html>
<html><head><title>BST Animation</title><style>body{display:flex;justify-content:center;align-items:center;margin:0;}canvas{border:1px solid #ccc;background:#f9f9f9;}</style></head><script>const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
let tree = null;
let insertionQueue = [];
let state = 'INIT'; // INIT, INSERTING, SEARCHING, PAUSED
let animationFrameId = null;
let nodeCountDisplay = document.createElement('div');
nodeCountDisplay.style.position = 'absolute';
nodeCountDisplay.style.top = '10px';
nodeCountDisplay.style.left = '10px';
document.body.appendChild(nodeCountDisplay);
const MAX_NODES = 25;
let randomValues = [];
function Node(val, parent) {
this.val = val;
this.parent = parent;
this.left = null;
this.right = null;
}
function initializeTree() {
tree = null;
insertionQueue = [];
randomValues = Array.from({length: Math.floor(Math.random() * 10) + 15}, () => Math.floor(Math.random() * 100) + 1);
nodeCountDisplay.textContent = `Nodes: 0 / ${MAX_NODES}`;
state = 'INIT';
}
function insertNode(val, root) {
const newNode = new Node(val, null);
if (!root) return newNode;
let current = root;
let parent = null;
while (current) {
parent = current;
if (val < current.val) {
current = current.left;
newNode.parent = current ? current : parent; // Attempt to set correct parent, though BST structure handles this naturally later
} else if (val > current.val) {
current = current.right;
newNode.parent = current ? current : parent;
} else {
return root; // Duplicate found (shouldn't happen with random set logic)
}
}
if (val < parent.val && parent.left === null) {
parent.left = newNode;
} else if (val > parent.val && parent.right === null) {
parent.right = newNode;
}
return root;
}
function buildTree(values) {
let root = null;
for (const val of values) {
if (!root) {
root = new Node(val, null);
} else {
root = insertNode(val, root);
}
}
return root;
}
function drawTree(r) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const START_X = 400;
const LEVEL_HEIGHT = 80;
const NODE_WIDTH = 60;
const NODE_HEIGHT = 30;
if (!r) return;
let queue = [{node: r, level: 1, xOffset: START_X}];
let nodesAtLevel = {};
let maxNodes = Math.pow(2, Math.floor((canvas.height - NODE_HEIGHT) / LEVEL_HEIGHT));
// Calculate required X spacing (simple breadth-first placement attempt)
function mapNode(node, level, xStart, xSpacing) {
const x = xStart + (xSpacing * 2) * Math.pow(0.85, level - 1);
const y = LEVEL_HEIGHT * (level - 1) + 60;
return { x: x, y: y };
}
// Simple recursive layout calculation for positioning the drawn nodes
function calculatePositions(node, level, parentPos) {
if (!node) return null;
let pos = mapNode(node, level, START_X - (Math.pow(2, Math.min(level-1, 8)) * 50), 4);
const leftChildPos = calculatePositions(node.left, level + 1, pos);
const rightChildPos = calculatePositions(node.right, level + 1, pos);
return { node: node, position: pos, leftPos: leftChildPos, rightPos: rightChildPos };
}
// The complexity of positioning requires simplifying the layout for performance/brevity
// We will use a fixed column placement based on depth and relative index.
const positions = [];
function collectPositions(node, level) {
if (!node) return;
let x = START_X - (Math.pow(2, Math.min(level-1, 8)) * 40);
let y = LEVEL_HEIGHT * (level - 1) + 60;
positions.push({ node: node, x: x, y: y });
collectPositions(node.left, level + 1);
collectPositions(node.right, level + 1);
}
collectPositions(r, 1);
// Draw Edges and Nodes based on calculated positions
ctx.lineWidth = 2;
for (let i = 0; i < positions.length - 1; i++) {
const p1 = positions[i];
if (!p1.node || !p1.node.parent) continue;
// Find the parent position relative to the child's position
const parentPosIndex = positions.findIndex(p => p.node === p1.node.parent);
if (parentPosIndex !== -1) {
ctx.beginPath();
ctx.moveTo(positions[parentPosIndex].x, positions[parentPosIndex].y + 20);
ctx.lineTo(p1.x, p1.y - 20);
ctx.strokeStyle = '#333';
ctx.stroke();
}
}
// Draw Nodes (must be done after edges)
for (const p of positions) {
const { node, x, y } = p;
ctx.beginPath();
ctx.arc(x, y, 20, 0, Math.PI * 2);
ctx.fillStyle = '#6aa';
ctx.fill();
ctx.strokeStyle = '#333';
ctx.stroke();
ctx.fillStyle = 'black';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(node.val, x, y);
}
}
function handleInsertion() {
if (insertionQueue.length === 0) {
state = 'SEARCHING_SETUP';
return;
}
const val = insertionQueue.shift();
let newRoot = tree ? insertNode(val, tree) : new Node(val, null);
tree = newRoot;
drawTree(tree);
nodeCountDisplay.textContent = `Nodes: ${insertionQueue.length + 1} / ${MAX_NODES}`;
setTimeout(() => {
state = 'INIT'; // Transition state for next draw cycle
animationFrameId = requestAnimationFrame(loop);
}, 500);
}
function handleSearchSetup() {
const values = Array.from({length: Math.floor(Math.random() * 10) + 15}, () => Math.floor(Math.random() * 100) + 1);
const root = buildTree(values);
tree = root;
// Pick a random existing value that is not the root, if possible
let targetVal = values[Math.floor(Math.random() * values.length)];
if (targetVal === root.val) {
targetVal = values[(Math.floor(Math.random() * (values.length - 1)) + 1) % (values.length - 1)];
}
animateSearch(root, targetVal);
}
function animateSearch(node, target) {
let pathNodes = [];
let current = node;
let stepDelay = 200;
function searchWalk(currentNode, targetValue) {
if (!currentNode) return false;
pathNodes.push(currentNode);
const comparison = (targetValue < currentNode.val) ? 'L' : (targetValue > currentNode.val) ? 'R' : null;
if (comparison === null) {
return true; // Found
} else if (comparison === 'L') {
current = currentNode.left;
} else {
current = currentNode.right;
}
return searchWalk(current, targetValue);
}
searchWalk(node, target);
let step = 0;
function searchStep() {
if (step >= pathNodes.length) {
// Search finished
state = 'FINISHED';
setTimeout(() => {
alert(`Search complete! Target ${target} was found/missed.`);
handleSearchSetup(); // Restart loop
}, 1000);
return;
}
const nodeToHighlight = pathNodes[step];
// Redraw the tree with highlights
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawTreeWithHighlights(nodeToHighlight, step === pathNodes.length - 1);
setTimeout(() => {
state = 'PAUSED'; // Indicate waiting phase
step++;
animationFrameId = requestAnimationFrame(searchStep);
}, stepDelay);
}
// Run the search animation loop
animationFrameId = requestAnimationFrame(searchStep);
}
function drawTreeWithHighlights(highlightNode, isTarget) {
const START_X = 400;
const LEVEL_HEIGHT = 80;
const NODE_WIDTH = 60;
const NODE_HEIGHT = 30;
// --- Simplified Layout Calculation (Re-using the structure from drawTree for simplicity) ---
const positions = [];
function collectPositions(node, level) {
if (!node) return;
let x = START_X - (Math.pow(2, Math.min(level-1, 8)) * 40);
let y = LEVEL_HEIGHT * (level - 1) + 60;
positions.push({ node: node, x: x, y: y });
collectPositions(node.left, level + 1);
collectPositions(node.right, level + 1);
}
collectPositions(tree, 1);
// Draw Edges and Nodes (Standard drawing)
ctx.lineWidth = 2;
for (let i = 0; i < positions.length - 1; i++) {
const p1 = positions[i];
if (!p1.node || !p1.node.parent) continue;
const parentPosIndex = positions.findIndex(p => p.node === p1.node.parent);
if (parentPosIndex !== -1) {
ctx.beginPath();
ctx.moveTo(positions[parentPosIndex].x, positions[parentPosIndex].y + 20);
ctx.lineTo(p1.x, p1.y - 20);
ctx.strokeStyle = '#333';
ctx.stroke();
}
}
// Draw Nodes with highlights
for (const p of positions) {
const { node, x, y } = p;
let fillStyle = '#6aa';
let strokeStyle = '#333';
let radius = 20;
if (node === highlightNode) {
fillStyle = isTarget ? '#aaa' : '#ffcc00'; // Target or Active Search Node
strokeStyle = '#ff9900';
} else if (p.node === tree && p !== positions[0]) {
// Highlight root on search start
if(typeof highlightNode === 'undefined') fillStyle = '#6aa';
}
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.strokeStyle = strokeStyle;
ctx.stroke();
ctx.fillStyle = 'black';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(node.val, x, y);
}
}
function loop() {
if (state === 'INIT') {
// Check if insertion is ready to proceed
handleInsertion();
} else if (state === 'SEARCHING_SETUP' || state === 'PAUSED') {
// Wait for the search animation to finish its cycle/timeout
animationFrameId = requestAnimationFrame(loop);
}
}
// --- Initialization and Startup ---
initializeTree();
handleSearchSetup(); // Start immediately with the first full sequence (Build -> Search)
requestAnimationFrame(loop);
</script></html>
```