diff --git a/README.md b/README.md new file mode 100755 index 0000000..97065e3 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# avl +WAVL Tree Visualization using D3JS + +Simply fork repository and run index.html + +![ScreenShot](avl.png) diff --git a/avl.png b/avl.png new file mode 100755 index 0000000..db8641b Binary files /dev/null and b/avl.png differ diff --git a/index.html b/index.html new file mode 100755 index 0000000..2809245 --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + + + + +
+ + +
+
+ + +
+ + + + + + diff --git a/style.css b/style.css new file mode 100755 index 0000000..0034652 --- /dev/null +++ b/style.css @@ -0,0 +1,23 @@ +.node circle { + fill: #ecf0f1; + stroke: #3498db; + stroke-opacity: 0.8; + stroke-width: 2.5px; +} + +.text { + font-family: 'Open Sans', sans-serif; + fill: #3498db; + font-size: 16px; +} + +.link { + fill: none; + stroke: #3498db; + stroke-opacity: 0.8; + stroke-width: 2px; +} + +form { + display: inline; +} diff --git a/tree-creation.js b/tree-creation.js new file mode 100755 index 0000000..8c0b72c --- /dev/null +++ b/tree-creation.js @@ -0,0 +1,541 @@ +var id = 1; + +var rootNode = { + id: id++, + key: null, + rank: -1, + parent: null, + children: [] +}; +var changeRoot = false; + +// Find tree's height +var findHeight = function (node) { + if (node.data === null || !node) return 0; + else { + var left = node.children[0] ? findHeight(node.children[0]) : 0; + var right = node.children[1] ? findHeight(node.children[1]) : 0; + return 1 + ((left > right) ? left : right); + } +}; + +var getHeight = function (node) { + if (node === null) { + return -1; + } + return node.rank; +} + +// Binary Search Tree rotation +var rotateLeft = function (node, callback) { + var parent = node.parent, + leftChild = node.children[0], + rightChild = node.children[1]; + + if (rightChild.children.length === 0) { + rightChild.children.push({ id: id++, key: null, rank: -1, parent: rightChild, children: [] }); + rightChild.children.push({ id: id++, key: null, rank: -1, parent: rightChild, children: [] }); + } + + rightChild.children[0].parent = node; + node.children[1] = rightChild.children[0]; + node.parent = rightChild; + rightChild.children[0] = node; + rightChild.parent = parent; + + if (parent === null) { // Root node + rootNode = rightChild; + + gLinks.selectAll('path').filter(function (d) { // Update root node + return d.data.id === node.parent.id; + }).datum(d3.hierarchy(node).descendants()[0]); + changeRoot = true; + + } else if (node === parent.children[0]) { // Left child of parent + parent.children[0] = rightChild; + changeRoot = false; + + } else if (node === parent.children[1]) { // Right child of parent + parent.children[1] = rightChild; + changeRoot = false; + } + + if (node.children.length !== 0 && node.children[0].key === null && node.children[1].key === null) node.children = []; + + setTimeout(function () { + if (callback instanceof Function) { + callback(); + } + }, duration); + + return rightChild; +}; + +var rotateRight = function (node, callback) { + var parent = node.parent, + leftChild = node.children[0], + rightChild = node.children[1]; + + if (leftChild.children.length === 0) { + leftChild.children.push({ id: id++, key: null, rank: -1, parent: leftChild, children: [] }); + leftChild.children.push({ id: id++, key: null, rank: -1, parent: leftChild, children: [] }); + } + + if (parent === null) { // Root node + leftChild.children[1].parent = node; + node.children[0] = leftChild.children[1]; + + node.parent = leftChild; + leftChild.children[1] = node; + + leftChild.parent = parent; + rootNode = leftChild; + + gLinks.selectAll('path').filter(function (d) { // Update root node's link + return d.data.id === node.parent.id; + }).datum(d3.hierarchy(node).descendants()[0]); + changeRoot = true; + + } else if (node === parent.children[0]) { // Left child of parent + leftChild.children[1].parent = node; + node.children[0] = leftChild.children[1]; + + node.parent = leftChild; + leftChild.children[1] = node; + + leftChild.parent = parent; + parent.children[0] = leftChild; + + changeRoot = false; + + } else if (node === parent.children[1]) { // Left child of parent + leftChild.children[1].parent = node; + node.children[0] = leftChild.children[1]; + + node.parent = leftChild; + leftChild.children[1] = node; + + leftChild.parent = parent; + parent.children[1] = leftChild; + + changeRoot = false; + } + + if (node.children.length !== 0 && node.children[0].key === null && node.children[1].key === null) node.children = []; + + setTimeout(function () { + if (callback instanceof Function) { + callback(); + } + }, duration); + + return leftChild; +}; + +// Node highlight for better visualization +var highlight = function (node) { + var hlNode = gNodes.selectAll('circle').filter(function (d) { + return d.data.id === node.id; + }); + hlNode.transition() + .duration(duration / 3) + .style('stroke', '#e74c3c') + .style('stroke-width', '3.5px'); +}; + +var removeHighlight = function (node) { + var hlNode = gNodes.selectAll('circle').filter(function (d) { + return d.data.id === node.id; + }); + hlNode.transition() + .duration(duration / 3) + .style('stroke', '#3498db') + .style('stroke-width', '2.5px'); +}; + +// AVL Tree balancing +var balance = function (node, callback) { + highlight(node); + var hLeft = node.children[0] ? findHeight(node.children[0]) : 0; + var hRight = node.children[1] ? findHeight(node.children[1]) : 0; + var hl, hr, + defer = 0.5; + if (hLeft - hRight >= 2) { // Left unbalance + var leftChild = node.children[0]; + hl = leftChild.children[0] ? findHeight(leftChild.children[0]) : 0; + hr = leftChild.children[1] ? findHeight(leftChild.children[1]) : 0; + if (hl >= hr) { // Left of left + rotateRight(node, updateTree); + defer = 1; + } else { // Right of left + defer = 3; + rotateLeft(leftChild, function () { + updateTree(); + setTimeout(function () { + rotateRight(node, updateTree); + }, duration); + }); + } + } else if (hRight - hLeft >= 2) { // Right unbalance + rotated = false; + isChanged = true; + var rightChild = node.children[1]; + hl = rightChild.children[0] ? findHeight(rightChild.children[0]) : 0; + hr = rightChild.children[1] ? findHeight(rightChild.children[1]) : 0; + if (hr >= hl) { // Right of right + rotateLeft(node, updateTree); + defer = 1; + } else { // Left of right + defer = 3; + rotateRight(rightChild, function () { + updateTree(); + setTimeout(function () { + rotateLeft(node, updateTree); + }, duration); + }); + } + } + + if (!node.parent) { + node.rank = 69; + } + + setTimeout(function () { + removeHighlight(node); + if (!node.parent) { // End balancing + if (callback instanceof Function) callback(); + } + else balance(node.parent, callback); + }, duration * defer); +}; + +var rank = function (node) { + if (node === null) { + return -1; + } + return node.rank; +}; + +var difference = function (node, parent) { + return rank(parent) - rank(node); +}; + +var differences = function (node) { + const nodeRank = rank(node); + + const leftRank = node ? rank(node.children[0]) : -1; + const rightRank = node ? rank(node.children[1]) : -1; + + return [nodeRank - leftRank, nodeRank - rightRank]; +}; + +var fix0Child = function (x, y, z, rotateL, rotateR) { + var newRoot = x.parent; + + if (!y || difference(y, y.parent) == 2) { + newRoot = rotateR(z, updateTree); + z.rank--; + } else if (difference(y, y.parent) == 1) { + rotateL(x, function () { + updateTree(); + setTimeout(function () { + newRoot = rotateR(z, updateTree); + }, duration); + }); + + y.rank++; + x.rank--; + z.rank--; + } + + return newRoot; +}; + +var insertFixup = function (x, callback) { + highlight(x); + + let [lDiff, rDiff] = differences(x.parent); + + while (x.parent && ((lDiff == 0 && rDiff == 1) || (lDiff == 1 && rDiff == 0))) { + console.log(`Node with key: ${x.parent.key} is ${lDiff},${rDiff}-node`); + x.parent.rank++; + + removeHighlight(x); + x = x.parent; + highlight(x); + + [lDiff, rDiff] = differences(x.parent); + } + + console.log(`Rank of node with key=${x.key}: ${x.rank}`); + console.log(`Parent node is ${lDiff},${rDiff}-node`); + + if (x.parent === null) { + console.log("exiting"); + setTimeout(function () { + removeHighlight(x); + if (callback instanceof Function) { + callback(); + } + }, duration * 3); + return; + } + + const rotatingAroundRoot = x.parent.parent === null; + var newRoot = x.parent; + + var rankDifference = difference(x, x.parent); + console.log(x); + console.log(`Before fix 0-child: ${rankDifference}`); + if (rankDifference != 0) { + setTimeout(function () { + removeHighlight(x); + if (callback instanceof Function) { + callback(); + } + }, duration * 3); + return; + } + + if (rankDifference == 0 && x.parent.children[0] === x) { + console.log("Fixing 0-child on left"); + newRoot = fix0Child( + x, x.children[1], x.parent, rotateLeft, rotateRight + ); + } else if (rankDifference == 0 && x.parent.children[1] === x) { + console.log("Fixing 0-child on right"); + newRoot = fix0Child( + x, x.children[0], x.parent, rotateRight, rotateLeft + ); + } + + // if (rotatingAroundRoot) { + // rootNode = newRoot; + // } + + setTimeout(function () { + removeHighlight(x); + if (callback instanceof Function) { + callback(); + } + }, duration * 3); +}; + +// Tree insertion +var insert = function (n, callback) { + console.log('Insert', n); + if (!n || !Number.isInteger(n)) return; + if (!rootNode.key) { + rootNode.key = n; + rootNode.rank++; + updateTree(); + callback(); + return; + } + + var walker = rootNode, + newNode; + + while (!newNode) { + if (n <= walker.key) { + if (walker.children.length === 0) { // No child + walker.children.push({ id: id++, key: n, rank: 0, parent: walker, children: [] }); // Left child + walker.children.push({ id: id++, key: null, rank: -1, parent: walker, children: [] }); // Empty right child + newNode = walker.children[0]; + } else if (walker.children[0].key === null) { // Already have right child, left child is empty + walker.children[0].key = n; + walker.children[0].rank++; + newNode = walker.children[0]; + } else { // Move left + walker = walker.children[0]; + } + } else { + if (walker.children.length === 0) { // No child + walker.children.push({ id: id++, key: null, rank: -1, parent: walker, children: [] }); // Empty left child + walker.children.push({ id: id++, key: n, rank: 0, parent: walker, children: [] }); // Right child + newNode = walker.children[1]; + } else if (walker.children[1].key === null) { // Already have left child, right child is empty + walker.children[1].key = n; + walker.children[1].rank++; + newNode = walker.children[1]; + } else { // Move left + walker = walker.children[1]; + } + } + } + updateTree(); + setTimeout(function () { + insertFixup(newNode, callback); + //callback(); + }, duration); +}; + +// Tree deletion +var deleteTree = function (n, callback) { + if (!rootNode.key) return false; + var walker = rootNode, + nodeDelete = null, // Node to be deleted + nodeReplace = null, // Node to replace deleted node + nodeBalance = null, // After deleting, perform balance on this node to root + parent; + + // Find node + if (n === walker.key) { // Deleting root + nodeDelete = walker; + if (nodeDelete.children.length === 0) nodeDelete.key = null; // Tree only has root node + else { + if (nodeDelete.children[0].key === null) { // Root does not have left subtree + rootNode = rootNode.children[1]; // Right subtree becomes new tree + rootNode.parent = null; + + gLinks.selectAll('path').filter(function (d) { + return d.data.id === rootNode.id; + }).remove(); + } else { + nodeReplace = nodeDelete.children[0]; + // In-order predecessor, largest child of left subtree + while (nodeReplace) { + if (!nodeReplace.children[1] || !nodeReplace.children[1].key) break; + nodeReplace = nodeReplace.children[1]; + } + + parent = nodeReplace.parent; + nodeBalance = parent; // Will start balacing from this node + + if (parent.children[0] === nodeReplace) { + if (nodeReplace.children[0]) { + nodeReplace.children[0].parent = parent; + parent.children[0] = nodeReplace.children[0] + } else { + parent.children[0] = { id: id++, key: null, parent: parent, children: [] }; + } + } + else if (parent.children[1] === nodeReplace) { + if (nodeReplace.children[0]) { + nodeReplace.children[0].parent = parent; + parent.children[1] = nodeReplace.children[0] + } else { + parent.children[1] = { id: id++, key: null, parent: parent, children: [] }; + } + } + // After moving, if nodeReplace's parent has 2 empty children + if (parent.children.length !== 0 && parent.children[0].key === null && parent.children[1].key === null) parent.children = []; + + // Add 2 subtrees of old root to new root + if (nodeDelete.children[0]) { + nodeDelete.children[0].parent = nodeReplace; + nodeReplace.children[0] = nodeDelete.children[0]; + } + if (nodeDelete.children[1]) { + nodeDelete.children[1].parent = nodeReplace; + nodeReplace.children[1] = nodeDelete.children[1]; + } + + // Replace deleted root node by largest node of left subtree (in-order predecessor) + nodeReplace.parent = null; + rootNode = nodeReplace; + + gLinks.selectAll('path').filter(function (d) { + return d.data.id === nodeReplace.id; + }).remove(); + } + } + + updateTree(); + setTimeout(function () { + if (nodeBalance) balance(nodeBalance, callback); + else if (callback instanceof Function) callback(); + }, duration); + return true; + } + + // Finding node + while (walker.key) { + if (n < walker.key) walker = walker.children[0]; // Move left + else if (n > walker.key) walker = walker.children[1]; // Move right + else if (n === walker.key) { + nodeDelete = walker; + break; + } + } + + if (!nodeDelete) return false; + + // Deletion + if (nodeDelete.children.length === 0) { // Node to be deleted is leaf node + parent = nodeDelete.parent; + nodeBalance = parent; // Will start balacing from this node + + if (parent.children[0] === nodeDelete) { // Remove left child + parent.children[0] = { id: id++, key: null, parent: parent, children: [] }; // Empty child + } + else if (parent.children[1] === nodeDelete) { // Remove right child + parent.children[1] = { id: id++, key: null, parent: parent, children: [] }; + } + + if (parent.children.length !== 0 && parent.children[0].key === null && parent.children[1].key === null) parent.children = []; + + } else { // Node to be deleted is internal node + nodeReplace = nodeDelete.children[0].key ? nodeDelete.children[0] : null; + // In-order predecessor, largest child of left subtree + while (nodeReplace) { + if (!nodeReplace.children[1] || !nodeReplace.children[1].key) break; + nodeReplace = nodeReplace.children[1]; + } + + if (!nodeReplace) { // No left child, right child of nodeDelete replace its position + parent = nodeDelete.parent; + nodeBalance = parent; // Will start balacing from this node + + nodeDelete.children[1].parent = parent; + if (parent.children[0] === nodeDelete) parent.children[0] = nodeDelete.children[1]; // Left child of parent + else if (parent.children[1] === nodeDelete) parent.children[1] = nodeDelete.children[1]; // Right child of parent + } else { + // Update nodeReplace's parent + parent = nodeReplace.parent; + nodeBalance = parent; // Will start balacing from this node + + if (parent.children[0] === nodeReplace) { + if (nodeReplace.children[0]) { + nodeReplace.children[0].parent = parent; + parent.children[0] = nodeReplace.children[0] + } else { + parent.children[0] = { id: id++, key: null, parent: parent, children: [] }; + } + } + else if (parent.children[1] === nodeReplace) { + if (nodeReplace.children[0]) { + nodeReplace.children[0].parent = parent; + parent.children[1] = nodeReplace.children[0] + } else { + parent.children[1] = { id: id++, key: null, parent: parent, children: [] }; + } + } + // After moving, if nodeReplace's parent has 2 empty children + if (parent.children.length !== 0 && parent.children[0].key === null && parent.children[1].key === null) parent.children = []; + + // Replace deleted node by largest node of left subtree (in-order predecessor) + parent = nodeDelete.parent; + nodeReplace.parent = parent; + if (parent.children[0] === nodeDelete) parent.children[0] = nodeReplace; // Left child of parent + else if (parent.children[1] === nodeDelete) parent.children[1] = nodeReplace; // Right child of parent + + if (nodeDelete.children[0]) { + nodeDelete.children[0].parent = nodeReplace; + nodeReplace.children[0] = nodeDelete.children[0]; + } + if (nodeDelete.children[1]) { + nodeDelete.children[1].parent = nodeReplace; + nodeReplace.children[1] = nodeDelete.children[1]; + } + } + } + + updateTree(); + setTimeout(function () { + if (nodeBalance) balance(nodeBalance, callback); + else if (callback instanceof Function) callback(); + }, duration); + return true; +}; + diff --git a/tree-visualization.js b/tree-visualization.js new file mode 100755 index 0000000..cb41523 --- /dev/null +++ b/tree-visualization.js @@ -0,0 +1,159 @@ +/*************Binary Search Tree Visualization using D3JS *************/ + +const NODE_SIZE = 40; + +var duration = 400; + +var tree = d3.tree().separation(function () { return 3 * NODE_SIZE; }); + +var svg = d3.select('svg'), + g = svg.append('g').attr('transform', `translate(${3 * NODE_SIZE},${3 * NODE_SIZE})`); + +var gLinks = g.append('g'), + gNodes = g.append('g'); + +svg.attr('width', '1920') + .attr('height', '1080'); + +var oldPos = {}; +var updateTree = function () { + var root = d3.hierarchy(rootNode); + + var newTreeSize = [root.descendants().length * 3 * NODE_SIZE, ((root.height + 1) * 2 - 1) * 30]; + + if (tree.size()[0] !== newTreeSize[0] || tree.size()[1] !== newTreeSize[1]) { + tree.size(newTreeSize); + center = pos = svg.attr('width') / 2 - tree.size()[0] / 2; + } + tree(root); + + var nodes = root.descendants().filter(function (d) { + return d.data.key !== null; + }); + + var link = gLinks.selectAll('path') + .data(nodes, function (d) { return d.data.id; }); + + link.exit().remove(); + + link.transition() // Update new position of old links + .duration(duration) + .attrTween('d', function (d) { + + var oldDraw = d3.select(this).attr('d'); + if (oldDraw) { + oldDraw = oldDraw.match(/(M.*)(L.*)/); + var oldMoveto = oldMoveto = oldDraw[1].slice(1).split(',').map(Number), + oldLineto = oldDraw[2].slice(1).split(',').map(Number); + // If root is changed, reverse to correctly animated if rotate left + if (changeRoot && oldMoveto[1] === 0) { // Old root node + oldMoveto = oldDraw[2].slice(1).split(',').map(Number); + oldLineto = oldDraw[1].slice(1).split(',').map(Number); + changeRoot = false; + } + + if ((oldLineto !== [d.x, d.y]) && (oldMoveto !== [d.parent.x, d.parent.y])) { + /*console.log(d.data.key, oldMoveto, oldLineto); + console.log(d.data.key, [d.parent.x, d.parent.y], [d.x, d.y]);*/ + var interpolatorMX = d3.interpolateNumber(oldMoveto[0], d.parent.x); + var interpolatorMY = d3.interpolateNumber(oldMoveto[1], d.parent.y); + var interpolatorLX = d3.interpolateNumber(oldLineto[0], d.x); + var interpolatorLY = d3.interpolateNumber(oldLineto[1], d.y); + + return function (t) { + return 'M' + interpolatorMX(t) + ',' + interpolatorMY(t) + 'L' + interpolatorLX(t) + ',' + interpolatorLY(t); + }; + } + } + }); + + link.enter().append('path') // Add new element for new data + .attr('class', 'link') + .transition() + .duration(duration) + .attrTween('d', function (d) { + if (d.parent) { + var parentOldPos = oldPos[d.parent.data.id.toString()]; + var interpolatorMX = d3.interpolateNumber(parentOldPos[0], d.parent.x); + var interpolatorMY = d3.interpolateNumber(parentOldPos[1], d.parent.y); + var interpolatorLX = d3.interpolateNumber(parentOldPos[0], d.x); + var interpolatorLY = d3.interpolateNumber(parentOldPos[1], d.y); + + return function (t) { + return 'M' + interpolatorMX(t) + ',' + interpolatorMY(t) + 'L' + interpolatorLX(t) + ',' + interpolatorLY(t); + }; + } + else { + d3.select(this).remove(); + } + }); + + var node = gNodes.selectAll('g') + .data(nodes, function (d) { return d.data.id; }); + + node.exit().remove(); + + node.transition() + .duration(duration) + .attr('transform', function (d) { + setTimeout(function () { // Finish transition, update old position of this node + oldPos[d.data.id.toString()] = [d.x, d.y]; + }, duration); + return 'translate(' + d.x + ',' + d.y + ')'; + }); + + var newNode = node.enter().append('g') + .attr('transform', function (d) { + if (!d.parent) return 'translate(' + d.x + ',' + (d.y - 30) + ')'; + else return 'translate(' + oldPos[d.parent.data.id.toString()][0] + ',' + (oldPos[d.parent.data.id.toString()][1] - 30) + ')'; + }) + .attr('class', 'node'); + + newNode.transition() + .duration(duration) + .attr('transform', function (d) { + oldPos[d.data.id.toString()] = [d.x, d.y]; + return 'translate(' + d.x + ',' + d.y + ')'; + }); + + newNode.append('circle') + .attr('r', NODE_SIZE); + + newNode.append('text') + .attr('class', 'text') + .attr('text-anchor', 'middle') + .attr('dy', 5) + .text(function (d) { return `${d.data.key} (r=${d.data.rank})`; }); +}; + +var handleInsert = function (event) { + var num = document.getElementById('insertInput').value; + if (num) { + document.getElementById('insertInput').value = ''; + d3.selectAll('#insertTree input').each(function () { // Disable insert + d3.select(this).attr('disabled', '') + }); + insert(parseInt(num), function () { + d3.selectAll('#insertTree input').each(function () { // Enable insert + d3.select(this).attr('disabled', null); + }); + }); + } + return false; +}; + +var handleDelete = function (event) { + var num = document.getElementById('deleteInput').value; + if (num && rootNode.key !== null) { // Tree is not empty + document.getElementById('deleteInput').value = ''; + d3.selectAll('#deleteTree input').each(function () { // Disable insert + d3.select(this).attr('disabled', '') + }); + deleteTree(parseInt(num), function () { + d3.selectAll('#deleteTree input').each(function () { // Enable insert + d3.select(this).attr('disabled', null); + }); + }); + } + return false; +}; \ No newline at end of file