Add nodes to the tree and observe how the red-black tree properties are maintained through rotations and color changes.
A red-black tree is a self-balancing binary search tree with the following properties:
Rotations are used to maintain tree balance:
When inserting a new node:
When deleting a node:
class Node {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
this.parent = null;
this.color = 'RED'; // New nodes are always red
}
}
class RedBlackTree {
constructor() {
this.root = null;
this.NIL = { color: 'BLACK' }; // NIL nodes are always black
}
// Insert a new node
insert(value) {
const newNode = new Node(value);
newNode.left = this.NIL;
newNode.right = this.NIL;
// Regular BST insertion
if (!this.root) {
this.root = newNode;
newNode.color = 'BLACK'; // Root must be black
return newNode;
}
let current = this.root;
let parent = null;
while (current !== this.NIL) {
parent = current;
if (value < current.value) {
current = current.left;
} else if (value > current.value) {
current = current.right;
} else {
// Value already exists
return null;
}
}
newNode.parent = parent;
if (value < parent.value) {
parent.left = newNode;
} else {
parent.right = newNode;
}
// Fix Red-Black properties
this.fixInsert(newNode);
return newNode;
}
// Fix Red-Black tree properties after insertion
fixInsert(node) {
while (node !== this.root && node.parent && node.parent.color === 'RED') {
let uncle;
if (node.parent === node.parent.parent.left) {
uncle = node.parent.parent.right;
if (uncle && uncle.color === 'RED') {
// Case 1: Uncle is red
node.parent.color = 'BLACK';
uncle.color = 'BLACK';
node.parent.parent.color = 'RED';
node = node.parent.parent;
} else {
if (node === node.parent.right) {
// Case 2: Node is right child
node = node.parent;
this.leftRotate(node);
}
// Case 3: Node is left child
node.parent.color = 'BLACK';
node.parent.parent.color = 'RED';
this.rightRotate(node.parent.parent);
}
} else {
uncle = node.parent.parent.left;
if (uncle && uncle.color === 'RED') {
// Case 1: Uncle is red
node.parent.color = 'BLACK';
uncle.color = 'BLACK';
node.parent.parent.color = 'RED';
node = node.parent.parent;
} else {
if (node === node.parent.left) {
// Case 2: Node is left child
node = node.parent;
this.rightRotate(node);
}
// Case 3: Node is right child
node.parent.color = 'BLACK';
node.parent.parent.color = 'RED';
this.leftRotate(node.parent.parent);
}
}
}
this.root.color = 'BLACK';
}
// Left rotation
leftRotate(x) {
const y = x.right;
x.right = y.left;
if (y.left !== this.NIL) {
y.left.parent = x;
}
y.parent = x.parent;
if (!x.parent) {
this.root = y;
} else if (x === x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
y.left = x;
x.parent = y;
}
// Right rotation
rightRotate(y) {
const x = y.left;
y.left = x.right;
if (x.right !== this.NIL) {
x.right.parent = y;
}
x.parent = y.parent;
if (!y.parent) {
this.root = x;
} else if (y === y.parent.right) {
y.parent.right = x;
} else {
y.parent.left = x;
}
x.right = y;
y.parent = x;
}
// Delete a node (simplified)
delete(value) {
// TODO: Implement delete operation
return false;
}
}
// Game logic
let tree = new RedBlackTree();
let manualMode = false;
let challengeMode = false;
let challengeTarget = null;
let errorCount = 0;
function addNode() {
const value = parseInt(document.getElementById('nodeValue').value);
if (isNaN(value)) return;
try {
const node = tree.insert(value);
if (node) {
logMessage(`Inserted node ${value}`, 'success');
updateVisualization();
} else {
logMessage(`Value ${value} already exists in the tree`, 'error');
}
} catch (err) {
logMessage(`Error inserting node: ${err.message}`, 'error');
}
}
function deleteNode() {
const value = parseInt(document.getElementById('nodeValue').value);
if (isNaN(value)) return;
if (tree.delete(value)) {
logMessage(`Deleted node ${value}`, 'success');
updateVisualization();
} else {
logMessage(`Value ${value} not found in the tree`, 'error');
}
}
function resetTree() {
tree = new RedBlackTree();
logMessage('Tree reset', 'info');
updateVisualization();
}
function startChallenge() {
challengeMode = true;
errorCount = 0;
// Generate a target challenge (e.g., insert nodes to create a specific structure)
challengeTarget = generateChallenge();
logMessage('Challenge mode activated! Try to match the target structure.', 'info');
}
function toggleManualMode() {
manualMode = !manualMode;
logMessage(`Manual mode ${manualMode ? 'enabled' : 'disabled'}`, 'info');
}
function openTab(evt, tabName) {
const tabcontent = document.getElementsByClassName('tab-content');
for (let i = 0; i < tabcontent.length; i++) {
tabcontent[i].classList.remove('active');
}
const tablinks = document.getElementsByClassName('tab');
for (let i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove('active');
}
document.getElementById(tabName).classList.add('active');
evt.currentTarget.classList.add('active');
}
// Visualization using D3.js
const svgWidth = 1000;
const svgHeight = 500;
const nodeRadius = 20;
const svg = d3
.select('#tree-display')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
let tooltip = d3.select('body').append('div')
.attr('class', 'tooltip');
function updateVisualization() {
svg.selectAll('*').remove();
if (!tree.root) {
svg
.append('text')
.attr('x', svgWidth / 2)
.attr('y', svgHeight / 2)
.attr('text-anchor', 'middle')
.text('Tree is empty');
return;
}
const treeLayout = d3.tree().size([svgWidth - 100, svgHeight - 100]);
const root = d3.hierarchy(tree.root, (d) => [d.left !== tree.NIL ? d.left : null, d.right !== tree.NIL ? d.right : null].filter(Boolean));
const treeData = treeLayout(root);
// Draw links
svg
.selectAll('.link')
.data(treeData.links())
.enter()
.append('path')
.attr('class', 'link')
.attr('d', d3.linkHorizontal()
.x((d) => d.x + 50)
.y((d) => d.y + 50)
);
// Draw nodes
const nodes = svg
.selectAll('.node')
.data(treeData.descendants())
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', (d) => `translate(${d.x + 50}, ${d.y + 50})`);
nodes
.append('circle')
.attr('r', nodeRadius)
.style('stroke', (d) => (d.data.color === 'RED' ? 'red' : 'black'))
.style('fill', (d) => (d.data.color === 'RED' ? '#ffcccc' : '#dddddd'))
.on('mouseover', function (event, d) {
tooltip.transition()
.duration(200)
.style('opacity', .9);
tooltip.html(`Value: ${d.data.value}
Color: ${d.data.color}`)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 28) + 'px');
})
.on('mouseout', function () {
tooltip.transition()
.duration(500)
.style('opacity', 0);
});
nodes
.append('text')
.attr('dy', 4)
.text((d) => d.data.value);
}
// Log messages
function logMessage(message, type = 'info') {
const log = document.getElementById('message-log');
const msg = document.createElement('div');
msg.className = `message ${type}`;
msg.textContent = message;
log.appendChild(msg);
log.scrollTop = log.scrollHeight;
}
function generateChallenge() {
// Simple challenge: create a balanced tree with specific values
const values = [50, 25, 75, 12, 37, 62, 87];
resetTree();
values.forEach(v => tree.insert(v));
updateVisualization();
return values;
}
// Initial setup
updateVisualization();
// Tabs setup
function openTab(evt, tabName) {
const tabcontent = document.getElementsByClassName('tab-content');
for (let i = 0; i < tabcontent.length; i++) {
tabcontent[i].classList.remove('active');
}
const tablinks = document.getElementsByClassName('tab');
for (let i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove('active');
}
document.getElementById(tabName).classList.add('active');
evt.currentTarget.classList.add('active');
}