Recursive Loop for Generic Tree - javascript

for days I am now trying to solve the following problem. A recursive function is suppose to iterate through a dictionary that is basically a representation of a tree.
The input looks like this:
var connections = {1:[2, 3, 4], 2:[5, 6, 7], 3:[8], 4:[9, 10]}
and the corresponding tree that it represents looks like this:
The intermediate goal is to print out the numbers in a depht first search manner, namely:
My solution for this problem is:
function dfs(connections, parent) {
console.log(parent);
if (!(parent in connections)) {
return;
}
for (i = 0; i < connections[parent].length; i++) {
dfs(connections, connections[parent][i]);
}
return;
}
However, calling the function
dfs(connections, 1)
leads to the following result:
1
2
5
6
7
That means it is not returning to the previous function and continuing the for-loop. If you have any idea whatsoever, I would be very grateful.
Cheers

Your i is implicitly global, so after it iterates through 2 (which has a length of 3), i is 4, so further tests of i < connections[parent].length fail.
You could use let to fix it (for (let i = 0), but it would probably be better to use forEach instead: array methods are less verbose, less error-prone, and more functional than for loops:
var connections = {
1: [2, 3, 4],
2: [5, 6, 7],
3: [8],
4: [9, 10]
}
function dfs(connections, parent) {
console.log(parent);
if (!(parent in connections)) {
return;
}
connections[parent].forEach(prop => dfs(connections, prop));
}
dfs(connections, 1)

Related

Javascript recursion related question about push() and unshift() methods working opposite

function countup(n) {
if (n < 1) {
return [];
} else {
const countArray = countup(n - 1);
countArray.push(n);
return countArray;
}
}
console.log(countup(5));
After running the above code it returns an array: [1, 2, 3, 4, 5], but push() adds new values at the end of an array so when value of n was 5 it should push 5 to the end of the array and when value of n got 4 it should push 4 at the end of the array like [5,4].
So why not it is returning [5,4,3,2,1] ? It is hard to understand what is happening in this code probably because of this recursion. Isn’t unshift() (which add new values to start of the array) should return [1,2,3,4,5] and push() [5,4,3,2,1] why opposite is happening?
As #Joseph stated in a comment the second to last function call would get pushed to the array first, then it would return that array, where it immediately adds the next number up the call stack.
Here are the steps being taken.
Initial call enters itself recursively n times, where the bottom call returns [] and then [1], [1, 2] ... [1, 2, ..., n] all the way up the call stack where upon the first function call ends and the program does something else.
To get [n, ..., 2, 1] you need to use the Array.prototype.unshift() method, which takes any javascript primitive type, ie String, Number, Boolean, and Symbols, in a comma separated format, like countArray.unshift(4, 5) or countArray.unshift(...anotherArray), and adds them to the beginning of the array.
ie
let someArr = [3, 2, 1];
someArr.unshift(5, 4);
console.log(JSON.stringify(someArr));
// outputs [5, 4, 3, 2, 1]
or
let someArr = [1, 2, 3];
let anotherArr = [5, 4]
someArr.unshift(...anotherArr);
console.log(someArr);
// outputs [5, 4, 1, 2, 3]
Where the output of
function countup(n) {
if (n < 1) {
return [];
} else {
const countArray = countup(n - 1);
countArray.unshift(n);
return countArray;
}
}
console.log(countup(5));
will be [5, 4, 3, 2, 1] tested with node in Vscode.
One useful way to think about this is to start by imagining you already had a function that does what you want for lower values, and then see how you would write one that works for higher values. That imaginary function should be a black box. All we should know is that it does what we want in the case of lower values. We don't care about its implementation details.
So lets say we had a function imaginaryBlackBox, and we knew that it returned the correct countUp values that we want. So, for instance, we know that imaginaryBlackBox (4) returns [1, 2, 3, 4].
Now, knowing that, how might we write a function that works for an input of 5 as well? How about something like this:
function countup(n) {
if (n < 1) {
return [];
} else {
const countArray = imaginaryBlackBox(n - 1);
countArray.push(n);
return countArray;
}
}
Again, we don't know how imaginaryBlackBox works. We just know that it returns the correct result for lower values of n. Our base case remains obvious. For another case, some n greater than 0, we call imaginaryBlackBox(n - 1), and by our basic assumption, we know that will return [1, 2, 3, ..., (n - 1)], which we store in countArray. Then we push n onto that array, to end up with [1, 2, 3, ..., (n - 1), n]. We return that value and we're done.
Now here's the trick. We know an implementation of imaginaryBlackBox -- it's the function we're writing! So we can simply replace it with countUp and know it will work.
function countup(n) {
if (n < 1) {
return [];
} else {
const countArray = countUp(n - 1);
countArray.push(n);
return countArray;
}
}
This required a few assumptions, and they are important to all recursive functions:
There is at least one base case whose value we can calculate without any recursive call. Here, when n is < 1, we simply return [].
For other cases, we can break down our problem into one or more recursive cases, where the input is in some clear and measurable way closer to a base case, so that subsequent steps will hit a base case in a finite number of calls. Here we reduce n by 1 on every step, so eventually it will be below 1.
Our function is effectively pure: its outputs depend only on its inputs. That means we can't count on changing a global variable or reading from one that might be changed elsewhere. Note that I use the qualifier "effectively" here; it doesn't matter if this function has observable side-effects such as logging to the console, so long as its output is dependent only on its input.
Anytime you have those conditions, you have the makings of a good recursive function.

What is the difference between returning a function call vs only calling the function again during recursion?

I am trying to implement a DFS and I do not understand the difference between calling a function inside itself (recursion) and returning the function call (also recursion?)
Snippet 1: Returning a function call (Wrong answer)
In this case, the code is not backtracking correctly.
const graph = {
1: [2, 3],
2: [4, 5],
3: [1],
4: [2, 6],
5: [2, 6],
6: [4, 5]
}
let visited = [];
const dfs = (node) => {
if (visited.includes(node))
return;
console.log(node);
visited.push(node);
for (let i = 0; i < graph[node].length; i++) {
if (!visited.includes(graph[node][i]))
return dfs(graph[node][i])
}
}
dfs(1);
Snippet 2: Only calling the function (Correct answer)
Seems to work okay
const graph = {
1: [2, 3],
2: [4, 5],
3: [1],
4: [2, 6],
5: [2, 6],
6: [4, 5]
}
let visited = [];
const dfs = (node) => {
if (visited.includes(node))
return;
console.log(node);
visited.push(node);
for (let i = 0; i < graph[node].length; i++) {
if (!visited.includes(graph[node][i]))
dfs(graph[node][i])
}
}
dfs(1);
What is the difference between the two? (I thought they'd be the same)
Is this some language specific (JS) thing or am I misunderstanding recursion?
When you return the "function call", you actually return the value that the function that is called yields. When you simply call a function recursively without returning it, you don't do anything with the return value. They are both cases of recursion, and they would work similarly when NOT in a loop.
In this case, since you are using the return value of the function within your for loop, once dfs(graph[node][i]) runs for the first time, and the function finishes executing by returning a value (or just finishes executing, like in this case) and exits the stack call, the for loop ends, and the function execution stops too.
You will experience fewer headaches if you write functions that avoid mutating external state and instead operate on the supplied arguments. Below we write dfs with three parameters
t - the input tree, or graph
i - the id to start the traversal
s - the set of visited nodes, defaults to a new Set
function* dfs (t, i, s = new Set)
{ if (s.has(i)) return
s.add(i)
yield i
for (const v of t[i] ?? [])
yield* dfs(t, v, s)
}
const graph =
{ 1: [2, 3]
, 2: [4, 5]
, 3: [1]
, 4: [2, 6]
, 5: [2, 6]
, 6: [4, 5]
}
for (const node of dfs(graph, 1))
console.log(node)
1
2
4
6
5
3
remarks
1. Your original dfs function has a console.log side effect — that is to say the main effect of our function is to traverse the graph and as a side (second) effect, it prints the nodes in the console. Separating these two effects is beneficial as it allows us to use the dfs function for any operation we wish to perform on the nodes, not only printing to the console -
dfs(1) // <- traverse + console.log
Using a generator allows us to easily separate the depth-first traversal from the console printing -
for (const node of dfs(graph, 1)) // <- traverse effect
console.log(node) // <- print effect
The separation of effects makes it possible to reuse dfs in any way we need. Perhaps we don't want to print all of the nodes and instead collect them in an array to send them elsewhere -
const a = []
for (const node of dfs(graph, 1)) // <- traverse effect
a.push(node) // <- array push effect
return a // <- return result
2. When we loop using an ordinary for statement, it requires intermediate state and more syntax boilerplate -
for (let i = 0; i < graph[node].length; i++)
if (!visited.includes(graph[node][i]))
dfs(graph[node][i])
Using for..of syntax (not to be confused with for..in) allows us to focus on the parts that matter. This does the exact same thing as the for loop above -
for (const child of graph[node])
if (!visited.includes(child))
dfs(child)
3. And using an Array to capture visited nodes is somewhat inefficient as Array#includes is a O(n) process -
const visited = [] // undefined
visited.push("x") // 1
visited.includes("x") // true
Using a Set works almost identically, however it provides instant O(1) lookups -
const s = new Set // undefined
s.add("x") // Set { "x" }
s.has("x") // true
Others have explained why return is short-circuiting your process.
But I would suggest that the main issue is that you are not really using that recursive function to return anything, but only relying on the the side effect (of printing to the console) inside the function. If you really want a traversal of your graph, it would be cleaner to write a function which returned an ordered collection of the nodes. The answer from Thankyou gives you one such function, using generator functions, as well as some valuable advice.
Here's another approach, which turns your (connected) graph into an array:
const dft = (graph, node, visited = new Set([node])) => [
node,
... (graph [node] .flatMap (
(n) => visited .has (n) ? [] : dft (graph, n, visited .add (n))
)),
]
const graph = {1: [2, 3], 2: [4, 5], 3: [1], 4: [2, 6], 5: [2, 6], 6: [4, 5]}
console .log (dft (graph, 1)) //~> [1, 2, 4, 6, 5, 3]
We also use a Set rather than an array to track the visited status of nodes. We first visit the node supplied, and then for each node it connects to, we recursively visit that node if we haven't already marked it as visited. (I call this dft as it's a depth-first traversal, not a depth-first search.)
But please do carefully read the advice in Thankyou's answer. It's quite valuable.
In programming terms a recursive function can be defined as a routine that calls itself directly or indirectly.So in your example both would be considered recursion.
But when considering the fact that the recursion principle is based on the fact that a bigger problem is solved by re-using the solution of subset problem, then we would need those subset results to compute the big result. Without that return you will only get an undefined which is not helping you to solve your problem.
A quite easy example would be the factorial where fact(n) = n * fact(n-1)
function fact (n) {
if (n===0 || n===1) return 1;
else return n*fact(n-1);
}
As you can see on this example, fact(4) = 4 * fact(3) without the return, it will be undefined.
P.S: In your example not calling a return might work simply because we are not re-using the result of the subset

Whiteboard problem finding an 'item' and returning the following item in the string or array

I return some of the tests correct but I am not sure why some of my answers come up undefined. I feel like my solution should work with both arrays and strings. Any advice would be appreciated!
Here is the problem:
Given a sequence of items and a specific item in that sequence, return the item immediately following the item specified. If the item occurs more than once in a sequence, return the item after the first occurence. This should work for a sequence of any type.
When the item isn't present or nothing follows it, the function should return nil in Clojure and Elixir, Nothing in Haskell, undefined in JavaScript, None in Python.
Examples provided:
nextItem([1, 2, 3, 4, 5, 6, 7], 3) // 4
nextItem("testing", "t") // "e"
My code:
function nextItem(xs, item) {
for(let i = 0; i < xs.length; i++){
if(xs[i] === item){
return xs[i + 1]
}
}
}
console.log(nextItem([1, 2, 3, 4, 5, 6, 7], 3)) // 4
console.log(nextItem("testing", "t")) //e

What is faster - merge 2 sorted arrays into a sorted array w/o duplicate values

I was trying to figure out which is the fastest way to do the following task:
Write a function that accepts two arrays as arguments - each of which is a sorted, strictly ascending array of integers, and returns a new strictly ascending array of integers which contains all values from both of the input arrays.
Eg. merging [1, 2, 3, 5, 7] and [1, 4, 6, 7, 8] should return [1, 2, 3, 4, 5, 6, 7, 8].
Since I don't have formal programming education, I fint algorithms and complexity a bit alien :) I've came with 2 solutions, but I'm not sure which one is faster.
solution 1 (it actually uses the first array instead of making a new one):
function mergeSortedArrays(a, b) {
for (let i = 0, bLen = b.length; i < bLen; i++) {
if (!a.includes(b[i])) {
a.push(b[i])
}
}
console.log(a.sort());
}
const foo = [1, 2, 3, 5, 7],
bar = [1, 4, 6, 7, 8];
mergeSortedArrays(foo, bar);
and solution 2:
function mergeSortedArrays(a, b) {
let mergedAndSorted = [];
while(a.length || b.length) {
if (typeof a[0] === 'undefined') {
mergedAndSorted.push(b[0]);
b.splice(0,1);
} else if (a[0] > b[0]) {
mergedAndSorted.push(b[0]);
b.splice(0,1);
} else if (a[0] < b[0]) {
mergedAndSorted.push(a[0]);
a.splice(0,1);
} else {
mergedAndSorted.push(a[0]);
a.splice(0,1);
b.splice(0,1);
}
}
console.log(mergedAndSorted);
}
const foo = [1, 2, 3, 5, 7],
bar = [1, 4, 6, 7, 8];
mergeSortedArrays(foo, bar);
Can someone help me with time complexity of both solutions? Also, is there another, faster solution? Should I be using reduce()?
Your first function modifying passed argument and this is bad practice.
You can do it in the following way:
function mergeSortedArrays(a, b) {
return a.concat(b.filter(el => !a.includes(el))).sort();
}
The complexity also depends on the methods that you use in your code.
1) An algorithm commonly used for sorting is Quicksort (introsort as a variation of quicksort).
It has O(n log n) complexity however the worst case may still be O(n^2) in case the input is already sorted. So your solution has O( length(b) + length(a)+length(b) log (length(a)+length(b)) ) runtime complexity.
2) According to this question on the complexity of splice() the javascript function needs O(n) steps at worst (copying all elements to the new array of size n+1). So your second solution takes length of array b multiplied by n steps needed to copy the elements during splice plus the time to push().
For a good solution that works in linear time O(n+m) refer to this Java example and port it (i.e. create an array of size length(a) + length(b) and step via the indeces as shown). Or check out the very tight and even a littler faster implementation below of the answer.

Flatten a jagged multi dimensional array

I need help flattening an array like this:
[1,2,[2,3],[5,[6,1],4],7]
I want it to be something like
[1,2,2,3,5,6,1,4,7].
I have searched something like this, and found [].concat.apply, but it will only take care of the two dimensional arrays.
I also want to use an algorithm that will work for any jagged multi dimensional arrays. Please help. Thx
My recommendation would be to take a dependency on lodash and use the flattenDeep function.
_.flattenDeep([1,2,[2,3],[5,[6,1],4],7])
// [ 1, 2, 2, 3, 5, 6, 1, 4, 7 ]
If you want to write your own function, you might want to peek at the lodash implementation.
In pseudo-code, here's a recursive approach:
result = []
function flatten(array)
for each element in array
if element is array
flatten(element)
else
result.append(element)
EDIT
Here's a "by-hand" approach, though I'd definitely recommend relying on the better-tested lodash implementation.
function flatten(arr, result) {
if (result === undefined) {
result = [];
}
for (var i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
flatten(arr[i], result);
} else {
result.push(arr[i]);
}
}
return result;
}
console.log(flatten([1, 2, [2, 3], [5, [6, 1], 4], 7]));
// Output:
// [ 1, 2, 2, 3, 5, 6, 1, 4, 7 ]
You can wrap the concat.apply thing in a loop to handle deeply nested arrays:
while (a.some(Array.isArray))
a = [].concat.apply([], a)
or in the ES6 syntax:
while (a.some(Array.isArray))
a = [].concat(...a);

Categories

Resources