Найти все комбинации опций в цикле

Я ищу лучший способ перебрать множество параметров, чтобы убедиться, что я ударил все доступные параметры.

Я создал функцию, которая позволяет клиенту создавать изображения, которые в основном представляют собой другие изображения, расположенные поверх друг друга. Эти другие изображения разделяются на разные группы. У них есть ссылки на стороне изображения, которые они могут щелкнуть, чтобы прокручивать все разные изображения, чтобы просмотреть их.

Теперь я делаю автоматизированный процесс, который будет запускать функцию, которая изменяет изображение, когда пользователь нажимает на одну из ссылок. Мне нужно убедиться, что во время этого процесса ударяется всякая комбинация различных изображений.

Скажите, что есть 3 разных шляпы, 4 разных рубашки, 5 разных пар брюк и 6 разных пар обуви. Я могу представить это как массив с количеством опций для каждой группы. Текущий массив [3, 4, 5, 6].

Каков наилучший способ перебрать этот массив, чтобы убедиться, что будут показаны все возможные параметры?

Ответ 1

Вы хотите Декартовы продукты всех ваших массивов.

У меня есть страница, обсуждающая это, включая реализации в JavaScript, на моем сайте:
http://phrogz.net/lazy-cartesian-product

Например, чтобы быстро перебирать их по порядку "вперед", вы можете использовать:

hats   = ['fez','fedora']
shirts = ['t-shirt','long']
pants  = ['shorts','jeans']
shoes  = ['sneaker','loafer']

lazyProduct( [hats,shirts,pants,shoes], function(hat,shirt,pant,shoe){
  // Your function is yielded unique combinations of values from the arrays
  console.log(hat,shirt,pant,shoe);
});

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

Выход:

fez t-shirt shorts sneaker
fez t-shirt shorts loafer
fez t-shirt jeans sneaker
fez t-shirt jeans loafer
fez long shorts sneaker
fez long shorts loafer
fez long jeans sneaker
fez long jeans loafer
fedora t-shirt shorts sneaker
fedora t-shirt shorts loafer
fedora t-shirt jeans sneaker
fedora t-shirt jeans loafer
fedora long shorts sneaker
fedora long shorts loafer
fedora long jeans sneaker
fedora long jeans loafer
fez t-shirt shorts sneaker
fez t-shirt shorts loafer

Это идентично результатам:

hats.forEach(function(hat){
  shirts.forEach(function(shirt){
    pants.forEach(function(pant){
      shoes.forEach(function(shoe){
        console.log(hat,shirt,pant,shoe);
      });
    });
  });
});

или (для старых браузеров):

for (var h=0;h<hats.length;h++){
  var hat = hats[h];
  for (var s=0;s<shirts.length;s++){
    var shirt = shirts[s];
    for (var p=0;p<pants.length;p++){
      var pant = pants[p];
      for (var e=0;e<shoes.length;e++){
        var shoe = shoes[e];
        console.log(hat,shirt,pant,shoe);        
      }
    }
  }
}

... но он поддерживает произвольное количество массивов, определенных во время выполнения. (И если вы используете первую "ленивую" реализацию с моего сайта, вы можете произвольно выбирать элементы, итерации в обратном порядке или легко останавливать итерацию в любой момент.)

Ответ 2

EDIT: Я сделал сравнение различных методов, используя jsperf здесь, способ Phrogz - это, очевидно, быстрее, чем в 3 раза.


Если я правильно понимаю, вы спрашиваете о подсчете, где каждый столбец цифр является другой базой. Вы можете сделать это рекурсивно.

function options(opArr, fullArray){
    var i = 0, j = opArr.length;
    if(j < fullArray.length){ // if opArr doesn't have item from each group, add new group
        while(i < fullArray[j]){ // count up for this group
            newArr = opArr.slice(0); // clone opArr so we don't run into shared reference troubles, not sure if necessary
            newArr[j] = i;
            i++;
            options(newArr, fullArray); // recurse
        }
    }else{ // opArr is now a unique array of your items
        // console.log(opArr);
    }
}
options([], [3, 9, 3, 3]);

Примечание: этот (пример) приведет к созданию массивов 3 * 9 * 3 * 3 = 243. Вы можете в конечном итоге есть много памяти таким образом.


Другой метод - преобразование из целого числа в массив, что может сэкономить на использовании памяти, поскольку вы можете забыть все предыдущие вычисленные массивы

function countUp(arrayOfBases, callback, self){
    var arr = arrayOfBases.reverse(), x = 1, i = arr.length,
        me = (self===undefined?this:self),
        makeArr = function(arr, x, fn, me){
        var a = arr.slice(0), n = [], i = x, j = 0, k = 0;
        while(a.length > 0){
            k = a[0];
            if(k !== 0) j = i % k, i = (i - j) / k;
            else j = 0;
            n.unshift(j);
            a.shift();
        }
        fn.call(me,n);
    };
    while (i-->0) if(arr[i] !== 0) x = x * arr[i];
    i = 0;
    while(i < x){
        makeArr(arr, i, callback, me);
        i++;
    }
}
countUp([3,9,3,3], function(a){console.log(a);});

Дополнительный метод, аналогичный предыдущему, сохраняет массив, созданный в прошлый раз, поэтому в циклах меньше вычислений за счет стоимости при инициализации.

function countUp2(arrayOfBases, callback, self){
    var arr = arrayOfBases.reverse(), x = 1, i = arr.length, last = [],
        me = (self===undefined?this:self),
        addOne = function(arr, n, fn, me){
        var j = n.length, i = j - 1;
        n[i]++;
        while(j = i, i-- > 0 && n[j] >= arr[j]){
            if(arr[j] === 0) n[i] += n[j], n[j] = 0;
            else n[i]++, n[j] -= arr[j];
        }
        return fn.call(me,n.slice(0)), n;
    };
    while (i-->0){
        if(arr[i] !== 0) x = x * arr[i];
        last[i] = 0;
    }
    i = 0;
    last[last.length-1] = -1;
    while(i < x){
        last = addOne(arr, last, callback, me);
        i++;
    }
}
countUp2([3,9,3,3], function(a){console.log(a);});

Все эти методы выводят

[0,0,0,0]
[0,0,0,1]
...
[0,8,1,2]
[0,8,2,0]
...
[2,8,2,1]
[2,8,2,2]

который вы можете обрабатывать по своему усмотрению.