diff --git a/app/engine/utils/BinarySearchTree.js b/app/engine/utils/BinarySearchTree.js new file mode 100644 index 0000000..7a181df --- /dev/null +++ b/app/engine/utils/BinarySearchTree.js @@ -0,0 +1,303 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/jaapvanekris/openrowingmonitor + + This creates an ordered series with labels + It allows for efficient determining the Median, Number of Above and Below +*/ + +function createLabelledBinarySearchTree () { + let tree = null + + function push (label, value) { + if (tree === null) { + tree = newNode(label, value) + } else { + // pushInTree(tree, label, value) + tree = pushInTree(tree, label, value) + } + } + + function pushInTree (currentTree, label, value) { + if (value <= currentTree.value) { + // The value should be on the left side of currentTree + if (currentTree.leftNode === null) { + currentTree.leftNode = newNode(label, value) + } else { + currentTree.leftNode = pushInTree(currentTree.leftNode, label, value) + } + } else { + // The value should be on the right side of currentTree + if (currentTree.rightNode === null) { + currentTree.rightNode = newNode(label, value) + } else { + currentTree.rightNode = pushInTree(currentTree.rightNode, label, value) + } + } + currentTree.numberOfLeafsAndNodes = currentTree.numberOfLeafsAndNodes + 1 + return currentTree + } + + function newNode (label, value) { + return { + label, + value, + leftNode: null, + rightNode: null, + numberOfLeafsAndNodes: 1 + } + } + + function size () { + if (tree !== null) { + return tree.numberOfLeafsAndNodes + } else { + return 0 + } + } + + function numberOfValuesAbove (testedValue) { + return countNumberOfValuesAboveInTree(tree, testedValue) + } + + function countNumberOfValuesAboveInTree (currentTree, testedValue) { + if (currentTree === null) { + return 0 + } else { + // We encounter a filled node + if (currentTree.value > testedValue) { + // testedValue < currentTree.value, so we can find the tested value in the left and right branch + return (countNumberOfValuesAboveInTree(currentTree.leftNode, testedValue) + countNumberOfValuesAboveInTree(currentTree.rightNode, testedValue) + 1) + } else { + // currentTree.value < testedValue, so we need to find values from the right branch + return countNumberOfValuesAboveInTree(currentTree.rightNode, testedValue) + } + } + } + + function numberOfValuesEqualOrBelow (testedValue) { + return countNumberOfValuesEqualOrBelowInTree(tree, testedValue) + } + + function countNumberOfValuesEqualOrBelowInTree (currentTree, testedValue) { + if (currentTree === null) { + return 0 + } else { + // We encounter a filled node + if (currentTree.value <= testedValue) { + // testedValue <= currentTree.value, so we can only find the tested value in the left branch + return (countNumberOfValuesEqualOrBelowInTree(currentTree.leftNode, testedValue) + countNumberOfValuesEqualOrBelowInTree(currentTree.rightNode, testedValue) + 1) + } else { + // currentTree.value > testedValue, so we only need to look at the left branch + return countNumberOfValuesEqualOrBelowInTree(currentTree.leftNode, testedValue) + } + } + } + + function remove (label) { + if (tree !== null) { + tree = removeFromTree(tree, label) + } + } + + function removeFromTree (currentTree, label) { + // Clean up the underlying sub-trees first + if (currentTree.leftNode !== null) { + currentTree.leftNode = removeFromTree(currentTree.leftNode, label) + } + if (currentTree.rightNode !== null) { + currentTree.rightNode = removeFromTree(currentTree.rightNode, label) + } + + // Next, handle the situation when we need to remove the node itself + if (currentTree.label === label) { + // We need to remove the current node, the underlying sub-trees determin how it is resolved + switch (true) { + case (currentTree.leftNode === null && currentTree.rightNode === null): + // As the underlying sub-trees are empty as well, we return an empty tree + currentTree = null + break + case (currentTree.leftNode !== null && currentTree.rightNode === null): + // As only the left node contains data, we can simply replace the removed node with the left sub-tree + currentTree = currentTree.leftNode + break + case (currentTree.leftNode === null && currentTree.rightNode !== null): + // As only the right node contains data, we can simply replace the removed node with the right sub-tree + currentTree = currentTree.rightNode + break + case (currentTree.leftNode !== null && currentTree.rightNode !== null): + // As all underlying sub-trees are filled, we need to move a leaf to the now empty node. Here, we can be a bit smarter + // as there are two potential nodes to use, we try to balance the tree a bit more as this increases performance + if (currentTree.leftNode.numberOfLeafsAndNodes > currentTree.rightNode.numberOfLeafsAndNodes) { + // The left sub-tree is bigger then the right one, lets use the closest predecessor to restore some balance + currentTree.value = clostestPredecessor(currentTree.leftNode).value + currentTree.label = clostestPredecessor(currentTree.leftNode).label + currentTree.leftNode = destroyClostestPredecessor(currentTree.leftNode) + } else { + // The right sub-tree is smaller then the right one, lets use the closest successor to restore some balance + currentTree.value = clostestSuccesor(currentTree.rightNode).value + currentTree.label = clostestSuccesor(currentTree.rightNode).label + currentTree.rightNode = destroyClostestSuccessor(currentTree.rightNode) + } + break + } + } + + // Recalculate the tree size + switch (true) { + case (currentTree === null): + // We are now an empty leaf, nothing to do here + break + case (currentTree.leftNode === null && currentTree.rightNode === null): + // This is a filled leaf + currentTree.numberOfLeafsAndNodes = 1 + break + case (currentTree.leftNode !== null && currentTree.rightNode === null): + currentTree.numberOfLeafsAndNodes = currentTree.leftNode.numberOfLeafsAndNodes + 1 + break + case (currentTree.leftNode === null && currentTree.rightNode !== null): + currentTree.numberOfLeafsAndNodes = currentTree.rightNode.numberOfLeafsAndNodes + 1 + break + case (currentTree.leftNode !== null && currentTree.rightNode !== null): + currentTree.numberOfLeafsAndNodes = currentTree.leftNode.numberOfLeafsAndNodes + currentTree.rightNode.numberOfLeafsAndNodes + 1 + break + } + return currentTree + } + + function clostestPredecessor (currentTree) { + // This function finds the maximum value in a tree + if (currentTree.rightNode !== null) { + // We haven't reached the end of the tree yet + return clostestPredecessor(currentTree.rightNode) + } else { + // We reached the largest value in the tree + return { + label: currentTree.label, + value: currentTree.value + } + } + } + + function destroyClostestPredecessor (currentTree) { + // This function finds the maximum value in a tree + if (currentTree.rightNode !== null) { + // We haven't reached the end of the tree yet + currentTree.rightNode = destroyClostestPredecessor(currentTree.rightNode) + currentTree.numberOfLeafsAndNodes = currentTree.numberOfLeafsAndNodes - 1 + return currentTree + } else { + // We reached the largest value in the tree + return currentTree.leftNode + } + } + + function clostestSuccesor (currentTree) { + // This function finds the maximum value in a tree + if (currentTree.leftNode !== null) { + // We haven't reached the end of the tree yet + return clostestSuccesor(currentTree.leftNode) + } else { + // We reached the smallest value in the tree + return { + label: currentTree.label, + value: currentTree.value + } + } + } + + function destroyClostestSuccessor (currentTree) { + // This function finds the maximum value in a tree + if (currentTree.leftNode !== null) { + // We haven't reached the end of the tree yet + currentTree.leftNode = destroyClostestSuccessor(currentTree.leftNode) + currentTree.numberOfLeafsAndNodes = currentTree.numberOfLeafsAndNodes - 1 + return currentTree + } else { + // We reached the smallest value in the tree + return currentTree.rightNode + } + } + + function median () { + if (tree !== null && tree.numberOfLeafsAndNodes > 0) { + // BE AWARE, UNLIKE WITH ARRAYS, THE COUNTING OF THE ELEMENTS STARTS WITH 1 !!!!!!! + // THIS LOGIC THUS WORKS DIFFERENT THAN MOST ARRAYS FOUND IN ORM!!!!!!! + const mid = Math.floor(tree.numberOfLeafsAndNodes / 2) + return tree.numberOfLeafsAndNodes % 2 !== 0 ? valueAtInorderPosition(tree, mid + 1) : (valueAtInorderPosition(tree, mid) + valueAtInorderPosition(tree, mid + 1)) / 2 + } else { + return 0 + } + } + + function valueAtInorderPos (position) { // BE AWARE TESTING PURPOSSES ONLY + if (tree !== null && position >= 1) { + return valueAtInorderPosition(tree, position) + } else { + return undefined + } + } + + function valueAtInorderPosition (currentTree, position) { + let currentNodePosition + if (currentTree === null) { + // We are now an empty tree, this shouldn't happen + return undefined + } + + // First we need to find out what the InOrder Postion we currently are at + if (currentTree.leftNode !== null) { + currentNodePosition = currentTree.leftNode.numberOfLeafsAndNodes + 1 + } else { + currentNodePosition = 1 + } + + switch (true) { + case (position === currentNodePosition): + // The current position is the one we are looking for + return currentTree.value + case (currentTree.leftNode === null): + // The current node's left side is empty, but position <> currentNodePosition, so we have no choice but to move downwards + return valueAtInorderPosition(currentTree.rightNode, (position - 1)) + case (currentTree.leftNode !== null && currentNodePosition > position): + // The position we look for is in the left side of the currentTree + return valueAtInorderPosition(currentTree.leftNode, position) + case (currentTree.leftNode !== null && currentNodePosition < position && currentTree.rightNode !== null): + // The position we look for is in the right side of the currentTree + return valueAtInorderPosition(currentTree.rightNode, (position - currentNodePosition)) + default: + return undefined + } + } + + function orderedSeries () { + return orderedTree(tree) + } + + function orderedTree (currentTree) { + if (currentTree === null) { + return [] + } else { + // We encounter a filled node + return [...orderedTree(currentTree.leftNode), currentTree.value, ...orderedTree(currentTree.rightNode)] + } + } + + function reset () { + tree = null + } + + return { + push, + remove, + size, + numberOfValuesAbove, + numberOfValuesEqualOrBelow, + median, + valueAtInorderPos, // BE AWARE TESTING PURPOSSES ONLY + orderedSeries, + reset + } +} + +export { createLabelledBinarySearchTree }