Декартово произведение нескольких массивов

240
01 февраля 2018, 20:01

Как можно реализовать декартово произведение нескольких массивов в JavaScript?

// например
cartesian([1,2], [10,20], [100,200,300])
// будет равно
// [[1, 10, 100], [1, 10, 200], [1, 10, 300], [1, 20, 100], [1, 20, 200], ...]
Answer 1
Самый короткий способ на чистом 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), [[]]);

Используются

  • методы для массивов .map, .reduce, .concat
  • оператор расширения ... и
  • синтаксис оставшихся аргументов
То же самое в виде одной функции с комментариями
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])
                    )
                )
            ),
        [[]]
    );
То же самое с использованием методов lodash
const cartesian = (...arrays) =>
    _.reduce(arrays, (a, b) =>
            _.flatten(
                _.map(a, ai =>
                    _.map(b, bi =>
                        ai.concat([bi])
                    )
                )
            ),
        [[]]
    );

Здесь методы JavaScript заменены на эквивалентные из 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;
}
Рекурсивный способ

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

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>

Замечание о пустом декартовом произведении

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

Готовые библиотечные реализации

В некоторых библиотеках существует функция декартова произведения:

  • js-combinatorics → cartesianProduct
  • cartesian-product — npm, github
  • d3-array → cross (только для двух массивов)
READ ALSO
JS, второй уровень вложенности массива

JS, второй уровень вложенности массива

ЗдравствуйтеУ меня затуп :)

149
Вопрос по позиционированию css/js

Вопрос по позиционированию css/js

Насколько я знаю свойство position: fixed позиционируется относительно объекта window то есть окна браузераВопрос в следующем: как можно спозиционировать...

181
Slick slider или как фиксить баг свойства centerMode?

Slick slider или как фиксить баг свойства centerMode?

Доброго времени суток! Есть некоторые баги slick-слайдера, возможно кто-то стыкался или подскажет как это обойтиКак всё обустроено: Делаю слайдер...

526
проверка полей на заполненость

проверка полей на заполненость

Ребят доброй ночи! Подскажите , пишу проверку полей на заполненность в табах, нужно что бы пока поля не заполнены нельзя было переключаться...

216