Prerequisites: I have a general tree class with: myTree = new Tree(string), appendChildNode(node), createChildNode(string)->node, myTree.print() functions and myTree.name, myTree.children, myTree.depth, etc attributes.
I also have a multimap with data which I want to decompose and put into a tree object. The data has occasionally cycles on further depth. For that reason I want to build a bredth-first tree structure which I can query. At a leaf, where a cycle reference is found I want the branching to stop. So I built a simple function that uses a queue. It pulls an element from the multimap, gets its children and puts them into the queue. That works fine.
My problem is: When putting an element into the queue, I am losing the information about which parent it had when it was added. So when I want to add a new node I end up with a flat tree.
Question: Would I need also to store the information about the parent in the queue somehow? Or how else would I go about it? Maybe I store an object in the queue that holds {childname, parentNode}?
Bonus question: Is this problem maybe better solved with some kind of cyclic graph? (due to cycles in data)
my multimap (just an example here)
aaaa -> [b, c, d]
b -> [f]
f -> [g]
g -> [c] (//this is the circle: g points back to higher node 'c')
d -> [e]
e -> [m,n]
r -> [p]
Here is the tree, if you start tracing the multimap from 'aaaa'. Notice, r->p is irrelevant if you start with 'aaaa'.
The result of myTree.print() should look like like this in the console:
My Diagrams
1 aaaa (8 descendants)
2 b (2 descendants)
3 f (1 descendants)
4 g (0 descendants) [hint: terminating here, g points back to 'c'] //in red
2 c (0 descendants)
2 d (3 descendants)
3 e (2 descendants)
4 m (0 descendants)
4 n (0 descendants)
So: Depth, Nodename, Amount of siblings below + note if it is a terminal node due to a circle reference
Edit: g
could have other children than c as trincot noted. In that case the tree should continue for those children. It should be possible to see that there was a reference to c
from g
and it stopped there. Idea: Maybe append a special node under g
that represents c
, but stops there. That special node below g
would be shown red in console.
function calcEnd2End(multiMap) {
queue = [];
resultTree = new Tree("My Diagrams");
queue.push("aaaa");
buildWideTreeFromMM("aaaa");
resultTree.print();
function buildWideTreeFromMM() {
counter = 0;
depth = 0;
node = resultTree;
// as long as queue has elements go on
while (queue.length != 0) {
// get oldest element
someParentStr = queue[0];
// if element is in multimap, check if it has children
if (multiMap.has(someParentStr)) {
// since it has children, depth is +1
depth++;
// iterate through all children
multiMap.get(someParentStr).forEach((childNameStr) => {
// check if this child points to higher element already in tree, if yes,
// set attribute 'isTerminalElement' to know later which nodes were circular ones
// set attribute 'setCircleReference' to know which higher sibling the reference pointed to, to show in hint message
// checkIfElemetIsInTree checks if there is already a node with that name in the tree (I have not written this func yet)
if (checkIfElemetIsInTree) {
node.createChildNode(childNameStr).setDepth(depth).setIsTerminalElement(true).setCircleReference(someParent)
// This is terminal element, no need to queue it
return
}
// childname is not in tree, so push it into queue to be checked on some next iteration
queue.push(child);
// create a new node and set its depth property
// PROBLEM IS HERE...
node.createChildNode(child).setDepth(depth)
});
queue.shift();
// if element is not in multimap, it has no children, so remove it from queue
} else {
queue.shift();
}
}
}
}
Some comments:
const
, let
or var
. Not doing that makes them global variables, which is bad practicedepth
keeps increasing even if the dequeued name corresponds to the same depth as the previously dequeued name.return
in the loop, as this will break any further processing of the queue.createChildNode
is called on the node that becomes the parent, there should be no need to pass a depth argument. It can be derived from the parent's depth
property.g
node, but this cannot be generally applied like that, because g
is not a terminal node. The terminal node is an extra node that was created with label c
. In general g
could have a mix of terminal children and normal children. So the hint would be better placed at the terminal children. I would therefore suggest that the output has another indented line below g
with c
(duplicate) with a hint that explains this is a back reference to the "real" c
.Here is an implementation with some more ideas included:
class Tree {
constructor(name, backReference) {
this.name = name;
this.backReference = backReference ?? null;
this.children = [];
this.parent = null;
this.depth = 0;
this.descendantCount = 0;
}
createChildNode(name, backReference) {
let node = new Tree(name, backReference);
// Deal with depth, parent, children, descendantCount
node.parent = this;
node.depth = this.depth + 1;
this.children.push(node);
if (!backReference) {
// Adjust the descendant count in all ancestor nodes
for (let node = this; node; node = node.parent) {
node.descendantCount++;
}
}
return node;
}
print() {
console.log(`${" ".repeat(this.depth)}${this.name} ${
this.backReference ? "[hint: terminating here; this is a backreference]" : `(${this.descendantCount} descendants)`
}`);
for (const child of this.children) child.print();
}
}
function calcEnd2End(multiMap, start="aaaa") { // Provide starting point
// declare your variables with explicit scope!
const root = new Tree(start); // Don't create a dummy root. Just create one for the start node
const visited = new Map().set(start, root); // For keying node instances by their name
const queue = [root];
while (queue.length) {
const node = queue.shift(); // Immediately shift it out
if (multiMap.has(node.name)) {
for (const childName of multiMap.get(node.name)) {
const child = node.createChildNode(childName, visited.get(childName));
if (!child.backReference) {
visited.set(childName, child);
queue.push(child);
}
}
}
}
return root;
}
// Example run
const multiMap = new Map([
["aaaa", ["b", "c", "d"]],
["b", ["f"]],
["f", ["g"]],
["g", ["c"]], // this is the cycle
["d", ["e"]],
["e", ["m","n"]],
["r", ["p"]]
]);
const root = calcEnd2End(multiMap, "aaaa");
root.print();