可变数量的嵌套for循环

27

编辑:非常抱歉,我忘记提到我需要计数器变量的值。所以单独使用一个循环并不能解决我的问题。

我不确定是否可能实现以下操作,但我想要做到如下。 将一个数字数组传递给函数,每个数字是一个for循环的上限值,例如,如果数组是[2, 3, 5],则应执行以下代码:

for(var a = 0; a < 2; a++) {
     for(var b = 0; b < 3; b++) {
          for(var c = 0; c < 5; c++) {
                doSomething([a, b, c]);
          }
     }
}

因此,嵌套的for循环数量等于数组的长度。有没有办法使这个工作?我在考虑创建一段代码,将每个for循环添加到一个字符串中,然后通过eval评估它。但我也读到过eval不应该是首选,因为它可能会产生危险的结果。

在这里可能适用的技术是什么?


2
所以你只是想调用一些函数,次数等于传入数组中数字的乘积? - Sean
1
不好意思,我需要for循环的变量(这里是a、b和c)。 - pimvdb
请查看此问题/解决方案,了解更简单、现代和优雅的解决方案,以解决一个更普遍的问题。 - Peter Krauss
9个回答

24

递归可以完美解决这个问题:

function callManyTimes(maxIndices, func) {
    doCallManyTimes(maxIndices, func, [], 0);
}

function doCallManyTimes(maxIndices, func, args, index) {
    if (maxIndices.length == 0) {
        func(args);
    } else {
        var rest = maxIndices.slice(1);
        for (args[index] = 0; args[index] < maxIndices[0]; ++args[index]) {
            doCallManyTimes(rest, func, args, index + 1);
        }
    }
}

这样调用函数:

callManyTimes([2,3,5], doSomething);

你的解决方案也非常好用。实际上,你的是最干净的,对我来说更容易理解。非常感谢。 - pimvdb
很棒的解决方案,你的也是提出的最快的一个。我的(非科学性)测试显示,如果我们以本地嵌套循环作为基准“X”,那么:Sean:4X,Guffa:8X,Mike Samuel:15X,Pointy:28X - serg
@Sean,你的递归算法非常完美,谢谢你的付出。在过去的两天里,我一直在无功而返地尝试重新编写它,以便能够从给定的索引(args在你的函数中)开始。例如,我想要 callManyTimes([5,5,5], doSomething); 不是从 [0,0,0] 开始,而是从 [2, 4, 5] 开始。有人可以帮助我使用 @Sean 的代码实现这一点吗? - Reath
1
@Reath 添加 minIndices 参数非常容易:https://pastebin.com/rxswG7bj - Sean

10

递归在这里就有点过头了。你可以使用生成器:

function* allPossibleCombinations(lengths) {
  const n = lengths.length;

  let indices = [];
  for (let i = n; --i >= 0;) {
    if (lengths[i] === 0) { return; }
    if (lengths[i] !== (lengths[i] & 0x7fffffff)) { throw new Error(); }
    indices[i] = 0;
  }

  while (true) {
    yield indices;
    // Increment indices.
    ++indices[n - 1];
    for (let j = n; --j >= 0 && indices[j] === lengths[j];) {
      if (j === 0) { return; }
      indices[j] = 0;
      ++indices[j - 1];
    }
  }
}

for ([a, b, c] of allPossibleCombinations([3, 2, 2])) {
  console.log(`${a}, ${b}, ${c}`);
}

这里的直觉是我们保持一个索引列表,这些索引始终小于相应的长度。

第二个循环处理进位。就像在增加十进制数199时,我们会到(1, 9, 10),然后进位得到(1, 10, 0)最后是(2, 0, 0)。如果没有足够的数字可以进位,那么我们就完成了。


这是一个巧妙的方法,你能否解释一下你在0x7fffffff上做了什么? - pimvdb
@pimvdb,请确保长度为非负整数,以便下面的 indices[j] === lengths[j] 检查有通过的机会。 - Mike Samuel
我看到了一些方法可以压缩它并使其更适用于我的用例:https://dev59.com/dG445IYBdhLWcg3w0dZD#44753698 谢谢Mike! - Web_Designer

4

设置一个与限制数组长度相同的计数器数组。使用单个循环,在每次迭代中增加最后一项。当它达到限制时,重新启动并增加下一项。

function loop(limits) {
  var cnt = new Array(limits.length);
  for (var i = 0; i < cnt.length; i++) cnt[i] = 0;
  var pos;
  do {
    doSomething(cnt);
    pos = cnt.length - 1;
    cnt[pos]++;
    while (pos >= 0 && cnt[pos] >= limits[pos]) {
      cnt[pos] = 0;
      pos--;
      if (pos >= 0) cnt[pos]++;
    }
  } while (pos >= 0);
}

2

不要想着使用嵌套的for循环,而是考虑递归函数调用。为了进行迭代,您需要做出以下决定(伪代码):

if the list of counters is empty
    then "doSomething()"
else
    for (counter = 0 to first counter limit in the list)
        recurse with the tail of the list

它可能看起来像这样:

function forEachCounter(counters, fn) {
  function impl(counters, curCount) {
    if (counters.length === 0)
      fn(curCount);
    else {
      var limit = counters[0];
      curCount.push(0);
      for (var i = 0; i < limit; ++i) {
        curCount[curCount.length - 1] = i;
        impl(counters.slice(1), curCount);
      }
      curCount.length--;
    }
  }
  impl(counters, []);
}

您需要使用一个参数作为计数限制列表,另一个参数作为要执行的每个有效计数数组的函数(“doSomething”部分)来调用该函数。上面的主函数在内部函数中完成所有真正的工作。在该内部函数中,第一个参数是计数器限制列表,随着递归调用函数而“缩减”。第二个参数用于保存当前的计数器值集,以便“doSomething”知道它正在进行与特定实际计数列表对应的迭代。
调用该函数的方式如下:
forEachCounter([4, 2, 5], function(c) { /* something */ });

2

一个不需要编写复杂程序的解决方案是将这些整数相乘。由于你只是嵌套了if语句,并且只有最内层的if语句具有功能,所以这应该有效:

var product = 0;
for(var i = 0; i < array.length; i++){
    product *= array[i];
}

for(var i = 0; i < product; i++){
    doSomething();
}

或者:

for(var i = 0; i < array.length; i++){
    for(var j = 0; j < array[i]; j++){
        doSomething();
    }
}

1
这是我试图简化Mike Samuel的非递归解决方案。我还添加了为每个整数参数设置范围(不仅仅是最大值)的功能。

function everyPermutation(args, fn) {
    var indices = args.map(a => a.min);

    for (var j = args.length; j >= 0;) {
        fn.apply(null, indices);

        // go through indices from right to left setting them to 0
        for (j = args.length; j--;) {
            // until we find the last index not at max which we increment
            if (indices[j] < args[j].max) {
                ++indices[j];
                break;
            }
            indices[j] = args[j].min;
        }
    }
}

everyPermutation([
    {min:4, max:6},
    {min:2, max:3},
    {min:0, max:1}
], function(a, b, c) {
    console.log(a + ',' + b + ',' + c);
});


0

使用2、3、5三个数字循环三次和使用一个30的循环(2*3*5)没有区别。

function doLots (howMany, what) {
    var amount = 0;

    // Aggregate amount
    for (var i=0; i<howMany.length;i++) {
        amount *= howMany[i];
    };

    // Execute that many times.    
    while(i--) {
        what();
    };
}

使用:

doLots([2,3,5], doSomething);

1
非常抱歉,但我还需要计数器变量的值。虽然我很喜欢你的解决方案,但那些信息会丢失。 - pimvdb
你比我先说了。另外,你需要哪种信息?可以将原始数组中的所有整数简单地保存吗?还是需要以某种不同的方式保存它们? - user535617
我正在尝试创建一个通用的多维数组函数,因此我需要用值填充每个索引组合。 因此需要嵌套for循环。其中一个for循环会使索引丢失并仅返回一个索引(这里是0-30)。 - pimvdb

0

您可以使用贪心算法枚举笛卡尔积 0:2 x 0:3 x 0:5 的所有元素。我的函数 greedy_backward 可以执行此算法。我不是 Javascript 方面的专家,也许这个函数还有改进的空间。

function greedy_backward(sizes, n) {
  for (var G = [1], i = 0; i<sizes.length; i++) G[i+1] = G[i] * sizes[i]; 
  if (n>=_.last(G)) throw new Error("n must be <" + _.last(G));
  for (i = 0; i<sizes.length; i++) if (sizes[i]!=parseInt(sizes[i]) || sizes[i]<1){  throw new Error("sizes must be a vector of integers be >1"); }; 
  for (var epsilon=[], i=0; i < sizes.length; i++) epsilon[i]=0;
  while(n > 0){
    var k = _.findIndex(G, function(x){ return n < x; }) - 1;
    var e = (n/G[k])>>0;
    epsilon[k] = e;
    n = n-e*G[k];
  }
  return epsilon;
}

它按反词典顺序枚举笛卡尔积的元素(您将在doSomething示例中看到完整的枚举):

~ var sizes = [2, 3, 5];
~ greedy_backward(sizes,0);
0,0,0
~ greedy_backward(sizes,1);
1,0,0
~ greedy_backward(sizes,2);
0,1,0
~ greedy_backward(sizes,3);
1,1,0
~ greedy_backward(sizes,4);
0,2,0
~ greedy_backward(sizes,5);
1,2,0

这是二进制表示的一般化(当 sizes=[2,2,2,...] 时的情况)。

例如:

~ function doSomething(v){
    for (var message = v[0], i = 1; i<v.length; i++) message = message + '-' + v[i].toString(); 
    console.log(message); 
  }
~ doSomething(["a","b","c"])
a-b-c
~ for (var max = [1], i = 0; i<sizes.length; i++) max = max * sizes[i]; 
30
~ for(i=0; i<max; i++){
    doSomething(greedy_backward(sizes,i));
  }
0-0-0
1-0-0
0-1-0
1-1-0
0-2-0
1-2-0
0-0-1
1-0-1
0-1-1
1-1-1
0-2-1
1-2-1
0-0-2
1-0-2
0-1-2
1-1-2
0-2-2
1-2-2
0-0-3
1-0-3
0-1-3
1-1-3
0-2-3
1-2-3
0-0-4
1-0-4
0-1-4
1-1-4
0-2-4
1-2-4

如果需要,反向操作很简单:
function greedy_forward(sizes, epsilon) {
  if (sizes.length!=epsilon.length) throw new Error("sizes and epsilon must have the same length");
  for (i = 0; i<sizes.length; i++) if (epsilon[i] <0 || epsilon[i] >= sizes[i]){  throw new Error("condition `0 <= epsilon[i] < sizes[i]` not fulfilled for all i"); }; 
  for (var G = [1], i = 0; i<sizes.length-1; i++) G[i+1] = G[i] * sizes[i]; 
  for (var n = 0, i = 0; i<sizes.length; i++)  n += G[i] * epsilon[i]; 
  return n;
}

例子:

~ epsilon = greedy_backward(sizes, 29)
1,2,4
~ greedy_forward(sizes, epsilon)
29

0

我们也可以使用生成器来实现:

  function loop(...times) {
    function* looper(times, prev = []) {
       if(!times.length) {
           yield prev;
           return;
       }
       const [max, ...rest] = times;
       for(let current = 0; current < max; current++) {
         yield* looper(rest, [...prev, current]);
       }
   }
   return looper(times);
 }

这可以被用作:

  for(const [j, k, l, m] of loop(1, 2, 3, 4)) {
     //...
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接