返回函数调用与仅在递归期间再次调用函数有啥区别?

Posted

技术标签:

【中文标题】返回函数调用与仅在递归期间再次调用函数有啥区别?【英文标题】:What is the difference between returning a function call vs only calling the function again during recursion?返回函数调用与仅在递归期间再次调用函数有什么区别? 【发布时间】:2021-03-26 18:34:18 【问题描述】:

我正在尝试实现 DFS,但我不明白在其内部调用函数(递归)和返回函数调用(也是递归?)之间的区别片段 1:返回函数调用(错误答案) 在这种情况下,代码没有正确回溯。

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);

片段 2:仅调用函数(正确答案) 好像没问题

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);
两者有什么区别? (我以为它们是一样的) 这是某种特定于语言(JS)的东西还是我误解了递归?

【问题讨论】:

你能解释一下为什么一个为 DFS 返回正确的结果而另一个没有? @HarshaLimaye 当你从你的 for 循环中返回时,你会提前停止循环(当你退出函数返回给调用者时),在你的第二个例子中你没有返回,所以你您的循环在调用 dfs() 后可以继续 【参考方案1】:

当你返回“函数调用”时,你实际上返回了被调用函数产生的值。当您只是递归地调用一个函数而不返回它时,您不会对返回值做任何事情。它们都是递归的情况,当不在循环中时它们的工作方式相似。

在这种情况下,由于您在 for 循环中使用函数的返回值,所以一旦 dfs(graph[node][i]) 第一次运行,并且函数通过返回一个值完成执行(或者只是完成执行,就像在这个case) 并退出堆栈调用,for 循环结束,函数执行也停止。

【讨论】:

对,如果我正确使用 return 语句,我的“循环”只会运行一次? @HarshaLimaye 是的,没错。在这种情况下,您可以使用forEach() 方法。 forEach() 不会使用 return 语句跳出循环。 Here's more information on that如果对你有用的话。【参考方案2】:

如果您编写的函数避免改变外部状态,而是对提供的参数进行操作,那么您会遇到更少的麻烦。下面我们用三个参数写dfs

    t - 输入树或图 i - 开始遍历的id s - 访问节点的集合,默认为 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

备注

1. 你原来的dfs 函数有一个console.log 副作用——也就是说我们函数的主要作用是遍历图并作为一个副作用(第二个),它在控制台中打印节点。分离这两种效果是有益的,因为它允许我们使用dfs 函数来执行我们希望在节点上执行的任何操作,而不仅仅是打印到控制台 -

dfs(1)  // <- traverse + console.log

使用生成器可以让我们轻松地将深度优先遍历与控制台打印分开 -

for (const node of dfs(graph, 1)) // <- traverse effect
  console.log(node)               // <- print effect

效果的分离使得以我们需要的任何方式重用dfs 成为可能。也许我们不想打印所有节点,而是将它们收集在一个数组中以将它们发送到其他地方 -

const a = []
for (const node of dfs(graph, 1)) // <- traverse effect
  a.push(node)                    // <- array push effect
return a                          // <- return result

2. 当我们使用普通的for 语句循环时,它需要中间状态和更多的语法样板 -

for (let i = 0; i < graph[node].length; i++)
  if (!visited.includes(graph[node][i]))
    dfs(graph[node][i])

使用for..of 语法(不要与for..in 混淆)可以让我们专注于重要的部分。这与上面的for 循环完全相同 -

for (const child of graph[node])
  if (!visited.includes(child))
    dfs(child)

3.并且使用数组来捕获visited 节点效率有点低,因为Array#includes 是一个O(n) 过程-

const visited = []    // undefined
visited.push("x")     // 1
visited.includes("x") // true

使用 Set 的工作方式几乎相同,但它提供了即时 O(1) 次查找 -

const s = new Set   // undefined
s.add("x")          // Set  "x" 
s.has("x")          // true

【讨论】:

打败我。我正在编写一种非生成器方法,它的工作方式类似。类似于const dfs = (t, i, s = new Set([i])) =&gt; [i, ... (t [i] .flatMap ((n) =&gt; s .has (n) ? [] : dfs (t, n, s .add (n))))] 你仍然应该发布它!一些初学者在使用生成器时遇到了困难,所以看看其他方法会很有帮助:D 谢谢,已发布!【参考方案3】:

其他人已经解释了为什么 return 会使您的流程短路。

但我建议主要问题是您并没有真正使用该递归函数来返回任何内容,而只是依赖于函数内部的副作用(打印到控制台)。如果您真的想要遍历您的图,那么编写一个返回有序节点集合的函数会更简洁。 Thankyou 的回答给了你一个这样的函数,使用生成器函数,以及一些有价值的建议。

这是另一种方法,它将您的(连接的)图变成一个数组:

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]

我们还使用 Set 而不是数组来跟踪节点的访问状态。我们首先访问提供的节点,然后对于它连接的每个节点,如果我们尚未将其标记为已访问,我们将递归访问该节点。 (我称之为dft,因为它是深度优先遍历,而不是深度优先搜索。)

但是请仔细阅读Thankyou的回答中的建议。很有价值。

【讨论】:

有趣,我经常写dfs之类的东西,以至于我有时会忘记它到底指的是什么^_^ @Thankyou:我相信这也是一个主要用于 trees 的术语,而不是用于任意 (di-)graphs。它可能只访问节点的一个子集。 (例如,尝试我们以6 开头的任一解决方案。它只会产生[6, 4, 2, 5])。【参考方案4】:

在编程术语中,递归函数可以定义为直接或间接调用自身的例程。因此在您的示例中,两者都将被视为递归。

但是考虑到递归原理是基于通过重用子集问题的解决方案来解决更大问题的事实,那么我们将需要这些子集结果来计算大结果。没有那个回报,你只会得到一个未定义的,它不能帮助你解决你的问题。

一个非常简单的例子是fact(n) = n * fact(n-1)的阶乘

function fact (n) 
 if (n===0 || n===1) return 1;
 else return n*fact(n-1);

正如你在这个例子中看到的,fact(4) = 4 * fact(3) 没有返回,它将是未定义的。

P.S:在您的示例中,不调用 return 可能只是因为我们没有重用子集的结果

【讨论】:

以上是关于返回函数调用与仅在递归期间再次调用函数有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

延迟为 0 的“performSelector:withObject:afterDelay:”与仅调用选择器有啥区别?

c语言中函数定义和声明有啥区别

main函数的返回类型有啥区别

调用函数和返回函数有啥区别?

JavaScript 递归算法

C 递归 详解(通俗易懂)