Create BinarySearchTree.js

Added the Binary Search Tree to greatly improve the performance of the Median calculation
This commit is contained in:
Jaap van Ekris 2023-12-03 20:24:28 +01:00 committed by GitHub
parent b542535bd7
commit f342e6691e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 303 additions and 0 deletions

View File

@ -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 }