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], ...] 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], ...] 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
.map , .reduce , .concat... and 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]) ) ) ), [[]] ); 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:
массив.map(маппер) ↔ _.map(массив, маппер)массив.reduce(редьюсер) ↔ _.reduce(массив, редьюсер)[].concat(...массив) ↔ _.flatten(массив) 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; } 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; } // 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> 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.
In some libraries there is a Cartesian product function:
cartesianProductcross (only for two arrays)Source: https://ru.stackoverflow.com/questions/778359/
All Articles