用外行的话来说就是使用PHP的递归函数


72

谁能用外行语言和示例向我解释PHP中的递归函数(不使用Fibonacci)?我在看一个例子,但斐波那契完全让我迷失了!

预先谢谢您;-)您还经常在Web开发中使用它们吗?


142
阅读stackoverflow.com/questions/2648968,这最终将有意义。
凯文

23
或尝试访问Google的“您不是要说”链接google.com/search?hl=zh
Gordon 2010年

9
@kevin也许你应该写为“重复注释stackoverflow.com/questions/2648968 ”)
普罗格曼

21
@Kevin:但是你忘了基本情况!他不会那样学习递归,他会一直单击它,直到他的堆栈溢出为止。:(
CA McCann

4
考虑到我们正在使用的网站,我认为@camccann是合适的
琥珀色

Answers:


89

外行条款:

递归函数是一个调用自身的函数

深入一点:

如果该函数不断调用自身,它如何知道何时停止?您设置了一个条件,称为基本情况。基本案例告诉我们递归调用何时停止,否则它将无限循环。

因为我在数学方面有很强的背景,对我来说,这是一个很好的学习例子,是阶乘的。通过下面的评论,似乎阶乘函数可能有点太多,我将其保留在此处,以防万一您需要它。

function fact($n) {
  if ($n === 0) { // our base case
     return 1;
  }
  else {
     return $n * fact($n-1); // <--calling itself.
  }
}

关于在Web开发中使用递归函数,我个人不求助于使用递归调用。并不是说依赖于递归是不好的做法,但是它们不应该是您的首选。如果使用不当,它们可能会致命。

尽管我无法与目录示例竞争,但我希望这有所帮助。

(10/4/20)更新:

检验这个问题,这也是很有用的,其中公认的答案以通俗易懂的方式演示了递归函数的工作原理。即使OP的问题与Java有关,但概念是相同的,


5
尽管您的示例很好地解释了该示例,但从外行角度来讲,几乎不能认为该示例是示例。如果OP在基于Fibonacci示例的情况下理解递归时遇到一些麻烦,我认为阶乘也不会削减它。特别是涉及这些数学定义。
体面的摔角手

@ fireeyedboy,stereofrog:在大学里,我学会递归时要学习阶乘函数,这让我点击了。我和OP可能有不同的数学背景,但为简洁起见,我省略了阶乘定义,并强调了递归的递归定义。
Anthony Forloney 2010年

不需要为函数定义一个结束条件(基本情况),这会使您的定义有些不正确。(尽管在PHP中,您还必须否则会导致堆栈溢出)
Yacoby 2010年

30

一个示例是在给定目录的任何子目录中打印每个文件(如果这些目录中没有符号链接,则可能以某种方式破坏该功能)。打印所有文件的伪代码如下所示:

function printAllFiles($dir) {
    foreach (getAllDirectories($dir) as $f) {
        printAllFiles($f); // here is the recursive call
    }
    foreach (getAllFiles($dir) as $f) {
        echo $f;
    }
}

这个想法是先打印所有子目录,然后再打印当前目录的文件。这个想法适用于所有子目录,这就是为所有子目录递归调用此函数的原因。

如果要尝试此示例,则必须检查特殊目录...,否则您将printAllFiles(".")始终无法进行调用。此外,您必须检查打印内容和当前的工作目录是什么(见opendir()getcwd()...)。


2
我喜欢这个答案而不是阶乘答案,因为它确实适用。实际需要多少人来计算阶乘,以及为什么您仍要递归进行阶乘(也许是记忆化,但在那一点上,为什么不只是从一开始就使用查找表)。
davidtbernal

23

递归是重复的。就像从内部调用自身的函数一样。让我通过一个伪示例进行演示:

想象一下,您与好友一起喝啤酒,但是如果您在午夜之前不回家,您的妻子将使您下地狱。为此,让我们创建orderAndDrinkBeer($ time)函数,其中$ time是午夜减去您喝完当前饮料并回家所需的时间。

因此,到达酒吧后,您订购了第一杯啤酒并开始饮用:

$timeToGoHome = '23';  // Let's give ourselves an hour for last call and getting home

function orderAndDrinkBeer($timeToGoHome) {  // Let's create the function that's going to call itself.
    $beer = New Beer();  // Let's grab ourselves a new beer
    $currentTime = date('G'); // Current hour in 24-hour format

    while ($beer->status != 'empty') {  // Time to commence the drinking loop
        $beer->drink();  // Take a sip or two of the beer(or chug if that's your preference)
    }

    // Now we're out of the drinking loop and ready for a new beer

    if ($currentTime < $timeToGoHome) { // BUT only if we got the time
        orderAndDrinkBeer($timeToGoHome);  // So we make the function call itself again!
    } else {  // Aw, snap!  It is time :S
        break; // Let's go home :(
    }
}

现在,我们只是希望您不能喝足够多的啤酒来陶醉,以至于妻子不准时回家就让您睡在沙发上-.-

但是,是的,这几乎就是递归进行的方式。


10年后的今天,这个答案已成为我的梦想!
Professorval

9

它是一个自我调用的函数。它对于遍历某些重复出现的数据结构(例如树)很有用。HTML DOM是一个经典示例。

javascript中树结构的示例以及“遍历”树的递归函数。

    1
   / \
  2   3
     / \
    4   5

-

var tree = {
  id: 1,
  left: {
    id: 2,
    left: null,
    right: null
  },
  right: {
    id: 3,
    left: {
      id: 4,
      left: null,
      right: null
    },
    right: {
      id: 5,
      left: null,
      right: null
    }
  }
};

为了遍历树,我们反复调用相同的函数,将当前节点的子节点传递给相同的函数。然后,我们再次调用该函数,首先在左侧节点上,然后在右侧上。

在此示例中,我们将获得树的最大深度

var depth = 0;

function walkTree(node, i) {

  //Increment our depth counter and check
  i++;
  if (i > depth) depth = i;

  //call this function again for each of the branch nodes (recursion!)
  if (node.left != null) walkTree(node.left, i);
  if (node.right != null) walkTree(node.right, i);

  //Decrement our depth counter before going back up the call stack
  i--;
}

最后我们调用函数

alert('Tree depth:' + walkTree(tree, 0));

理解递归的一种好方法是在运行时逐步遍历代码。


8

简而言之:递归函数是一个调用自身的函数。


5

很简单,当一个函数调用自己以完成一段不确定的有限时间的任务时。我自己的代码中的一个示例,该函数用于填充多级类别树

函数category_tree($ parent = 0,$ sep ='')
{
    $ q =“选择ID,类别中的名称,其中parent_id =”。$ parent;
    $ rs = mysql_query($ q);
    while($ rd = mysql_fetch_object($ rs))
    {
        echo('id。'“>'。$ sep。$ rd-> name。'');
        category_tree($ rd-> id,$ sep .'--');
    }
}

4

递归是一种“重复做直到完成”的奇特方式。

有两个重要的事情:

  1. 一个基本案例-您有一个目标。
  2. 测试-如何知道您是否要去往哪里。

想象一个简单的任务:按字母顺序对一堆书进行排序。一个简单的过程就是将前两本书整理一下。现在,这是递归部分:还有更多书吗?如果是这样,请再做一次。“再次执行”是递归。“还有书吗”就是考验。而“没有,没有更多的书”是基本情况。


2

当我了解自己的时候,我已经找到了最好的解释:http : //www.elated.com/articles/php-recursive-functions/

这是因为一件事:

该函数在调用时在内存中创建(创建新实例)

因此,递归函数不会调用自己,而是调用其他实例-因此它不是内存中的一个函数在发挥作用。它在内存中的几个实例正在返回自身一些值-例如,当函数a调用函数b时,此行为是相同的。您有两个实例,以及递归函数是否称为自身的新实例。

尝试在纸上用实例绘制内存-这将是有意义的。


1

递归是循环的一种替代方法,很少使循环为您的代码带来更多的清晰度。Progman的答案给出了一个很好的例子,如果他不使用递归,他将被迫跟踪他当前所在的目录(这称为状态)。递归允许他使用堆栈(变量所在的区域)进行簿记。并存储方法的返回地址)

标准示例阶乘和斐波那契对理解概念没有用,因为它们很容易被循环替换。


1

基本上是这样。它会不断自我调用直到完成

void print_folder(string root)
{
    Console.WriteLine(root);
    foreach(var folder in Directory.GetDirectories(root))
    {
        print_folder(folder);
    }
}

也适用于循环!

void pretend_loop(int c)
{
    if(c==0) return;
    print "hi";
    pretend_loop(c-);
}

您也可以尝试谷歌搜索。注意“您的意思是您的意思”(单击它...)。http://www.google.com/search?q=recursion&spell=1


错字...(您的第一个函数的参数列表的结尾括号在哪里?)
Ponkadoodle 2010年

1

这是一个实际的例子(已经有好几个例子了)。我只想添加一个对几乎所有开发人员都有用的功能。

在某个时候,开发人员将需要解析对象,以及API或某种类型的对象或数组的响应。

最初调用此函数来分析可能仅包含参数的对象,但是如果该对象还包含其他对象或数组怎么办?这将需要解决,并且在大多数情况下,基本功能已经做到了,因此该功能只是再次调用自身(在确认键或值是对象还是数组之后)并解析此新对象或数组。最终返回的是一个字符串,该字符串可以在一行上自行创建每个参数以提高可读性,但是您可以轻松地将值记录到日志文件中或插入到DB或其他任何东西中。

我添加了$prefix参数以使用父元素来帮助描述end变量,以便我们可以看到该值的含义。它不包含null值之类的东西,但是可以通过此示例进行修改。

如果有对象:

$apiReturn = new stdClass();
$apiReturn->shippingInfo = new stdClass();
$apiReturn->shippingInfo->fName = "Bill";
$apiReturn->shippingInfo->lName = "Test";
$apiReturn->shippingInfo->address1 = "22 S. Deleware St.";
$apiReturn->shippingInfo->city = "Chandler";
$apiReturn->shippingInfo->state = "AZ";
$apiReturn->shippingInfo->zip = 85225;
$apiReturn->phone = "602-312-4455";
$apiReturn->transactionDetails = array(
    "totalAmt" => "100.00",
     "currency" => "USD",
     "tax" => "5.00",
     "shipping" => "5.00"
);
$apiReturn->item = new stdClass();
$apiReturn->item->name = "T-shirt";
$apiReturn->item->color = "blue";
$apiReturn->item->itemQty = 1;

并使用:

var_dump($apiReturn);

它会返回:

object(stdClass)#1(4){[“” shippingInfo“] => object(stdClass)#2(6){[” fName“] => string(4)” Bill“ [” lName“] => string( 4)“ Test” [“ address1”] =>字符串(18)“ 22 S. Deleware St.” [“ city”] =>字符串(8)“ Chandler” [“ state”] =>字符串(2)“ AZ” [“ zip”] => int(85225)} [“ phone”] =>字符串(12 )“ 602-312-4455” [“ transactionDetails”] => array(4){[“” totalAmt“] => string(6)” 100.00“ [” currency“] => string(3)” USD“ [”税“] => string(4)” 5.00“ [” shipping“] => string(4)” 5.00“} [” item“] => object(stdClass)#3(3){[” name“] = >字符串(7)“ T恤” [“颜色”] =>

这是将其解析为每个参数都带有换行符的字符串的代码:

function parseObj($obj, $prefix = ''){
    $stringRtrn = '';
    foreach($obj as $key=>$value){
        if($prefix){
            switch ($key) {
                case is_array($key):
                    foreach($key as $k=>$v){
                        $stringRtrn .= parseObj($key, $obj);
                    }
                    break;
                case is_object($key):
                    $stringRtrn .= parseObj($key, $obj);
                    break;
                default:
                    switch ($value) {
                        case is_array($value):
                            $stringRtrn .= parseObj($value, $key);
                            break;
                        case is_object($value):
                            $stringRtrn .= parseObj($value, $key);
                            break;
                        default:
                            $stringRtrn .= $prefix ."_". $key ." = ". $value ."<br>";
                            break;
                    }
                    break;
            }
        } else { // end if($prefix)
            switch($key){
                case is_array($key):
                    $stringRtrn .= parseObj($key, $obj);
                    break;
                case is_object($key):

                    $stringRtrn .= parseObj($key, $obj);
                    break;
                default:
                    switch ($value) {
                        case is_array($value):
                            $stringRtrn .= parseObj($value, $key);
                            break;
                        case is_object($value):
                            $stringRtrn .= parseObj($value, $key);
                            break;                      
                        default:
                            $stringRtrn .= $key ." = ". $value ."<br>";
                            break;
                    } // end inner switch 
            } // end outer switch
        } // end else
    } // end foreach($obj as $key=>$value)
    return $stringRtrn;
} // END parseObj()

这将返回对象,如下所示:

shippingInfo_fName = Bill
shippingInfo_lName = Test
shippingInfo_address1 = 22 S. Deleware St.
shippingInfo_city = Chandler
shippingInfo_state = AZ
shippingInfo_zip = 85225
phone = 602-312-4455
transactionDetails_totalAmt = 100.00
transactionDetails_currency = USD
transactionDetails_tax = 5.00
transactionDetails_shipping = 5.00
item_name = T-shirt
item_color = blue
item_itemQty = 1

为了避免与混淆,我做了嵌套的switch语句if . . . ifelse . . . else,但是它几乎一样长。如果有帮助,请询问是否有条件,我可以将它们粘贴给需要的人。


0

遍历目录树是一个很好的例子。您可以执行类似的操作来处理数组。这是一个非常简单的递归函数,它仅处理一个字符串,一个简单的字符串数组或任何深度的字符串嵌套数组,将字符串中的“ hello”的实例替换为字符串中的“再见”或该数组的值或任何其他值子数组:

function replaceHello($a) {
    if (! is_array($a)) {
        $a = str_replace('hello', 'goodbye', $a);
    } else {
        foreach($a as $key => $value) {
            $a[$key] = replaceHello($value);
        }
    }
    return $a
}

它知道何时退出,因为在某些时候,它正在处理的“事物”不是数组。例如,如果调用replaceHello('hello'),它将返回“再见”。如果您向它发送一个字符串数组,尽管它将为该数组的每个成员调用一次自身,然后返回处理后的数组。


0

如果您在Anthony Forloney的示例中添加某个值(例如“ 1”),那么一切将变得很清楚:

function fact(1) {
  if (1 === 0) { // our base case
  return 1;
  }
  else {
  return 1 * fact(1-1); // <--calling itself.
  }
}

原版的:

function fact($n) {
  if ($n === 0) { // our base case
    return 1;
  }
  else {
  return $n * fact($n-1); // <--calling itself.
  }
}

你如何阻止它?
Pathros

0

这是带有递归的阶乘的非常简单的示例:

阶乘是一个非常简单的数学概念。他们写成5!这意味着5 * 4 * 3 * 2 *1。所以6!是720和4!是24。

function factorial($number) { 

    if ($number < 2) { 
        return 1; 
    } else { 
        return ($number * factorial($number-1)); 
    } 
}

希望这对您有用。:)


你如何阻止它?
Pathros

1
在阶乘中,我们不断调用直到传递的参数为1且当参数为1时我们仅返回1即可停止递归


0

用于Kaprekar常数的递归

function KaprekarsConstant($num, $count = 1) {
    $input = str_split($num);
    sort($input);

    $ascendingInput  = implode($input);
    $descendingInput = implode(array_reverse($input));

    $result = $ascendingInput > $descendingInput 
        ? $ascendingInput - $descendingInput 
        : $descendingInput - $ascendingInput;

    if ($result != 6174) {
        return KaprekarsConstant(sprintf('%04d', $result), $count + 1);
    }

    return $count;

}

该函数将继续使用计算结果调用自身,直到达到Kaprekars常数为止,该常数将返回进行计算的次数。

/ edit对于不了解Kaprekars Constant的任何人,它需要输入4位数字,至少要包含两个不同的数字。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.