2

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?
mrBen
  • 276
  • 1
  • 5
  • 15
Harsha Limaye
  • 915
  • 9
  • 29
  • Could you explain why one returns the correct result for DFS and the other does not ? – Harsha Limaye Dec 16 '20 at 04:51
  • 1
    @HarshaLimaye when you return from within your for loop, you stop the loop early (as you exit the function to return back to the caller), in your second example you don't return, so you your loop can continue after it has done a call to `dfs()` – Nick Parsons Dec 16 '20 at 04:54

4 Answers4

2

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.

Enrico Cortinovis
  • 811
  • 3
  • 8
  • 31
  • Right so my 'loop' is only running once if I use the return statement correct ? – Harsha Limaye Dec 16 '20 at 05:27
  • 2
    @HarshaLimaye Yes, that's correct. You could otherwise use a `forEach()` method, in a case like this. `forEach()` does not break out of the loop with a return statement. [Here's more information on that](https://stackoverflow.com/questions/34653612/what-does-return-keyword-mean-inside-foreach-function) if it can be useful to you. – Enrico Cortinovis Dec 16 '20 at 05:29
2

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

  1. t - the input tree, or graph
  2. i - the id to start the traversal
  3. 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
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    Beat me to it. I was writing a non-generator approach, that would work similarly. Something like `const dfs = (t, i, s = new Set([i])) => [i, ... (t [i] .flatMap ((n) => s .has (n) ? [] : dfs (t, n, s .add (n))))]`. – Scott Sauyet Dec 16 '20 at 14:12
  • 1
    You should still post it! Some beginners struggle with generators so it would be beneficial to see other approaches :D – Mulan Dec 16 '20 at 14:34
2

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.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    funny, i write things like `dfs` so often that i sometimes forget what it precisely refers to ^_^ – Mulan Dec 16 '20 at 16:54
  • @Thankyou: It's also a term used mostly with *trees*, I believe, not with arbitrary (di-)graphs. It may only visit a subset of nodes. (Try either of our solutions starting with `6`, for instance. It will only yield `[6, 4, 2, 5]`). – Scott Sauyet Dec 16 '20 at 17:05
0

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

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
Pascal Nitcheu
  • 667
  • 7
  • 8