如何保持"调试"我脑子里的递归函数?

时间:2016-06-06 12:23:26

标签: javascript algorithm debugging recursion computer-science

我有一个关于递归的问题:我应该如何思考"在我的头脑中处理,保持和调试递归? 让我解释一下:例如,我们有一个计算Fibonacci数的函数:

function fib(n) {
    if(n < 3) return 1;
    return fib(n - 1) + fib(n - 2);
}

看起来很简单。让我们更简单的调试:

function fib(n) {
    if(n < 3) return 1;
    var r1 = fib(n - 1);
    var r2 = fib(n - 2);
    var result = r1 + r2;
    return result;
}

现在,让我们看看这个函数如何工作,如果n = 5:

fib(5) //n > 2, we need go deeper r1 = fib(n - 1) -> call fib(4)
    ->fib(4) //n > 2, we need go deeper r1 = fib(n - 1) -> call fib(3)
        ->fib(3) //n > 2, we need go deeper r1 = fib(n - 1) -> call fib(2)
            ->fib(2) n < 3 -> return 1

现在,让我再写一遍(请从下到上阅读,从fib(2)开始):

fib(5) n = 5; r1 = fib(4) -> 3; r2 = fib(3)//here we go one more time:
                                         r1 = fib(2) return [1]
                                         r2 = fib(1) return [1]
                                         r = r1 + r2 = 1 + 1 = [2]
                                         So fib(3) -> [2];
                                         Only now we can calculate fib(5):
                                            n = 5; r1 = 3; r2 = 2 ->
                                            r = r1 + r2 = 3 + 2 = 5; //Answer

    ->fib(4) n = 4; r1 = fib(3) -> 2; r2 = fib(2) -> 1; r = r1 + r2 = 2 + 1 = [3]
        ->fib(3) n = 3; r1 = fib(2) -> 1; r2 = fib(1) -> 1; r = r1 + r2 = 2 + 1 = [2] 
            ->fib(2) return [1]

现在让我们看看Fibonacci数字不是递归函数:

function fn(number) {
    if(number === 0) return 0;
    var fib = [1, 1];

    for(var i = 2; i < number; i++) {
        var temp = fib[i - 1] + fib[i - 2];
        fib.push(temp);
    }
    return fib[fib.length - 1];
}

该函数有更多的代码,但它只包含一个循环,我可以轻松地将所有内容保存在我的头脑中而没有大量的递归级别。

我作为示例展示的递归函数不是我的,我不明白如何实现这样的函数。 使用循环一切都非常简单:

  1. 我们有一个带有两个值arr = [1,1]的数组。
  2. 我们想获得下一个价值?
  3. 没问题:只为arr [n] = arr [n - 1] + arr [n - 求和 2]; return arr [n];
  4. 就是这样:))

    不要误会我的意思,我花了几个月的时间来理解如何解析&#34;递归功能在我脑海中,但仍然没有找到任何解决方案。只用纸和思考几个小时对我有用。

5 个答案:

答案 0 :(得分:2)

想象它!

function fib(n, indent) {
  console.log(indent + "fib(" + n + ")");
  if(n < 3) return 1;
  indent += "  ";
  return fib(n - 1, indent) + fib(n - 2, indent);
}

此修改将保留与indent参数中的递归深度相对应的空格,并将它们与调用一起打印。应该有助于更好地了解哪个呼叫导致其他呼叫,以及各个分支如何终止。

初次通话:

 fib(4, "");

请注意,在简单的递归情况下,运行时是指数的,而迭代版本是线性的(尝试20或30来看看差异 - 你不需要数组,只需要最后两个值)

答案 1 :(得分:2)

问题可能是你想要做太多事情。

递归函数是声明性的。在您提供的情况下,第n个Fibonacci是(n-1)Fibonacci数和(n-2)Fibonacci数之和。而已。这就是你的所有功能所说的,这也是问题的确切解决方案:不需要思考

你通常需要考虑的 ONLY 是基本情况。

诀窍? 假设您的功能已经完美运行。 不要考虑它是如何工作的。想象一下它已经存在,正如您所需要的那样工作。你可以假装你甚至没有写这个功能 - 其他人写了它,它可以工作。

同样的方法几乎适用于任何递归求解的问题。

假设您希望获得数组中的最小值。那么,这与从数组中取出第一个值并询问“这个值是否比其他所有值都小?”一样。

即。 在数组中,[1,2,3,4,5]是第一个元素,1,小于数组[2,3,4,5]其余部分中的最小元素?

我们知道我们可以在[2,3,4,5]中得到最小值,因为我们假设我们的函数有效。

还有一件事,基本情况是什么?

如果数组为空,则最小值没有意义,我们可能需要返回null类型值或引发异常。

如果数组有1个元素,那么这必须是最小值,因为没有其他元素。大。所以我们有这个:

function minimumValue(arr) {
    if (arr.length == 0) {
        // handle this problem
    } else if (array.length == 1) {
        let firstElement = arr[0];
        return firstElement;
    }

    // assume the minimumValue function works
    let firstElement = arr[0];
    let restOfArray = arr.slice(1, arr.length);
    return min(firstElement, minimumValue(restOfArray));
}

我不需要考虑任何事情。我刚刚将我想到的确切解决方案翻译成代码,并且它有效,并且它是非常易读的IMO。

如果你完全学习数学,那么你可以把它想象成归纳证明。假设它最多可以达到N,你只需要编写N + 1个案例。当然,不要忘记基本情况!

边缘情况和其他问题是不可避免的,有时将递归调用扩展几次,或者只是仔细检查您的编程解决方案是否遵循您想到的实际解决方案是有用的。通常需要仔细考虑基本情况。

答案 2 :(得分:0)

以下是您应该如何考虑递归函数的方法。

  1. 答:这应该是一个递归函数吗?在满足终止条件之前,我是否需要调用迭代操作?
  2. 答:我的终止条件是什么?
  3. 答案:我是否需要在执行操作之前或之后检查我的终止条件?
  4. 从那里,您可以根据您的使用案例制定更具体的策略。

答案 3 :(得分:0)

我觉得,简单地说,如果你可以使用forwhile循环做一些比你应该做的事情。如果它不是必须的话,请不要试图让它递归,因为它们可能会成倍地(字面上)变得复杂并且屁股的痛苦会进行调试。

当你需要做一些需要递归的事情时你才会知道。您知道因为您开始forwhile循环,并且在2天后意识到您无法做您需要的事情而您将需要某种递归函数。

答案 4 :(得分:0)

当可视化工作原理时,我建议你从边缘情况开始,然后开始向上(或向下)。如果一切都检查了前几个输入,那么事情应该适用于&#34;进一步&#34;值。

数学中的定义通常是递归给出的。看看如何定义fibonnaci序列:

f[0] = 0
f[1] = 1
f[n] = f[n-1] + f[n-2]

试着想象n是2.你得到的是

f[2] = f[1] + f[0] = 1 + 0 = 1

现在您知道f[2]是什么,在计算f[3]时,您不必重新考虑它。只需获取前面步骤中的结果。

f[3] = f[2] + f[1] = (1 + 0) + 1 = 1 + 1 = 2
f[4] = f[3] + f[2] = 2 + 1 = 3
f[5] = f[4] + f[3] = 3 + 2 = 5
...
f[n] = f[n-1] + f[n-2]

现在让我们将其转换为JavaScript:

var fib = function(n){
  if( n === 0 || n === 1 )
    return n;
  return fib(n-1) + fib(n-2);
};

如您所见,该函数与数学定义非常相似。 在Haskell等声明性语言中看起来更是如此:

fib 0 = 0
fib 1 = 1
fib n = fib(n-1) + fib(n-2)

我衷心建议你学习Haskell的基础知识,以便掌握递归,因为它被广泛使用。那里有一个非常有趣的教程learnyouahaskell

即使您没有通过,也请阅读section on recursion

起初看起来有点复杂,但经过一些练习后,它往往是最简单,最自然的解决方案。在处理自己递归的结构(例如树)时尤其如此。

一些例子:

// get the sum of array
var sumArray = function( array ){
  if( array.length === 1 )                 // if only one element, it is the sum
    return array[0];
  return array.shift() + sumArray(array);  // return array[0] + sumArray(array.slice(1))
};
console.log( sumArray( [1,2,3] ) );


// recursive functions don't have to return a value
var repeatFunction = function( action, times ){
  if( times === 0 )
    return;
  action();
  repeatFunction( action, times-1 );
};
repeatFunction( function(){ alert('jah'); }, 3 );


// used in function below
var repeatString = function( string, times ){
  if( times < 1 )
    return '';
  return string + repeatString( string, times-1 );
};
console.log( repeatString( 'jah', 3 ) );


// and finally, a tree traversal
var readDOMStructure = function( element, level ){
  if( typeof level === 'undefined' )
    level = 0;
  else
    level++;
  console.log( repeatString( '  ', level ) + element.nodeName );
  for( var i=0, n=element.children.length; i<n; i++ )
    readDOMStructure( element.children[i], level );
};
readDOMStructure( document.getElementById( 'jah' ) );