【问题标题】:Recursive generators in PHPPHP中的递归生成器
【发布时间】:2013-11-19 01:44:13
【问题描述】:

简介

自 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 或者重写我的函数以避免递归。但我想用生成器来实现。这可能吗?这值得努力使用这种方式吗?

【问题讨论】:

  • 好问题。我真的很期待看到答案
  • 当你调用一个生成器时,你得到的是一个...生成器对象。该对象保存 state 并在被要求时返回和推进该状态。递归函数 OTOH 接受一个值并返回一个值。我看不出这两者如何相互兼容。等待进一步的意见。
  • @deceze 这也是我现在的想法。 IE。如果有足够的证据,“没办法做到这一点”也是很好的答案。
  • 我猜你可以通过足够的内省和多态行为来做到这一点,但你为什么要这样做呢?我认为很难证明它不可能,但很容易证明以非递归方式编写它要简单得多。 (证明:你甚至不知道从哪里开始。;))
  • .. 这也是问题的一部分:p即如果“这样做不值得”——那么我很高兴看到答案——为什么。我现在不知道,是的:/我真丢脸。我通常的 SO 问题总是有我自己的解决方案(即使它不是很好)

标签: php recursion generator


【解决方案1】:

因此,我在尝试创建递归生成器函数时遇到的问题是,一旦您超过了第一个深度级别,每个后续的 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

【讨论】:

  • 是的,知道该功能。 PHP 7 万岁,但是,在生产中它不会很快成为可能。当然,感谢您的洞察力
  • 在 PHP5 上我们可以解决这个问题,在这里查看我类似的 sn-p:pastebin.com/00x0DXBG
【解决方案2】:

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

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

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

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

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

代码太多了,这里就不贴了;但您可以查看github 上的实现。

它比非生成器版本慢一点(但不显着):主要好处是减少内存,因为它不仅仅是返回可变大小的数组(根据结果的数量,这可能是一个显着的好处回来)。最大的缺点是结果不容易排序(我的非生成器版本在返回结果数组后对结果数组执行了 usort())。

【讨论】:

  • 非常有趣。这比我想要的还要多。
  • 偶然发现它比设计更偶然:当我与生成器交谈的人询问它们是否必须是函数或可以是类方法时,我已经编写了返回数组的基本 QuadTree 代码....由于我没有答案,并且那天早些时候一直在研究 QuadTrees,我决定尝试 search();当我看到它是如何工作的时,我意识到它也是递归的,并记住了这个问题。
  • 我还在 PHP 中实现了一个 Trie 数据结构,这与生成器的工作方式相同;所以我会在适当的时候将它的一个版本发布到 Github ......它的优点是它会自动按键排序,因为在读取数据结构时遍历 trie 的方式
  • 找到递归生成器可以证明有用的真实案例绝对不容易;但是,虽然 Tries 或 QuadTrees 是为非常特定的用例而设计的,但遍历任何树结构可能是递归生成器可能具有真正价值的领域。
【解决方案3】:
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;
}

【讨论】:

  • 嗯。那好多了。 IE。我们可以依靠current(),对吧?
  • 虽然这在技术上确实将递归函数与生成器结合在一起,但它有点否定了生成器的使用,不是吗? :)
  • @deceze - 简单地回答如何使用生成器的问题,就好像它是一个简单的递归函数:) 我不评论这样做的适当性
  • @deceze 是的,我注意到了。用这种方式我不能像foreach(..)那样做——但这至少是一个起点
  • 我在出去喝咖啡的时候一直在思考这个问题。可能的用途:蛮力迷宫寻路器。生成器被赋予地图、起始节点/点、行进方向和目标节点/点;它返回它所做的任何重要发现 - 转弯、交叉点、死角(null 终止) - 当它遇到一个交叉点时,它自己采用一个方向并产生其他生成器以遵循其他可能的方向。当然有更好的方法来做到这一点。
【解决方案4】:

如果你首先想制作一个生成器,你不妨使用斐波那契的迭代版本:

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 " 
}

【讨论】:

    【解决方案5】:

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

    <?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.

    【讨论】:

      【解决方案6】:

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

      我把它变成了一个包,所以你可以用 composer 来要求它,或者在这里查看源代码: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
      }
      

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

      【讨论】:

        【解决方案7】:

        简答:递归生成器很简单。穿过树的例子:

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

        就是这样。

        关于斐波那契的长答案:

        Generator 是与foreach (generator() as $item) { ... } 一起使用的东西。但是 OP 希望 fib() 函数返回 int,但同时他希望它返回 generator 以在 foreach 中使用。这很混乱。

        可以为斐波那契实现递归生成器解决方案。我们只需要在fib() 函数中放置一个循环,该循环实际上将yield 序列的每个成员。由于generator应该和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 . " ";
        }
        

        【讨论】:

          【解决方案8】:

          我为斐波那契数提供了两种解决方案,带递归和不带递归:

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-02-09
            • 2021-09-27
            • 2015-12-23
            • 2012-05-03
            • 2019-08-25
            • 2021-05-22
            • 2020-08-04
            • 2016-05-03
            相关资源
            最近更新 更多