How can I implement a Cartesian product of several arrays in JavaScript?

// например cartesian([1,2], [10,20], [100,200,300]) // будет равно // [[1, 10, 100], [1, 10, 200], [1, 10, 300], [1, 20, 100], [1, 20, 200], ...] 

1 answer 1

The shortest way to pure javascript

 const f = (a, b) => [].concat(...a.map(ai => b.map(bi => ai.concat([bi])))); const cartesian = (...arrays) => arrays.reduce((a, b) => f(a, b), [[]]); 

Are used

The same as a single function with comments.

 const cartesian = (...arrays) => // итеративно получаем декартово произведение // нескольких первых массивов из arrays, // начиная с нуля массивов и пустого декартова произведения --- [[]] arrays.reduce((cartesianPart, array) => // сглаживание массива массивов [].concat(... // cartesianPart --- декартово произведение нескольких первых массивов из arrays cartesianPart.map(cartesianPartTuple => // array --- новый массив из arrays для добавления в декартово произведение array.map(arrayElement => // cartesianPartTuple --- массив-префикс одного из элементов декартова произведения // arrayElement --- элемент одного из массива из arrays cartesianPartTuple.concat([arrayElement]) ) ) ), [[]] ); 

The same with lodash methods .

 const cartesian = (...arrays) => _.reduce(arrays, (a, b) => _.flatten( _.map(a, ai => _.map(b, bi => ai.concat([bi]) ) ) ), [[]] ); 

Here, JavaScript methods are replaced by equivalent ones from lodash:

Non-recursive method with explicit construction of an element of a Cartesian product by its number

 function cartesian(...arrays) { // находим число элементов в декартовом произведении let resultLength = 1; for (let array of arrays) { resultLength *= array.length; } // создаём массив такого размера и перебираем номер элемента let result = new Array(resultLength); for (let i = 0; i < resultLength; ++i) { // один элемент декартова произведения let element = new Array(arrays.length); let elementIndex = i; // цикл в обратном порядке нужен чтобы элементы начинали изменяться с конца for (let j = arrays.length - 1; j >= 0; --j) { const array = arrays[j]; // имеем биекцию между индексами элементов декартова произведения (числа от 0 до resultLength-1) // и кортежами длины arrays.length, в которых каждый элемент — индекс в соответствующем массиве element[j] = array[elementIndex % array.length]; // целочисленное деление elementIndex = Math.floor(elementIndex / array.length); } result[i] = element; } return result; } 

Recursive way

Using recursion, Cartesian product elements will be created: starting with an empty array, each element of the current array is searched at each recursion step, a copy of the typed prefix of the Cartesian product element is created, an array element is added to the copy and a recursive call is made on the received new prefix.

 function cartesian(...arrays) { let result = []; // функция, которая будет рекурсивно вызываться // глубина рекурсии равна arrays.length // в процессе рекурсии функция будет создавать часть элемента декартова произведения // в конце рекусрии функция добавит созданный элемент в массив result const recursion = (tuplePart) => { if (tuplePart.length === arrays.length) { result.push(tuplePart); } else { const array = arrays[tuplePart.length]; for (let element of array) { // создаём копию tuplePart и добавляем в неё очередной элемент const tuplePartWithNewElement = tuplePart.concat([element]); recursion(tuplePartWithNewElement); } } }; recursion([]); return result; } 

Snippet, which verifies the correctness of all the proposed methods

 // cartesian 1 const f = (a, b) => [].concat(...a.map(ai => b.map(bi => ai.concat([bi])))); const cartesian1 = (...arrays) => arrays.reduce((a, b) => f(a, b), [[]]); // cartesian 2 const cartesian2 = (...arrays) => // итеративно получаем декартово произведение // нескольких первых массивов из arrays, // начиная с нуля массивов и пустого декартова произведения --- [[]] arrays.reduce((cartesianPart, array) => // сглаживание массива массивов [].concat(... // cartesianPart --- декартово произведение нескольких первых массивов из arrays cartesianPart.map(cartesianPartTuple => // array --- новый массив из arrays для добавления в декартово произведение array.map(arrayElement => // cartesianPartTuple --- массив-префикс одного из элементов декартова произведения // arrayElement --- элемент одного из массива из arrays cartesianPartTuple.concat([arrayElement]) ) ) ), [[]] ); // cartesian 3 const cartesian3 = (...arrays) => _.reduce(arrays, (a, b) => _.flatten( _.map(a, ai => _.map(b, bi => ai.concat([bi]) ) ) ), [[]] ); // cartesian 4 function cartesian4(...arrays) { // находим число элементов в декартовом произведении let resultLength = 1; for (let array of arrays) { resultLength *= array.length; } // создаём массив такого размера и перебираем номер элемента let result = new Array(resultLength); for (let i = 0; i < resultLength; ++i) { // один элемент декартова произведения let tuple = new Array(arrays.length); let tupleIndex = i; // цикл в обратном порядке нужен чтобы элементы начинали изменяться с конца for (let j = arrays.length - 1; j >= 0; --j) { const array = arrays[j]; // имеем биекцию между индексами элементов декартова произведения (числа от 0 до resultLength-1) // и кортежами длины arrays.length, в которых каждый элемент — индекс в соответствующем массиве tuple[j] = array[tupleIndex % array.length]; // целочисленное деление tupleIndex = Math.floor(tupleIndex / array.length); } result[i] = tuple; } return result; } // cartesian 5 function cartesian5(...arrays) { let result = []; // функция, которая будет рекурсивно вызываться // глубина рекурсии равна arrays.length // в процессе рекурсии функция будет создавать часть элемента декартова произведения // в конце рекусрии функция добавит созданный элемент в массив result const recursion = (tuplePart) => { if (tuplePart.length === arrays.length) { result.push(tuplePart); } else { const array = arrays[tuplePart.length]; for (let element of array) { // создаём копию tuplePart и добавляем в неё очередной элемент const tuplePartWithNewElement = tuplePart.concat([element]); recursion(tuplePartWithNewElement); } } }; recursion([]); return result; } // tests const cartesians = [ cartesian1, cartesian2, cartesian3, cartesian4, cartesian5 ]; const tests = [ { name: 'product of single array', input: [ [1, 2, 3] ], output: [ [1], [2], [3] ] }, { name: 'product of two arrays', input: [ [1, 2, 3], [10, 20] ], output: [ [1, 10], [1, 20], [2, 10], [2, 20], [3, 10], [3, 20] ] }, { name: 'product of three arrays', input: [ [1, 2], [10, 20], [100, 200, 300] ], output: [ [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] ] }, { name: 'nested arrays', input: [ [[1], [2], [3]], [[10], [20]] ], output: [ [[1], [10]], [[1], [20]], [[2], [10]], [[2], [20]], [[3], [10]], [[3], [20]] ] } ]; console.log('если нет сообщений об ошибке, то все функции работают правильно'); cartesians.forEach((cartesian, index) => { for (let test of tests) { const output = cartesian(...test.input); const ok = _.isEqual(output, test.output); if (!ok) { console.log(`FAIL: cartesian function #${index + 1} for test ${test.name}`); console.log(` expected: ${JSON.stringify(test.output)}`); console.log(` received: ${JSON.stringify(output)}`); } } }); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script> 

Note on the empty Cartesian product

Almost all the proposed methods incorrectly handle the empty Cartesian product, that is, the call to the cartesian function without arguments. The Cartesian product of zero sets is equal to the empty set, in our case it is an empty array. If you wish, you can add a check for this.

Ready library implementations

In some libraries there is a Cartesian product function: