PHP中的递归生成器

时间:2013-11-07 12:06:18

标签: php recursion generator

简介

从PHP 5.5版开始,就像generators一样出色。我不会重复官方手册页,但它们对于迭代器的简短定义是很好的。最着名的样本是:

function xrange($from, $till, $step)
{
   if ($from>$till || $step<=0)
   {
      throw new InvalidArgumentException('Invalid range initializers');
   }

   for ($i = $from; $i < $till; $i += $step)
   {
      yield $i;
   }
}

//...

foreach (xrange(2, 13, 3) as $i)
{
   echo($i.PHP_EOL); // 2,5,8,11
}

和生成器实际上不是一个函数,而是一个具体类的实例:

get_class(xrange(1, 10, 1)); // Generator


问题

完成RTM的东西,现在继续我的问题。想象一下,我们想要创建Fibonacci numbers的生成器。通常,为了得到这些,我们可以使用简单的函数:

function fibonacci($n)
{
   if(!is_int($n) || $n<0)
   {
      throw new InvalidArgumentException('Invalid sequence limit');
   }
   return $n < 2 ? $n : fibonacci($n-1) + fibonacci($n-2);
}

var_dump(fibonacci(6)); // 8

让我们把它变成一些东西,它包含 sequence 而不仅仅是它的最后一个成员:

function fibonacci($n)
{
   if (!is_int($n) || $n<0)
   {
      throw new InvalidArgumentException('Invalid sequence limit');
   }
   if ($n<2)
   {
      return range(0, $n);
   }
   $n1 = fibonacci($n-1);
   $n2 = fibonacci($n-2);
   return array_merge($n1, [array_pop($n1)+array_pop($n2)]);
}

//...

foreach (fibonacci(6) as $i)
{
   echo($i.PHP_EOL); // 0,1,1,2,3,5,8
}

我们现在有一个函数返回带有完整序列的数组


问题

最后,问题部分:我如何转换我最新的fibonacci函数,以便产生我的值,而不是将它们保存在数组中?我的$n可能很大,所以我想使用生成器的好处,例如xrange示例。伪代码将是:

function fibonacci($n)
{
   if (!is_int($n) || $n<0)
   {
      throw new InvalidArgumentException('Invalid sequence limit');
   }

   if ($n<2)
   {
      yield $n;
   }

   yield fibonacci($n-2) + fibonacci($n-1);
}

但显然这是废话,因为我们无法像这样处理它,因为递归会导致类Generator的对象而不是int值。

奖金:获取斐波纳契序列只是一个更普遍问题的样本:如何在常见情况下使用带递归的生成器?当然,我可以使用标准Iterator或重写我的函数以避免递归。但我想用发电机实现这一目标。这可能吗?这是否值得努力以这种方式使用?

8 个答案:

答案 0 :(得分:14)

因此,在尝试创建递归生成器函数时遇到的问题是,一旦超过第一个深度级别,每个后续的yield都会屈服于其父调用而不是迭代实现(循环)。

从php 7开始,添加了一项新功能,允许您产生后续生成器功能。这是新的生成器委派功能:https://wiki.php.net/rfc/generator-delegation

这允许我们从后续的递归调用中产生,这意味着我们现在可以使用生成器有效地编写递归函数。

$items = ['what', 'this', 'is', ['is', 'a', ['nested', 'array', ['with', 'a', 'bunch',  ['of', ['values']]]]]];

function processItems($items)
{
    foreach ($items as $value)
    {
        if (is_array($value))
        {
            yield from processItems($value);
            continue;
        }
        yield $value;
    }
}

foreach (processItems($items) as $item)
{
    echo $item . "\n";
}

这给出了以下输出..

what
this
is
is
a
nested
array
with
a
bunch
of
values

答案 1 :(得分:10)

我终于确定了递归生成器的真实用途。

我最近一直在探索QuadTree数据结构。对于那些不熟悉QuadTrees的人来说,他们是基于树的数据结构用于地理空间索引,并允许快速搜索定义的边界框内的所有点/位置。 QuadTree中的每个节点代表映射区域的一部分,并充当存储位置的存储桶......但是存储大小受限的存储桶。当存储桶溢出时,QuadTree节点会分割出4个子节点,分别代表父节点的西北,东北,西南和东南地区,并开始填充这些节点。

搜索指定边界框内的位置时,搜索例程从顶级节点开始,测试该存储桶中的所有位置;然后递归到子节点,测试它们是否与边界框相交,或者是否被边界框包围,测试该集合中的每个QuadTree节点,然后再次向下再次通过树。每个节点可以返回任何一个或多个位置。

我在PHP中实现了一个基本的QuadTree,旨在返回一个结果数组;然后意识到它可能是一个递归生成器的有效用例,所以我实现了一个GeneratorQuadTree,可以在foreach()循环中访问,每次迭代产生一个结果。

对于递归生成器来说,这似乎是一个更有效的用例,因为它是一个真正的递归搜索函数,并且因为每个生成器可能不返回任何一个或多个结果而不是单个结果。实际上,每个嵌套的生成器都在处理搜索的一部分,通过其父级将结果反馈到树中。

这里的代码太多了,无法发布;但您可以查看github上的实施。

它比非发电机版本慢得多(但并不显着):主要好处是减少了内存,因为它不是简单地返回一个可变大小的数组(这可能是一个重要的好处取决于返回的结果数量)。最大的缺点是结果不能轻易排序(我的非生成器版本在结果数组返回后会对其进行usort())。

答案 2 :(得分:2)

function fibonacci($n)
{
    if($n < 2) {
        yield $n;
    }

    $x = fibonacci($n-1);
    $y = fibonacci($n-2);
    yield $x->current() + $y->current();
}


for($i = 0; $i <= 10; $i++) {
    $x = fibonacci($i);
    $value = $x->current();
    echo $i , ' -> ' , $value, PHP_EOL;
}

答案 3 :(得分:2)

如果您首先要制作一个生成器,您也可以使用斐波那契的迭代版本:

function fibonacci ($from, $to)
{
  $a = 0;
  $b = 1;
  $tmp;
  while( $to > 0 ) {
    if( $from > 0 )
      $from--;
    else
      yield $a;

    $tmp = $a + $b;
    $a=$b;
    $b=$tmp;
    $to--;
  }
}

foreach( fibonacci(10,20) as $fib ) {  
    print "$fib "; // prints "55 89 144 233 377 610 987 1597 2584 4181 " 
}

答案 4 :(得分:0)

这是一个用于组合的递归生成器(命令不重要,无需替换):

<?php

function comb($set = [], $size = 0) {
    if ($size == 0) {
        // end of recursion
        yield [];
    }
    // since nothing to yield for an empty set...
    elseif ($set) {
        $prefix = [array_shift($set)];

        foreach (comb($set, $size-1) as $suffix) {
            yield array_merge($prefix, $suffix);
        }

        // same as `yield from comb($set, $size);`
        foreach (comb($set, $size) as $next) {
            yield $next;
        }
    }
}

// let's verify correctness
assert(iterator_to_array(comb([0, 1, 2, 3, 4], 3)) == [
    [0, 1, 2], [0, 1, 3], [0, 1, 4], [0, 2, 3], [0, 2, 4],
    [0, 3, 4], [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]
]);

foreach (comb([0, 1, 2, 3], 3) as $combination) {
    echo implode(", ", $combination), "\n";
}

输出:

0, 1, 2
0, 1, 3
0, 2, 3
1, 2, 3

Same thing non-yielding.

答案 5 :(得分:0)

最近遇到了需要'递归'生成器或生成器委派的问题。我最后写了一个小函数,将委托的生成器调用转换为单个生成器。

我把它变成了一个包,所以你可以用作曲家来要求它,或者在这里查看来源:hedronium/generator-nest

代码:

function nested(Iterator $generator)
{
    $cur = 0;
    $gens = [$generator];

    while ($cur > -1) {
        if ($gens[$cur]->valid()) {
            $key = $gens[$cur]->key();
            $val = $gens[$cur]->current();
            $gens[$cur]->next();
            if ($val instanceof Generator) {
                $gens[] = $val;
                $cur++;
            } else {
                yield $key => $val;
            }
        } else {
            array_pop($gens);
            $cur--;
        }
    }
}

你可以像使用它一样:

foreach (nested(recursive_generator()) as $combination) {
    // your code
}

检查上面的链接。它有例子。

答案 6 :(得分:0)

简短回答:递归生成器很简单。步行穿过树的示例:

class Node {

    public function getChildren() {
        return [ /* array of children */ ];
    }

    public function walk() {
        yield $this;

        foreach ($this->getChildren() as $child) {
            foreach ($child->walk() as $return) {
                yield $return;
            };
        }
    }
}

一切都是。

关于斐波纳契的长篇答案:

生成器与foreach (generator() as $item) { ... }一起使用。但是OP希望fib()函数返回int,但同时他希望它返回generator以在foreach中使用。这非常令人困惑。

可以为斐波纳契实现递归生成器解决方案。我们只需要在fib()函数内放置一个循环,它确实yield序列的每个成员。由于生成器应该与foreach一起使用,它看起来真的很奇怪,我不认为它是有效的,但它在这里:

function fibGenerator($n) {
    if ($n < 2) {
        yield $n;
        return;
    }

    // calculating current number
    $x1 = fibGenerator($n - 1);
    $x2 = fibGenerator($n - 2);
    $result = $x1->current() + $x2->current();

    // yielding the sequence
    yield $result;
    yield $x1->current();
    yield $x2->current();

    for ($n = $n - 3; $n >= 0; $n--) {
        $res = fibGenerator($n);
        yield $res->current();
    }
}

foreach (fibGenerator(15) as $x) {
    echo $x . " ";
}

答案 7 :(得分:-1)

我提供两种Fibonacci数的解决方案,有和没有递归:

function fib($n)
{
    return ($n < 3) ? ($n == 0) ? 0 : 1 : fib($n - 1) + fib($n - 2);
}
function fib2()
{
    $a = 0;
    $b = 1;
    for ($i = 1; $i <= 10; $i++)
    {
        echo $a  . "\n";
        $a = $a + $b;
        $b = $a - $b;
    }
}

for ($i = 0; $i <= 10; $i++)
{
    echo fib($i) . "\n";
}

echo fib2();