【问题标题】:Lazy Cartesian product of arrays (arbitrary nested loops)数组的惰性笛卡尔积(任意嵌套循环)
【发布时间】:2012-02-23 22:27:11
【问题描述】:

还有其他关于这个inotherlanguages和其他non-lazyJavaScript versions的问题,但我没有找到惰性JavaScript版本。

给定一个由任意数量的任意大小数组组成的数组:

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

请注意,这些组合与嵌套循环所获得的结果相同:

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

笛卡尔积的好处是:

  1. 它允许您嵌套任意数量的循环(也许您不知道要迭代多少项)
  2. 它可以让您更改循环顺序(例如先按形容词循环),而无需编辑代码或写出所有可能的循环顺序组合。

基准

您可以在此处查看以下答案的基准:
http://jsperf.com/lazy-cartesian-product/26

【问题讨论】:

  • 好奇:你为什么问这个问题?
  • @RobW 因为我需要它却找不到它。具体来说,我的distinct color generator 正在获得使用 HSV 或 Lab 的能力,并以任意顺序迭代这些轴(这对算法有影响),我不想手写 12不同的 3-nested-for-loops 集。现在我已经写了它,我不相信我的实现是最优雅的。 :)
  • 惰性笛卡尔积、幂集、排列和组合在 js-combinatorics 模块中实现:github.com/dankogai/js-combinatorics

标签: javascript


【解决方案1】:

递归和迭代的组合可以完成这项工作。

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

演示: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);

【讨论】:

  • 我的方法比另一种更快 :) 见这个基准:jsperf.com/…
  • +1 不错。比起重用函数本身,我更喜欢使用辅助函数的方式。还有performs comparably to mine.
  • 嘿,有趣的是我们都 JSPerf 了它,奇怪的是我们得到了如此不同的结果。也许性能特征会因数组的大小和数量而异?
  • 我真的很惊讶这比我的执行速度更快,甚至相当,因为我总共使用了 13 个递归函数调用(在提供的测试集上),而这个答案使用了 37 个调用。 (嗯,与我的第一个版本相比;我的第二个版本现在赢了:)
【解决方案2】:

巧合地在周末做同样的事情。我一直在寻找替代基于 [].every 的算法的实现,结果证明它在 Firefox 中的性能非常糟糕(但在 Chrome 中尖叫——速度是下一个的两倍多)。

最终结果是 http://jsperf.com/lazy-cartesian-product/19 。它类似于 Tomalak 的方法,但只有一个参数数组随着插入符号的移动而发生变异,而不是每次都生成。

我确信通过使用其他算法中的巧妙数学可以进一步改进它。不过我不是很了解,所以我把它留给其他人尝试。

编辑:实际代码,与 Tomalak 的界面相同。我喜欢这个界面,因为它可以随时breaked。它只比函数本身内联循环稍微慢一点。

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

【讨论】:

  • 非常好。我将为您提供原始速度的接受,并为回调应用程序包含一个可选的上下文。为什么每次调用next() 时都检查args.length 而不是保证在构造时运行一次init()
  • 如果您有兴趣,请参阅我的回答,了解对产品执行随机延迟访问的方法。
  • 这是因为我需要第一次调用 next() 以不增加最后一个插入符号。它也可以通过在外部进行初始化并将最后一个插入符号设置为 -1 来解决。但没关系,我看到您已经大大改进了您的第二个实现!做得好,我正在避免递归,因为我认为这会减慢我的第一个算法,但实际上你的算法要快得多!
  • 啊,有道理。是的,您可以为每组参数重复使用相同的数组的想法确实加快了递归版本的速度。 :)
  • +1 FWIW,这是与我的实现的几个变体的性能比较:jsperf.com/lazycartesianiterator/2。干得好!
【解决方案3】:

我已经创建了这个解决方案:

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

它是这样使用的:

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

它似乎运行良好,但没有经过彻底测试,如果发现错误,请告诉我。

查看比较:http://jsperf.com/lazy-cartesian-product/8

【讨论】:

  • 很有趣,谢谢分享。虽然我通常讨厌迭代器,但我喜欢使用回调的返回值在中间停止迭代是多么容易。我已经在 OS X 下的 Chrome/Safari/FF/Opera 上运行了测试,包括your benchmark 和一个modified to use strings。跨浏览器的性能差异是惊人的!
  • @Phrogz 我认为能够在事物中间停止迭代是懒惰的全部意义所在。 ;) 无论如何,由于某种原因,这在我的 Firefox 9 上速度慢得令人羞耻,而且相比之下总体上太复杂了。如果集合包含空数组,它也会失败。
  • @Phrogz 我已经设法通过caching the array lengths 在 Chrome 上挤出更多内容,但在其他浏览器上它仍然很慢。我怀疑array.map() 负责。由于某种原因,jsPerf 似乎也没有正确注册基准测试结果——至少我的运行没有显示在图表中。知道为什么会这样吗?
  • 不错的方法。在 Chrome 中,这是所有列出的方法中最快的方法。在所有其他浏览器中,它与其他方法一样快/慢。我想当您将 "do" 移动到 while 的主体并将 reset 移动到 next (因为它只使用一次)时,您的代码可以(快得多)。以一种好的方式使用 cmets 仍然可以保持代码的可读性,同时您每次迭代都消除了两个函数调用。
  • @RobW 将 do 移动到 while 的正文中,IMO 破坏了可用性。此外,每次迭代只调用一次重置,这当然对整体速度没有影响。无论如何,如果您想继续尝试,可能有办法改进这一点。我也会考虑一下的
【解决方案4】:

这是我的解决方案,使用递归。我不喜欢它在第一次通过时创建一个空数组,或者它在for 循环内使用if 的事实(而不是将测试展开为两个循环以提高速度,以牺牲干燥为代价) 但至少它有点简洁:

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

实际操作:http://jsfiddle.net/RRcHN/


编辑:这是一个更快的版本,大约比上面的快2x–10x

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

不是为每个递归调用创建自定义数组,而是为所有参数重用一个数组 (p)。它还允许您为函数应用程序传入上下文参数。


编辑 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"

对上述内容进行解码,数组[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 ],
]

你可以看到上面公式的漂亮版本on my website

可以通过以相反的顺序迭代集合来预先计算红利和模数:

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

有了这个,查找一个特定的组合是一个简单的映射集合的问题:

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

但是,对于纯粹的速度和前向迭代,请参阅接受的答案。使用上述技术——即使我们预先计算一次股息和模数列表——也比那个答案慢 2-4 倍。

【讨论】:

    猜你喜欢
    • 2021-05-25
    • 2012-04-24
    • 1970-01-01
    • 2017-03-22
    • 2010-10-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多