Декартовий продукт з декількох масивів у JavaScript


112

Як би ви реалізували декартовий продукт з декількох масивів у JavaScript?

Як приклад,

cartesian([1, 2], [10, 20], [100, 200, 300]) 

повинен повернутися

[
  [1, 10, 100],
  [1, 10, 200],
  [1, 10, 300],
  [2, 10, 100],
  [2, 10, 200]
  ...
]


3
Це реалізовано в модулі js-комбінаторики: github.com/dankogai/js-combinatorics
Segal-Halevi


Я згоден щодо underscore.js, але я не впевнений, що бачу, як видалення тега функціонального програмування допоможе @le_m
viebel

Fwiw, d3 додано d3.cross(a, b[, reducer])у лютому. github.com/d3/d3-array#cross
Тоф

Відповіді:


105

Оновлення 2017 року: 2-рядова відповідь з ванільним JS

Усі відповіді тут надмірно складні , більшість з них займає 20 рядків коду або навіть більше.

У цьому прикладі використовуються лише два рядки JavaScript ванілі , без подачі, підкреслення чи інших бібліотек:

let f = (a, b) => [].concat(...a.map(a => b.map(b => [].concat(a, b))));
let cartesian = (a, b, ...c) => b ? cartesian(f(a, b), ...c) : a;

Оновлення:

Це те саме, що вище, але вдосконалено для чіткого дотримання посібника зі стилю JavaScript Airbnb - перевірено за допомогою ESLint з eslint-config-airbnb-base :

const f = (a, b) => [].concat(...a.map(d => b.map(e => [].concat(d, e))));
const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a);

Особлива подяка ZuBB за те, що він повідомив мене про проблеми з лінером з оригінальним кодом.

Приклад

Це точний приклад вашого запитання:

let output = cartesian([1,2],[10,20],[100,200,300]);

Вихід

Це вихід цієї команди:

[ [ 1, 10, 100 ],
  [ 1, 10, 200 ],
  [ 1, 10, 300 ],
  [ 1, 20, 100 ],
  [ 1, 20, 200 ],
  [ 1, 20, 300 ],
  [ 2, 10, 100 ],
  [ 2, 10, 200 ],
  [ 2, 10, 300 ],
  [ 2, 20, 100 ],
  [ 2, 20, 200 ],
  [ 2, 20, 300 ] ]

Демо

Перегляньте демонстраційні демонстрації на:

Синтаксис

Синтаксис, який я тут використав, не є новим. У моєму прикладі використовується оператор розповсюдження та інші параметри - особливості JavaScript, визначені в 6-му випуску стандарту ECMA-262, опублікованому в червні 2015 року та розробленому набагато раніше, більш відомі як ES6 або ES2015. Побачити:

Це робить такий код таким простим, що не використовувати його гріхом. Для старих платформ, які не підтримують його на початковому рівні, ви завжди можете використовувати Babel або інші інструменти, щоб перевести його в старіший синтаксис - і фактично мій приклад, перекладений Babel, все ще коротший і простіший, ніж більшість прикладів тут, але це не насправді має значення, тому що результат транспіляції - це не те, що потрібно розуміти чи підтримувати, це лише факт, який мені здається цікавим.

Висновок

Не потрібно писати сотні рядків коду, які важко підтримувати, і немає необхідності використовувати цілі бібліотеки для такої простої речі, коли два рядки ванільного JavaScript можуть легко виконати роботу. Як ви бачите, справді окупається використання сучасних можливостей мови, і у випадках, коли вам потрібно підтримувати архаїчні платформи без вбудованої підтримки сучасних функцій, ви завжди можете використовувати Babel або інші інструменти для перекладу нового синтаксису на старий .

Не кодуйте, як це 1995 рік

JavaScript розвивається, і це робиться не просто так. TC39 виконує дивовижну роботу мовного дизайну з додаванням нових функцій, а постачальники браузерів роблять дивовижну роботу щодо впровадження цих функцій.

Щоб побачити поточний стан нативної підтримки будь-якої функції у веб-переглядачах, див.

Щоб побачити підтримку у версіях Node, див.

Щоб використовувати сучасний синтаксис на платформах, які не підтримують його спочатку, використовуйте Babel:


Ось версія машинопису з невеликою зміною для врахування способу розповсюдження масиву. gist.github.com/ssippe/1f92625532eef28be6974f898efb23ef
Sam Sippe

1
@rsp дякую за дуже гарну відповідь. хоча я хотів би попросити вас трохи покращити його, щоб отримати попередження про затінені змінні (2 локальні вари aта 2 місцеві варіанти b)
ZuBB

7
"Не кодуйте, як це 1995" - не потрібно бути неприємним, ще не все наздогнали.
Godwhacker

7
Це добре, однак не вдається, якщо годувати , в результаті ['a', 'b'], [1,2], [[9], [10]]чого дасть [ [ 'a', 1, 9 ], [ 'a', 1, 10 ], [ 'a', 2, 9 ], [ 'a', 2, 10 ], [ 'b', 1, 9 ], [ 'b', 1, 10 ], [ 'b', 2, 9 ], [ 'b', 2, 10 ] ]результат. Я маю на увазі, не буде зберігати тип предметів [[9], [10]].
Скорочення

1
Оскільки ми вже користуємося ..., чи не слід це робити [].concat(...[array])просто [...array]?
Лазар Любенович

88

Ось функціональне рішення проблеми (без змінної змінної !) З використанням reduceта flatten, передбачених underscore.js:

function cartesianProductOf() {
    return _.reduce(arguments, function(a, b) {
        return _.flatten(_.map(a, function(x) {
            return _.map(b, function(y) {
                return x.concat([y]);
            });
        }), true);
    }, [ [] ]);
}

// [[1,3,"a"],[1,3,"b"],[1,4,"a"],[1,4,"b"],[2,3,"a"],[2,3,"b"],[2,4,"a"],[2,4,"b"]]
console.log(cartesianProductOf([1, 2], [3, 4], ['a']));  
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore.js"></script>

Зауваження: Це рішення було натхнене http://cwestblog.com/2011/05/02/cartesian-product-of-multiple-arrays/


У цій відповіді є помилка друку, не повинно бути "справжнього" (можливо, лодаш змінився з того часу, як ви зробили цю посаду?)
Кріс Джефферсон

@ChrisJefferson другий параметр - flattenце зробити сплющення дрібним. Тут це обов’язково!
viebel

4
Вибачте, це несумісність лодашу / підкреслення, вони помінялися навколо прапора.
Кріс Джефферсон

1
Таким чином , при випрямленні, використання trueз підкресленням і використанням falseз lodash для забезпечення дрібного уплощенія.
Акселі Пален

Як змінити цю функцію, щоб вона прийняла масив масивів?

44

Ось модифікована версія коду @ viebel у простому Javascript, не використовуючи жодної бібліотеки:

function cartesianProduct(arr) {
    return arr.reduce(function(a,b){
        return a.map(function(x){
            return b.map(function(y){
                return x.concat([y]);
            })
        }).reduce(function(a,b){ return a.concat(b) },[])
    }, [[]])
}

var a = cartesianProduct([[1, 2,3], [4, 5,6], [7, 8], [9,10]]);
console.log(JSON.stringify(a));


2
Помилки для декартового продукту ([[[1], [2], [3]], ['a', 'b'], [['gamma'], [['alfa']]], ['zii', 'faa']]), оскільки він сплющує ['gamma'] до 'gamma', а [['alfa']] до ['alpha']
Mzn

бо .concat(y)замість.concat([ y ])
Дякую

@Thankyou Ви можете редагувати відповідь безпосередньо, а не коментувати, просто це зробили, тому немає потреби зараз: P
Олів'є Лалонде

28

здається, що громада вважає це банальним і або легко знайти еталонну реалізацію, після короткого огляду я не міг, а може, це просто те, що мені подобається переосмислити колесо або вирішити проблеми, пов'язані з програмуванням у класі, так чи інакше, ваш щасливий день :

function cartProd(paramArray) {

  function addTo(curr, args) {

    var i, copy, 
        rest = args.slice(1),
        last = !rest.length,
        result = [];

    for (i = 0; i < args[0].length; i++) {

      copy = curr.slice();
      copy.push(args[0][i]);

      if (last) {
        result.push(copy);

      } else {
        result = result.concat(addTo(copy, rest));
      }
    }

    return result;
  }


  return addTo([], Array.prototype.slice.call(arguments));
}


>> console.log(cartProd([1,2], [10,20], [100,200,300]));
>> [
     [1, 10, 100], [1, 10, 200], [1, 10, 300], [1, 20, 100], 
     [1, 20, 200], [1, 20, 300], [2, 10, 100], [2, 10, 200], 
     [2, 10, 300], [2, 20, 100], [2, 20, 200], [2, 20, 300]
   ]

Повна реалізація посилань, яка є досить ефективною ... :-D

з точки зору ефективності: ви можете отримати певну кількість, вийнявши if із циклу та маючи дві окремі петлі, оскільки це технічно постійне значення, і ви б допомагали з передбаченням гілок та всім цим безладом, але ця точка є своєрідною суперечкою у javascript

anywho, насолоджуватися -ck


1
Дякую @ckoz за детальну відповідь. Чому б ви не використали reduceфункцію масиву?
viebel

1
@viebel, чому ти хочеш скористатись скороченням? для одного, у зменшенні є дуже погана підтримка старих браузерів (див.: developer.mozilla.org/en-US/docs/JavaScript/Reference/… ), і в будь-якому випадку, чи справді цей божевільний код із цієї іншої відповіді виглядає читабельним для вас ? це не мені. переконайтеся, що він коротший, але як тільки він буде зміщений, цей код буде приблизно однакової довжини, простіше налагоджувати / оптимізувати, по-друге, всі ті "зменшити" рішення розпадаються на одне і те ж, за винятком того, що вони мають пошук закриття (теоретично повільніше), це також складніше розробити так, щоб він обробляв нескінченні набори ...
ckozl

5
Я створив у 2+ рази швидшу і (імо) більш чисту версію: pastebin.com/YbhqZuf7 Він досягає збільшення швидкості, не використовуючи result = result.concat(...)та не використовуючи args.slice(1). На жаль, мені не вдалося знайти спосіб позбутися curr.slice()та рекурсії.
Пауан

2
@Pauan приємна робота, приємне зменшення гарячих точок в цілому за лігу на 10% -50% підвищення продуктивності на основі того, що я бачу. Я не можу говорити про "чистоту", хоча, я вважаю, що за вашою версією насправді важче дотримуватися через використання змінних областей закриття. Але загалом кажучи, більш ефективний код важче дотримуватися. Я написав оригінальну версію для читабельності, хотів би, щоб у мене було більше часу, щоб я міг залучати вас до виступу на виставу;) можливо пізніше ...
ckozl

це справді одна з таких проблем
Джеймс

26

Наступна ефективна функція генератора повертає декартовий продукт усіх заданих ітерабелей :

// Generate cartesian product of given iterables:
function* cartesian(head, ...tail) {
  const remainder = tail.length > 0 ? cartesian(...tail) : [[]];
  for (let r of remainder) for (let h of head) yield [h, ...r];
}

// Example:
console.log(...cartesian([1, 2], [10, 20], [100, 200, 300]));

Він приймає масиви, рядки, набори та всі інші об'єкти, що реалізують ітерабельний протокол .

Слідуючи специфікації декартового продукту n-арі, який він дає

  • []якщо один або більше заданих ітерабелів порожні, наприклад, []або''
  • [[a]]якщо задано єдиний ітерабельний вміст, що містить одне значення a.

Усі інші випадки розглядаються як очікується, як показано в наступних тестових випадках:


Ви не проти пояснити, що відбувається на цьому? Дуже дякую!
ЛеандроП

Дякуємо, що ви навчили нас досить чудовим прикладом використання функції генератора + хвостова рекурсія + двошарові петлі! Але положення першого for-циклу в коді потрібно змінити, щоб зробити порядок вихідних підмасивів правильним. Фіксований код:function* cartesian(head, ...tail) { for (let h of head) { const remainder = tail.length > 0 ? cartesian(...tail) : [[]]; for (let r of remainder) yield [h, ...r] } }
ooo

@ooo Якщо ви хочете відтворити порядок декартових продуктових кортежів, наданих коментарем ОП, то ваша модифікація правильна. Однак порядок кортежів у виробі зазвичай не має значення, наприклад, математично результатом є невпорядкований набір. Я вибрав це замовлення, оскільки воно вимагає набагато менших рекурсивних дзвінків і тому трохи ефективніше - хоча я не виконував орієнтир.
le_m

Помилка: У моєму коментарі вище "хвоста рекурсія" повинна бути "рекурсією" (а не хвостовим викликом у цьому випадку).
ooo

20

Ось нефантастичне, прямолінійне рекурсивне рішення:

function cartesianProduct(a) { // a = array of array
    var i, j, l, m, a1, o = [];
    if (!a || a.length == 0) return a;

    a1 = a.splice(0, 1)[0]; // the first array of a
    a = cartesianProduct(a);
    for (i = 0, l = a1.length; i < l; i++) {
        if (a && a.length) for (j = 0, m = a.length; j < m; j++)
            o.push([a1[i]].concat(a[j]));
        else
            o.push([a1[i]]);
    }
    return o;
}

console.log(cartesianProduct([[1,2], [10,20], [100,200,300]]));
// [[1,10,100],[1,10,200],[1,10,300],[1,20,100],[1,20,200],[1,20,300],[2,10,100],[2,10,200],[2,10,300],[2,20,100],[2,20,200],[2,20,300]]


2
Цей виявляється найефективнішим чистим кодом JS у цій темі. Потрібно, як ~ 600 мс, щоб закінчити на масивах 3 х 100, щоб створити масив довжиною 1М.
Скорочення

1
Працює для декартового продукту ([[[1], [2], [3]], ['a', 'b'], [['gamma'], [['alfa']]], ['zii', 'faa']]); без вирівнювання початкових значень
Mzn

10

Ось рекурсивний спосіб, що використовує функцію генератора ECMAScript 2015, тому вам не потрібно створювати всі кортежі відразу:

function* cartesian() {
    let arrays = arguments;
    function* doCartesian(i, prod) {
        if (i == arrays.length) {
            yield prod;
        } else {
            for (let j = 0; j < arrays[i].length; j++) {
                yield* doCartesian(i + 1, prod.concat([arrays[i][j]]));
            }
        }
    }
    yield* doCartesian(0, []);
}

console.log(JSON.stringify(Array.from(cartesian([1,2],[10,20],[100,200,300]))));
console.log(JSON.stringify(Array.from(cartesian([[1],[2]],[10,20],[100,200,300]))));


Це не працюватиме, коли в одному з масивів є елементи масиву, такі якcartesian([[1],[2]],[10,20],[100,200,300])
Відновити

@Redu відповідь було оновлено для підтримки аргументів масиву.
heenenee

Так, .concat()вбудований оператор розповсюдження іноді може стати обманним.
Скорочення

10

Ось однорівневий з використанням нативного ES2019 flatMap. Бібліотеки не потрібні, лише сучасний браузер (або транспілер):

data.reduce((a, b) => a.flatMap(x => b.map(y => [...x, y])), [[]]);

Це по суті сучасний варіант відповіді вібеля, без ломашу.


9

Використовуючи типовий зворотний трек з генераторами ES6,

function cartesianProduct(...arrays) {
  let current = new Array(arrays.length);
  return (function* backtracking(index) {
    if(index == arrays.length) yield current.slice();
    else for(let num of arrays[index]) {
      current[index] = num;
      yield* backtracking(index+1);
    }
  })(0);
}
for (let item of cartesianProduct([1,2],[10,20],[100,200,300])) {
  console.log('[' + item.join(', ') + ']');
}
div.as-console-wrapper { max-height: 100%; }

Нижче є схожа версія, сумісна зі старими браузерами.



7

Версія для створення кави з лодашем:

_ = require("lodash")
cartesianProduct = ->
    return _.reduceRight(arguments, (a,b) ->
        _.flatten(_.map(a,(x) -> _.map b, (y) -> x.concat(y)), true)
    , [ [] ])

7

Підхід єдиного рядка для кращого читання з відступами.

result = data.reduce(
    (a, b) => a.reduce(
        (r, v) => r.concat(b.map(w => [].concat(v, w))),
        []
    )
);

Він займає єдиний масив з масивами шуканих декартових елементів.

var data = [[1, 2], [10, 20], [100, 200, 300]],
    result = data.reduce((a, b) => a.reduce((r, v) => r.concat(b.map(w => [].concat(v, w))), []));

console.log(result.map(a => a.join(' ')));
.as-console-wrapper { max-height: 100% !important; top: 0; }


Мені довелося додати оператор-захист, щоб правильно обробити випадок, коли масив має єдиний елемент:if (arr.length === 1) return arr[0].map(el => [el]);
JacobEvelyn

5

Це позначено функціональним програмуванням, тому давайте подивимось на монаду « Список» :

Один додаток до цього монадичного списку представляє недетерміновані обчислення. List може містити результати для всіх шляхів виконання в алгоритмі ...

Добре, що це ідеально підходить для cartesian. JavaScript дає нам, Arrayі функція прив'язки монади є Array.prototype.flatMap, тому давайте використовувати їх для використання -

const cartesian = (...all) =>
{ const loop = (t, a, ...more) =>
    a === undefined
      ? [ t ]
      : a .flatMap (x => loop ([ ...t, x ], ...more))
  return loop ([], ...all)
}

console .log (cartesian ([1,2], [10,20], [100,200,300]))

Замість loopвищезазначеного tможна додати як вивірений параметр -

const makeCartesian = (t = []) => (a, ...more) =>
  a === undefined
    ? [ t ]
    : a .flatMap (x => makeCartesian ([ ...t, x ]) (...more))

const cartesian =
  makeCartesian ()

console .log (cartesian ([1,2], [10,20], [100,200,300]))


3

Деякі відповіді в цій темі не вдається, коли будь-який з вхідних масивів містить елемент масиву. Вам краще це перевірити.

У будь-якому разі, немає необхідності підкреслювати, будь-що. Я вважаю, що це слід робити з чистим JS ES6, настільки ж функціональним, наскільки він отримує.

Цей фрагмент коду використовує зменшену та вкладену карту, просто для отримання декартового добутку двох масивів, однак другий масив походить від рекурсивного виклику до тієї ж функції з одним менш масивом; отже .. a[0].cartesian(...a.slice(1))

Array.prototype.cartesian = function(...a){
  return a.length ? this.reduce((p,c) => (p.push(...a[0].cartesian(...a.slice(1)).map(e => a.length > 1 ? [c,...e] : [c,e])),p),[])
                  : this;
};

var arr = ['a', 'b', 'c'],
    brr = [1,2,3],
    crr = [[9],[8],[7]];
console.log(JSON.stringify(arr.cartesian(brr,crr))); 


3

У моїй конкретній обстановці "старомодний" підхід видався більш ефективним, ніж методи, засновані на більш сучасних особливостях. Нижче наведено код (включаючи невелике порівняння з іншими рішеннями, розміщеними в цій темі від @rsp та @sebnukem), якщо він виявиться корисним і для когось іншого.

Ідея наступна. Скажімо, ми будуємо зовнішній добуток Nмасивів, a_1,...,a_Nкожен з яких має m_iкомпоненти. Зовнішній добуток цих масивів має M=m_1*m_2*...*m_Nелементи, і ми можемо ототожнити кожен з них за N-розмірним вектором, складовими якого є додатні цілі числа, а i-та складова строго обмежена зверху m_i. Наприклад, вектор (0, 0, ..., 0)відповідав би певній комбінації, в рамках якої кожен бере перший елемент з кожного масиву, в той час (m_1-1, m_2-1, ..., m_N-1)як ототожнюється з комбінацією, де кожний приймає останній елемент з кожного масиву. Таким чином, щоб сконструювати всеM комбінації, функція нижче послідовно будує всі такі вектори і для кожного з них ідентифікується відповідна комбінація елементів вхідних масивів.

function cartesianProduct(){
    const N = arguments.length;

    var arr_lengths = Array(N);
    var digits = Array(N);
    var num_tot = 1;
    for(var i = 0; i < N; ++i){
        const len = arguments[i].length;
        if(!len){
            num_tot = 0;
            break;
        }
        digits[i] = 0;
        num_tot *= (arr_lengths[i] = len);
    }

    var ret = Array(num_tot);
    for(var num = 0; num < num_tot; ++num){

        var item = Array(N);
        for(var j = 0; j < N; ++j){ item[j] = arguments[j][digits[j]]; }
        ret[num] = item;

        for(var idx = 0; idx < N; ++idx){
            if(digits[idx] == arr_lengths[idx]-1){
                digits[idx] = 0;
            }else{
                digits[idx] += 1;
                break;
            }
        }
    }
    return ret;
}
//------------------------------------------------------------------------------
let _f = (a, b) => [].concat(...a.map(a => b.map(b => [].concat(a, b))));
let cartesianProduct_rsp = (a, b, ...c) => b ? cartesianProduct_rsp(_f(a, b), ...c) : a;
//------------------------------------------------------------------------------
function cartesianProduct_sebnukem(a) {
    var i, j, l, m, a1, o = [];
    if (!a || a.length == 0) return a;

    a1 = a.splice(0, 1)[0];
    a = cartesianProduct_sebnukem(a);
    for (i = 0, l = a1.length; i < l; i++) {
        if (a && a.length) for (j = 0, m = a.length; j < m; j++)
            o.push([a1[i]].concat(a[j]));
        else
            o.push([a1[i]]);
    }
    return o;
}
//------------------------------------------------------------------------------
const L = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const args = [L, L, L, L, L, L];

let fns = {
    'cartesianProduct': function(args){ return cartesianProduct(...args); },
    'cartesianProduct_rsp': function(args){ return cartesianProduct_rsp(...args); },
    'cartesianProduct_sebnukem': function(args){ return cartesianProduct_sebnukem(args); }
};

Object.keys(fns).forEach(fname => {
    console.time(fname);
    const ret = fns[fname](args);
    console.timeEnd(fname);
});

з node v6.12.2, я отримую такі таймінги:

cartesianProduct: 427.378ms
cartesianProduct_rsp: 1710.829ms
cartesianProduct_sebnukem: 593.351ms

3

Для тих, хто потребує TypeScript (повторно доповнено @ відповідь Денні)

/**
 * Calculates "Cartesian Product" sets.
 * @example
 *   cartesianProduct([[1,2], [4,8], [16,32]])
 *   Returns:
 *   [
 *     [1, 4, 16],
 *     [1, 4, 32],
 *     [1, 8, 16],
 *     [1, 8, 32],
 *     [2, 4, 16],
 *     [2, 4, 32],
 *     [2, 8, 16],
 *     [2, 8, 32]
 *   ]
 * @see https://stackoverflow.com/a/36234242/1955709
 * @see https://en.wikipedia.org/wiki/Cartesian_product
 * @param arr {T[][]}
 * @returns {T[][]}
 */
function cartesianProduct<T> (arr: T[][]): T[][] {
  return arr.reduce((a, b) => {
    return a.map(x => {
      return b.map(y => {
        return x.concat(y)
      })
    }).reduce((c, d) => c.concat(d), [])
  }, [[]] as T[][])
}

2

Просто для вибору реальна проста реалізація з використанням масиву reduce:

const array1 = ["day", "month", "year", "time"];
const array2 = ["from", "to"];
const process = (one, two) => [one, two].join(" ");

const product = array1.reduce((result, one) => result.concat(array2.map(two => process(one, two))), []);

2

Сучасний JavaScript у кількох рядках. Ніяких зовнішніх бібліотек або залежностей, таких як Лодаш.

function cartesian(...arrays) {
  return arrays.reduce((a, b) => a.flatMap(x => b.map(y => x.concat([y]))), [ [] ]);
}

console.log(
  cartesian([1, 2], [10, 20], [100, 200, 300])
    .map(arr => JSON.stringify(arr))
    .join('\n')
);


2

Ви можете reduce2D масив. Використовуйте flatMapна масиві акумуляторів, щоб отримати acc.length x curr.lengthкількість комбінацій у кожному циклі. [].concat(c, n)використовується тому c, що це число в першій ітерації, а масив - згодом.

const data = [ [1, 2], [10, 20], [100, 200, 300] ];

const output = data.reduce((acc, curr) =>
  acc.flatMap(c => curr.map(n => [].concat(c, n)))
)

console.log(JSON.stringify(output))

(На основі відповіді Ніни Шольц )


1

Нерекурсивний підхід, який додає можливість фільтрувати та змінювати продукти, перш ніж фактично додавати їх до набору результатів. Зверніть увагу на використання .map, а не .forEach. У деяких браузерах .map працює швидше.

function crossproduct(arrays,rowtest,rowaction) {
      // Calculate the number of elements needed in the result
      var result_elems = 1, row_size = arrays.length;
      arrays.map(function(array) {
            result_elems *= array.length;
      });
      var temp = new Array(result_elems), result = [];

      // Go through each array and add the appropriate element to each element of the temp
      var scale_factor = result_elems;
      arrays.map(function(array)
      {
        var set_elems = array.length;
        scale_factor /= set_elems;
        for(var i=result_elems-1;i>=0;i--) {
            temp[i] = (temp[i] ? temp[i] : []);
            var pos = i / scale_factor % set_elems;
            // deal with floating point results for indexes, this took a little experimenting
            if(pos < 1 || pos % 1 <= .5) {
                pos = Math.floor(pos);
            } else {
                pos = Math.min(array.length-1,Math.ceil(pos));
            }
            temp[i].push(array[pos]);
            if(temp[i].length===row_size) {
                var pass = (rowtest ? rowtest(temp[i]) : true);
                if(pass) {
                    if(rowaction) {
                        result.push(rowaction(temp[i]));
                    } else {
                        result.push(temp[i]);
                    }
                }
            }
        }
      });
      return result;
    }

1

Просте "розумне та візуально друге" рішення.

введіть тут опис зображення


// t = [i, length]

const moveThreadForwardAt = (t, tCursor) => {
  if (tCursor < 0)
    return true; // reached end of first array

  const newIndex = (t[tCursor][0] + 1) % t[tCursor][1];
  t[tCursor][0] = newIndex;

  if (newIndex == 0)
    return moveThreadForwardAt(t, tCursor - 1);

  return false;
}

const cartesianMult = (...args) => {
  let result = [];
  const t = Array.from(Array(args.length)).map((x, i) => [0, args[i].length]);
  let reachedEndOfFirstArray = false;

  while (false == reachedEndOfFirstArray) {
    result.push(t.map((v, i) => args[i][v[0]]));

    reachedEndOfFirstArray = moveThreadForwardAt(t, args.length - 1);
  }

  return result;
}

// cartesianMult(
//   ['a1', 'b1', 'c1'],
//   ['a2', 'b2'],
//   ['a3', 'b3', 'c3'],
//   ['a4', 'b4']
// );

console.log(cartesianMult(
  ['a1'],
  ['a2', 'b2'],
  ['a3', 'b3']
));

1

Проста, модифікована версія коду @ viebel у простому Javascript:

function cartesianProduct(...arrays) {
  return arrays.reduce((a, b) => {
    return [].concat(...a.map(x => {
      const next = Array.isArray(x) ? x : [x];
      return [].concat(b.map(y => next.concat(...[y])));
    }));
  });
}

const product = cartesianProduct([1, 2], [10, 20], [100, 200, 300]);

console.log(product);
/*
[ [ 1, 10, 100 ],
  [ 1, 10, 200 ],
  [ 1, 10, 300 ],
  [ 1, 20, 100 ],
  [ 1, 20, 200 ],
  [ 1, 20, 300 ],
  [ 2, 10, 100 ],
  [ 2, 10, 200 ],
  [ 2, 10, 300 ],
  [ 2, 20, 100 ],
  [ 2, 20, 200 ],
  [ 2, 20, 300 ] ];
*/

1

Більш зрозуміла реалізація

function productOfTwo(one, two) {
  return one.flatMap(x => two.map(y => [].concat(x, y)));
}

function product(head = [], ...tail) {
  if (tail.length === 0) return head;
  return productOfTwo(head, product(...tail));
}

const test = product(
  [1, 2, 3],
  ['a', 'b']
);

console.log(JSON.stringify(test));


1
f=(a,b,c)=>a.flatMap(ai=>b.flatMap(bi=>c.map(ci=>[ai,bi,ci])))

Це для 3 масивів.
Деякі відповіді дали можливість для будь-якої кількості масивів.
Це може легко скоротити або розширитись до меншої чи більшої кількості масивів.
Мені потрібні були комбінації одного набору з повторами, тому я міг використовувати:

f(a,a,a)

але використовується:

f=(a,b,c)=>a.flatMap(a1=>a.flatMap(a2=>a.map(a3=>[a1,a2,a3])))

0

Я помітив, що ніхто не розміщував рішення, яке дозволяє передавати функцію для обробки кожної комбінації, тому ось моє рішення:

const _ = require('lodash')

function combinations(arr, f, xArr = []) {
    return arr.length>1 
    ? _.flatMap(arr[0], x => combinations(arr.slice(1), f, xArr.concat(x)))
    : arr[0].map(x => f(...xArr.concat(x)))
}

// use case
const greetings = ["Hello", "Goodbye"]
const places = ["World", "Planet"]
const punctuationMarks = ["!", "?"]
combinations([greetings,places,punctuationMarks], (greeting, place, punctuationMark) => `${greeting} ${place}${punctuationMark}`)
  .forEach(row => console.log(row))

Вихід:

Hello World!
Hello World?
Hello Planet!
Hello Planet?
Goodbye World!
Goodbye World?
Goodbye Planet!
Goodbye Planet?

0

Звичайний підхід JS грубої сили, який приймає масив масивів як вхід.

var cartesian = function(arrays) {
    var product = [];
    var precals = [];
    var length = arrays.reduce(function(acc, curr) {
        return acc * curr.length
    }, 1);
    for (var i = 0; i < arrays.length; i++) {
        var array = arrays[i];
        var mod = array.length;
        var div = i > 0 ? precals[i - 1].div * precals[i - 1].mod : 1;
        precals.push({
            div: div,
            mod: mod
        });
    }
    for (var j = 0; j < length; j++) {
        var item = [];
        for (var i = 0; i < arrays.length; i++) {
            var array = arrays[i];
            var precal = precals[i];
            var k = (~~(j / precal.div)) % precal.mod;
            item.push(array[k]);
        }
        product.push(item);
    }
    return product;
};

cartesian([
    [1],
    [2, 3]
]);

cartesian([
    [1],
    [2, 3],
    [4, 5, 6]
]);

0

var chars = ['A', 'B', 'C']
var nums = [1, 2, 3]

var cartesianProduct = function() {
  return _.reduce(arguments, function(a, b) {
    return _.flatten(_.map(a, function(x) {
      return _.map(b, function(y) {
        return x.concat(y);
      });
    }), true);
  }, [
    []
  ]);
};

console.log(cartesianProduct(chars, nums))
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>

Щойно перетворена відповідь @ dummersl з CoffeScript в JavaScript. Це просто працює.

var chars = ['A', 'B', 'C']
var nums = [1, 2, 3]

var cartesianProduct = function() {
  return _.reduce(arguments, function(a, b) {
    return _.flatten(_.map(a, function(x) {
      return _.map(b, function(y) {
        return x.concat(y);
      });
    }), true);
  }, [[]]);
};

console.log( cartesianProduct(chars, nums) )

0

Ще одна реалізація. Не найкоротший або вигадливий, але швидкий:

function cartesianProduct() {
    var arr = [].slice.call(arguments),
        intLength = arr.length,
        arrHelper = [1],
        arrToReturn = [];

    for (var i = arr.length - 1; i >= 0; i--) {
        arrHelper.unshift(arrHelper[0] * arr[i].length);
    }

    for (var i = 0, l = arrHelper[0]; i < l; i++) {
        arrToReturn.push([]);
        for (var j = 0; j < intLength; j++) {
            arrToReturn[i].push(arr[j][(i / arrHelper[j + 1] | 0) % arr[j].length]);
        }
    }

    return arrToReturn;
}

0

Бібліотеки не потрібні! :)

Потрібні функції стрілок, хоча і, мабуть, не настільки ефективні. : /

const flatten = (xs) => 
    xs.flat(Infinity)

const binaryCartesianProduct = (xs, ys) =>
    xs.map((xi) => ys.map((yi) => [xi, yi])).flat()

const cartesianProduct = (...xss) =>
    xss.reduce(binaryCartesianProduct, [[]]).map(flatten)
      
console.log(cartesianProduct([1,2,3], [1,2,3], [1,2,3]))


0

Для запису

Ось мова йде про мою версію. Я зробив це за допомогою найпростішого ітератора JavaScript "for ()", тому він сумісний у кожному випадку та має найкращі показники.

function cartesian(arrays){
    var quant = 1, counters = [], retArr = [];

    // Counts total possibilities and build the counters Array;
    for(var i=0;i<arrays.length;i++){
        counters[i] = 0;
        quant *= arrays[i].length;
    }

    // iterate all possibilities
    for(var i=0,nRow;i<quant;i++){
        nRow = [];
        for(var j=0;j<counters.length;j++){
            if(counters[j] < arrays[j].length){
                nRow.push(arrays[j][counters[j]]);
            } else { // in case there is no such an element it restarts the current counter
                counters[j] = 0;
                nRow.push(arrays[j][counters[j]]);
            }
            counters[j]++;
        }
        retArr.push(nRow);
    }
    return retArr;
}

З найкращими побажаннями.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.