递归消耗完内存_递归生成器,以及如何不消耗它们的所有内存

递归消耗完内存

A short while back I wrote a post touching upon combinatorics. Part of the code of that article used a Combinator object, which generated combinations of choices and stored them in an array.

不久前, 我写了一篇关于组合语言的文章。 该文章的部分代码使用了Combinator对象,该对象生成选择的组合并将它们存储在数组中。

The problem with combinatorial operations is that the number of combinations can grow explosively fast with every additional choice added — greater than exponentially fast, in some cases.

组合操作的问题在于,添加了每个其他选择后,组合的数量可能会爆炸性地增长 -在某些情况下,会比指数级地增长。

If I have three items and allow 0, 1, 2, or 3 of those to be chosen, I get 8 unique choices if I disregard order, allow no repeats and include the null set. Double that to six items and you wind up with 64 choices (8*8). Double that again (12 items), there are 4096 choices (64*64). In this case, with the restrictions noted above, the number of combinations is 2 to the power of n choices, so it grows merely(!) exponentially.

如果我有三个项目,并允许选择其中的0、1、2或3个项目,那么如果我不考虑订单,不重复,不包含null集,则会得到8个唯一选择。 将其加倍至六项,最后有64种选择(8 * 8)。 再次翻倍(12项),有4096个选项(64 * 64)。 在这种情况下,由于上述限制,组合的数量是n个选择的幂的2,因此它仅呈指数增长。

For a large number of items, storing every combination in an array could lead to memory exhaustion. Instead of having the Combinator return an array only after all combinations have been generated, how about if it returned each combo one-by-one, as needed? Since the Combinator is generating combinations, can it be converted to a generator?

对于大量项目,将每个组合存储在一个数组中可能会导致内存耗尽。 而不是让Combinator仅在生成所有组合之后才返回数组,而是根据需要将每个组合一个接一个地返回怎么办? 由于组合器正在生成组合,因此可以将其转换为生成器吗?

原始的Combinator.js (Original Combinator.js)

In the original code, every combination created by calling combine() is stored in a combinations array:

在原始代码中,通过调用combin()创建的每个组合都存储在组合数组中:

var Combinator = function (opts) {
    var combinations = [];

    function combine(current, remainder) {
        if (remainder.length === 0) {
            if (current.length >= (opts.min || 0) &&
                current.length <= (opts.max || current.length))
                combinations.push(current);
        } else {
            combine(current.concat(remainder[0]), remainder.slice(1, remainder.length));
            combine(current, remainder.slice(1, remainder.length));
        }
        return this;
    }
    return {
        combinations: combinations,
        combine: combine
    }
}

module.exports = Combinator;

The algorithm is embellished a bit with the addition of min/max options — these limit the number of combinations that contain at least min, and at most max, elements. I can be used like so:

最小/最大选项的添加使该算法更加美观-这些限制了至少包含minmax元素的组合的数量。 我可以这样使用:

var menu = {
   threeItems: {
        min: 0,
        max: 3,
        values: [1, 2, 3]
    }
}

var threeCombos = new Combinator({
            min: menu.threeItems.min,
            max: menu.threeItems.max
        })
        .combine([], menu.threeItems.values)
        .combinations;

The menu.threeItems.values property has (surprise!) three values. The min and max properties determine the set of combinations to be generated. In this case, we ask for sets from 0 length (the null set) to full length (the entire values set). Remember that we’re not interested in order, nor do we allow duplicates. Let see it in action:

menu.threeItems.values属性具有(惊奇!)三个值。 最小最大属性确定要生成的组合的集合。 在这种情况下,我们要求从0长度(空集)到全长(整个值集)的集合。 请记住,我们对顺序不感兴趣,也不允许重复。 让我们来看看它的作用:

console.log('threeCombos.length =', threeCombos.length, threeCombos);

-- output --

threeCombos.length = 8 [ [ 1, 2, 3 ], [ 1, 2 ], [ 1, 3 ], [ 1 ], [ 2, 3 ], [ 2 ], [ 3 ], [] ]

Now, instead of using an array to store all combinations, let’s convert this bit of JavaScript to use the new ES6 generator functionality. A generator is a stateful function that yields values one-by-one, in an iterative fashion.

现在,让我们将这部分JavaScript转换为使用新的ES6生成器功能,而不是使用数组存储所有组合。 生成器是一个有状态函数,它以迭代方式逐一生成值。

天真的尝试 (Naive attempt)

A generator function is declared using function* instead of function. The yield operator is called within the generator function to return single values back to the caller. The generator remembers the state of the previous call, so subsequent yields will return the next logical value. The caller uses the next() method to get each subsequent value from the generator function. No arrays required!

生成器函数是使用function *而不是function声明的。 在生成器函数中调用yield运算符,以将单个值返回给调用者。 生成器会记住上一个调用的状态,因此后续的yield s 将返回下一个逻辑值。 调用者使用next()方法从生成器函数获取每个后续​​值。 无需数组!

I can be pretty lazy at times, so I took the tl;dr approach to the JavaScript documentation on generators and just winged it. The first attempt was:

有时我可能会很懒惰,所以我将tl; dr方法应用于生成器上JavaScript 文档 ,并对其进行了改进。 第一次尝试是:

var CombinatorGenerator = function (opts) {
    function* combine(current, remainder) {
        if (remainder.length === 0) {
            if (current.length >= (opts.min || 0) &&
                current.length <= (opts.max || current.length)) {
                yield(current);
            }
        } else {
            combine(current.concat(remainder[0]), remainder.slice(1, remainder.length))
            combine(current, remainder.slice(1, remainder.length))
        }
    }
    return {
        combine: combine
    }
}

This makes sense, right? Instead of pushing a set of choices to an array, I just yield a value. In the client code, I keep calling next() until the generator tells me it’s done.

这很有道理吧? 而不是将一组选择推入数组,我只是产生一个值。 在客户端代码中,我一直调用next(),直到生成器告诉我完成为止。

var menu = require('./menu');
var Combinator = require('./Combinator-generator-naive');

function run() {
    var threeCombos = new Combinator({
            min: menu.threeItems.min,
            max: menu.threeItems.max
        })
        .combine([], menu.threeItems.values);

    for (;;) {
        var it = threeCombos.next();
        if (it.done) {
            console.log("done!")
            break;
        }
        console.log("choice", it.value);
    }
}

run();

Alas, my hopes were dashed. The output is:

las,我的希望破灭了。 输出为:

PS C:\Users\Jeff\workspace\Generator> node .\test-generated.js

done!

Alright, so obviously the new Combinator is returning before the first yield does, so we’re “done!” before we’re actually done.

好吧,很明显,新的Combinator在第一个收益率之前就已经返回了,所以我们“完成了!” 在我们实际完成之前。

直觉尝试 (Intuitive attempt)

Still loathe to read documentation, I next try to intuit the bug fix. So what happens if I just yield from the internal combine calls — logical, no? Instead of:

仍然不愿阅读文档,接下来我尝试了解该错误修复。 那么,如果我只是从内部合并调用中屈服,那会发生什么—逻辑,不是吗? 代替:

} else {
            combine(current.concat(remainder[0]), remainder.slice(1, remainder.length))
            combine(current, remainder.slice(1, remainder.length))
        }

I try yielding from the recursive calls:

我尝试从递归调用中屈服:

} else {
   yield combine(current.concat(remainder[0]), remainder.slice(1, remainder.length)).next()
   yield combine(current, remainder.slice(1, remainder.length)).next()
}

Truly, this will work. So let’s run it:

确实,这会起作用。 因此,让我们运行它:

PS C:\Users\Jeff\workspace\Generator> node .\generated.js
choice { value: { value: { value: [Object], done: false }, done: false },
  done: false }
choice { value: { value: { value: [Object], done: false }, done: false },
  done: false }
done!

Hmmm…that’s no good — what gets returned are the recursive generators’ state, but not the actual values from the yield operations.

嗯……那不好-返回的是递归生成器的状态,而不是yield操作的实际值。

体贴的尝试 (Thoughtful attempt)

Okay, time to buckle down. A little googling on “recursive generator” turns up a reference to Python’s yield from. That syntax delegates the yield calls to another generator. Is there an equivalent in JavaScript?

好吧,时间到了。 仔细搜索“递归生成器”,就可以发现对Python的收益的引用 该语法将yield调用委托给另一个生成器。 JavaScript是否有等效功能?

Yes! — and it’s the yield* syntax. This is actually in the document link about generators; had I read it, I possibly would have figured this out sooner (laziness, like crime, doesn’t [always] pay). The correct syntax is:

是! —这是yield *语法。 这实际上是关于生成器的文档链接; 如果我读了它,我可能会早点弄清楚的(懒惰,像犯罪一样,并不会(总是)付钱)。 正确的语法是:

} else {
            yield* combine(current.concat(remainder[0]), remainder.slice(1, remainder.length))
            yield* combine(current, remainder.slice(1, remainder.length))
        }

And now, when I call the combine method, I see:

现在,当我调用Combine方法时,我看到:

node .\generated.js
choice [ 1, 2, 3 ]
choice [ 1, 2 ]
choice [ 1, 3 ]
choice [ 1 ]
choice [ 2, 3 ]
choice [ 2 ]
choice [ 3 ]
choice []
done!

Good! I’m getting back all the combinations, one-by-one. Success!

好! 我要一一找回所有组合。 成功!

Full code used in this post can be found here. Happy generating!

这篇文章中使用的完整代码可以在这里找到。 生成愉快!

Update 2/26/2017

更新2/26/2017

After reading this article by the indefatigable Eric Elliott, I began to think I had traded one type of resource exhaustion (memory) for another (stack). However, I’ve run the Combinator with an input array of length 30 and it ran to completion: that’s 2³⁰ combinations generated (over a billion). Note that the algorithm

在阅读了不知疲倦的埃里克·埃利奥特(Eric Elliott)的这篇文章之后,我开始认为我已经将一种类型的资源耗尽(内存)换成了另一种(堆栈)。 但是,我使用长度为30的输入数组来运行Combinator,并且运行完成:即生成了2³3个组合(超过10亿个)。 注意算法

  1. is not using tail recursion (or maybe it’s ‘split-tail’ recursion?); and

    不使用尾递归(或者可能是“尾尾递归”?); 和
  2. yield *, according to Eric’s article, should not be optimized as a tail recursive call in any case

    根据Eric的文章, yield *在任何情况下都不应优化为尾递归调用

Yet, it works. Proof can be found by running generated30.js in the git repository for this post.

但是,它有效。 可以通过在git存储库中运行post.30.js来找到证明。

翻译自: https://www.freecodecamp.org/news/recursive-generator-f8bc30e5e412/

递归消耗完内存

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值