简体   繁体   English

数组的懒惰笛卡尔积(任意嵌套循环)

[英]Lazy Cartesian product of arrays (arbitrary nested loops)

There are other questions about this in other languages , and other non-lazy JavaScript versions , but no lazy JavaScript versions that I have found. 其他 语言和其他非懒惰的 JavaScript版本中还有其他问题,但我找不到懒惰的JavaScript版本。

Given an array of an arbitrary number of arbitrary-sized arrays: 给定一个任意数量的任意大小数组的数组:

var sets = [ [2,3,4,5], ['sweet','ugly'], ['cats','dogs','hogs'] ];

and a callback function: 和回调函数:

function holla( n, adj, noun ){
  console.log( [n,adj,noun].join(' ') );
}

what's an elegant way to iterate the entire product space without creating a huge array of all possible combinations first ? 什么是优雅的方式来迭代整个产品空间而不首先创建大量所有可能的组合

lazyProduct( sets, holla );
// 2 sweet cats
// 2 sweet dogs
// 2 sweet hogs
// 2 ugly cats
// 2 ugly dogs
// 2 ugly hogs
// 3 sweet cats
// 3 sweet dogs
// 3 sweet hogs
// 3 ugly cats
// 3 ugly dogs
// 3 ugly hogs
// 4 sweet cats
// 4 sweet dogs
// 4 sweet hogs
// 4 ugly cats
// 4 ugly dogs
// 4 ugly hogs
// 5 sweet cats
// 5 sweet dogs
// 5 sweet hogs
// 5 ugly cats
// 5 ugly dogs
// 5 ugly hogs

Note that these combinations are the same as the results you would get if you had nested loops: 请注意,这些组合与嵌套循环时获得的结果相同:

var counts     = [2,3,4,5];
var adjectives = ['sweet','ugly'];
var animals    = ['cats','dogs','hogs'];
for (var i=0;i<counts.length;++i){
  for (var j=0;j<adjectives.length;++j){
    for (var k=0;k<animals.length;++k){
      console.log( [ counts[i], adjectives[j], animals[k] ].join(' ') );
    }
  }
}

The benefits of the Cartesian product are: 笛卡尔积的好处是:

  1. It lets you nest an arbitrary number of loops (perhaps you don't know how many items you'll iterate) 它允许你嵌套任意数量的循环(也许你不知道你会迭代多少项)
  2. It lets you change the order of looping (eg loop by adjectives first) without having to edit your code or write out all possible combinations of looping order. 它允许您更改循环的顺序(例如,首先通过形容词循环),而无需编辑代码或写出循环顺序的所有可能组合。

Benchmarks 基准

You can see the benchmarks for the answers below here: 您可以在此处查看以下答案的基准:
http://jsperf.com/lazy-cartesian-product/26 http://jsperf.com/lazy-cartesian-product/26

A combination of recursion and iteration will do the job. 递归和迭代的组合将完成这项工作。

function lazyProduct(sets, holla) {
    var setLength = sets.length;
    function helper(array_current, set_index) {
        if (++set_index >= setLength) {
            holla.apply(null, array_current);
        } else {
            var array_next = sets[set_index];
            for (var i=0; i<array_next.length; i++) {
                helper(array_current.concat(array_next[i]), set_index);
            }
        }
    }
    helper([], -1);
}

Demo: http://jsfiddle.net/nV2XU/ 演示: http//jsfiddle.net/nV2XU/

var sets = [ [2,3,4,5], ['sweet','ugly'], ['cats','dogs','hogs'] ];
function holla( n, adj, noun ){
  console.log( [n,adj,noun].join(' ') );
}

lazyProduct(sets,holla);

Here's my solution, using recursion. 这是我的解决方案,使用递归。 I'm not fond of the fact that it creates an empty array on the first pass, or that it uses the if inside the for loop (instead of unrolling the test into two loops for speed, at the expense of DRYness) but at least it's sort of terse: 我不喜欢它在第一次传递时创建一个空数组的事实,或者它在for循环中使用if (而不是将测试展开为两个循环以获得速度,但代价是DRYness)但至少这有点简洁:

function lazyProduct(arrays,callback,values){
  if (!values) values=[];
  var head = arrays[0], rest = arrays.slice(1), dive=rest.length>0;
  for (var i=0,len=head.length;i<len;++i){
    var moreValues = values.concat(head[i]);
    if (dive) lazyProduct(rest,callback,moreValues);
    else      callback.apply(this,moreValues);
  }
}

Seen in action: http://jsfiddle.net/RRcHN/ 见过: http//jsfiddle.net/RRcHN/


Edit : Here's a far faster version, roughly 2x–10x faster than the above: 编辑 :这是一个更快的版本,大约比上面快2倍-10倍

function lazyProduct(sets,f,context){
  if (!context) context=this;
  var p=[],max=sets.length-1,lens=[];
  for (var i=sets.length;i--;) lens[i]=sets[i].length;
  function dive(d){
    var a=sets[d], len=lens[d];
    if (d==max) for (var i=0;i<len;++i) p[d]=a[i], f.apply(context,p);
    else        for (var i=0;i<len;++i) p[d]=a[i], dive(d+1);
    p.pop();
  }
  dive(0);
}

Instead of creating custom arrays for each recursive call it re-uses a single array ( p ) for all params. 不是为每个递归调用创建自定义数组,而是为所有参数重新使用单个数组( p )。 It also lets you pass in a context argument for the function application. 它还允许您传递函数应用程序的上下文参数。


Edit 2 : If you need random access into your Cartesian product, including the ability to perform iteration in reverse, you can use this: 编辑2 :如果您需要随机访问笛卡尔积,包括反向执行迭代的功能,您可以使用:

function LazyProduct(sets){
  for (var dm=[],f=1,l,i=sets.length;i--;f*=l){ dm[i]=[f,l=sets[i].length] }
  this.length = f;
  this.item = function(n){
    for (var c=[],i=sets.length;i--;)c[i]=sets[i][(n/dm[i][0]<<0)%dm[i][1]];
    return c;
  };
};

var axes=[[2,3,4],['ugly','sappy'],['cats','dogs']];
var combos = new LazyProduct(axes);

// Iterating in reverse order, for fun and profit
for (var i=combos.length;i--;){
  var combo = combos.item(i);
  console.log.apply(console,combo);
}
//-> 4 "sappy" "dogs"
//-> 4 "sappy" "cats"
//-> 4 "ugly" "dogs"
...
//-> 2 "ugly" "dogs"
//-> 2 "ugly" "cats"

Decoding the above, the n th combination for the Cartesian product of arrays [a,b,...,x,y,z] is: 解码上述,阵列[a,b,...,x,y,z]的笛卡尔乘积的第n个组合是:

[
  a[ Math.floor( n / (b.length*c.length*...*y.length*z.length) ) % a.length ],
  b[ Math.floor( n / (c.length*...*x.length*y.length*z.length) ) % b.length ],
  ...
  x[ Math.floor( n / (y.length*z.length) ) % x.length ],
  y[ Math.floor( n / z.length ) % y.length ],
  z[ n % z.length ],
]

You can see a pretty version of the above formula on my website . 您可以在我的网站上看到上述公式的漂亮版本。

The dividends and moduli can be precalculated by iterating the sets in reverse order: 通过以相反的顺序迭代集合,可以预先计算红利和模数:

var divmod = [];
for (var f=1,l,i=sets.length;i--;f*=l){ divmod[i]=[f,l=sets[i].length] }

With this, looking up a particular combination is a simple matter of mapping the sets: 有了这个,查找特定组合只是映射集合的简单问题:

// Looking for combination n
var combo = sets.map(function(s,i){
  return s[ Math.floor(n/divmod[i][0]) % divmod[i][1] ];
});

For pure speed and forward iteration, however, see the accepted answer. 但是,对于纯粹的速度和前向迭代,请参阅接受的答案。 Using the above technique—even if we precalculate the list of dividends and moduli once—is 2-4x slower than that answer. 使用上述技术 - 即使我们预先计算一次股息和模数列表 - 比该答案慢2-4倍。

I've created this solution: 我创建了这个解决方案:

function LazyCartesianIterator(set) {
  var pos = null, 
      len = set.map(function (s) { return s.length; });

  this.next = function () {
    var s, l=set.length, p, step;
    if (pos == null) {
      pos = set.map(function () { return 0; });
      return true;
    }
    for (s=0; s<l; s++) {
      p = (pos[s] + 1) % len[s];
      step = p > pos[s];
      if (s<l) pos[s] = p;
      if (step) return true;
    }
    pos = null;
    return false;
  };

  this.do = function (callback) { 
    var s=0, l=set.length, args = [];
    for (s=0; s<l; s++) args.push(set[s][pos[s]]);
    return callback.apply(set, args);
  };
}

It's used like this: 它的使用方式如下:

var iter = new LazyCartesianIterator(sets);
while (iter.next()) iter.do(callback);

It seems to work well but it is not very thoroughly tested, tell me if you find bugs. 它似乎运行良好,但它没有经过彻底的测试,告诉我你是否发现了错误。

See how it compares: http://jsperf.com/lazy-cartesian-product/8 看看它如何比较: http//jsperf.com/lazy-cartesian-product/8

Coincidentally working on the same thing over the weekend. 巧合的是周末同样的事情。 I was looking to find alternative implementations to my [].every -based algo which turned out to have abyssmal performance in Firefox (but screams in Chrome -- more than twice as fast as the next). 我正在寻找替代我的[].every基础算法的替代实现,结果证明在Firefox中具有深度表现(但在Chrome中的尖叫声 - 比下一次快两倍)。

The end result is http://jsperf.com/lazy-cartesian-product/19 . 最终结果是http://jsperf.com/lazy-cartesian-product/19 It's similar to Tomalak's approach but there is only one arguments array which is mutated as the carets move instead of being generated each time. 它类似于Tomalak的方法,但是只有一个参数数组随着插入符移动而不是每次生成而变异。

I'm sure it could be improved further by using the clever maths in the other algos. 我相信通过在其他算法中使用聪明的数学可以进一步改进它。 I don't quite understand them though, so I leave it to others to try. 我不太了解他们,所以我把它留给别人试试。

EDIT: the actual code, same interface as Tomalak's. 编辑:实际代码,与Tomalak相同的界面。 I like this interface because it could be break ed anytime. 我喜欢这个界面,因为它可以随时break It's only slightly slower than if the loop is inlined in the function itself. 它仅比在函数本身内联循环时稍微慢一些。

var xp = crossProduct([
  [2,3,4,5],['angry','happy'], 
  ['monkeys','anteaters','manatees']]);
while (xp.next()) xp.do(console.log, console);
function crossProduct(sets) {
  var n = sets.length, carets = [], args = [];

  function init() {
    for (var i = 0; i < n; i++) {
      carets[i] = 0;
      args[i] = sets[i][0];
    }
  }

  function next() {
    if (!args.length) {
      init();
      return true;
    }
    var i = n - 1;
    carets[i]++;
    if (carets[i] < sets[i].length) {
      args[i] = sets[i][carets[i]];
      return true;
    }
    while (carets[i] >= sets[i].length) {
      if (i == 0) {
        return false;
      }
      carets[i] = 0;
      args[i] = sets[i][0];
      carets[--i]++;
    }
    args[i] = sets[i][carets[i]];
    return true;
  }

  return {
    next: next,
    do: function (block, _context) {
      return block.apply(_context, args);
    }
  }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM