Iloczyn kartezjański wielu tablic w JavaScript


112

Jak zaimplementowałbyś iloczyn kartezjański wielu tablic w JavaScript?

Jako przykład,

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

powinien wrócić

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


3
Zaimplementowano to w module js-combinatorics: github.com/dankogai/js-combinatorics
Erel Segal-Halevi


Zgadzam się z underscore.js, ale nie jestem pewien, jak pomoże usunięcie tagu programowania funkcjonalnego @le_m
viebel

Fwiw, d3 dodane d3.cross(a, b[, reducer])w lutym. github.com/d3/d3-array#cross
Toph

Odpowiedzi:


106

Aktualizacja 2017: 2-wierszowa odpowiedź z waniliowym JS

Wszystkie odpowiedzi tutaj są zbyt skomplikowane , większość z nich zajmuje 20 linii kodu lub nawet więcej.

W tym przykładzie użyto tylko dwóch wierszy zwykłego JavaScript , żadnych bibliotek lodash, podkreślenia ani innych:

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;

Aktualizacja:

Jest to to samo, co powyżej, ale poprawione, aby ściśle przestrzegać Przewodnika po stylach JavaScript Airbnb - zweryfikowanego za pomocą ESLint z 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);

Specjalne podziękowania dla ZuBB za poinformowanie mnie o problemach lintera z oryginalnym kodem.

Przykład

Oto dokładny przykład z Twojego pytania:

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

Wynik

Oto wynik tego polecenia:

[ [ 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 ] ]

Próbny

Zobacz dema na:

Składnia

Składnia, której tutaj użyłem, nie jest niczym nowym. Mój przykład wykorzystuje operator spreadu i pozostałe parametry - cechy JavaScript zdefiniowane w 6. edycji standardu ECMA-262 opublikowanego w czerwcu 2015 roku i opracowanego znacznie wcześniej, lepiej znanego jako ES6 lub ES2015. Widzieć:

To sprawia, że ​​taki kod jest tak prosty, że grzechem jest go nie używać. W przypadku starych platform, które nie obsługują go natywnie, zawsze możesz użyć Babel lub innych narzędzi, aby przenieść go do starszej składni - i w rzeczywistości mój przykład transponowany przez Babel jest nadal krótszy i prostszy niż większość przykładów tutaj, ale tak nie jest naprawdę ważne, ponieważ efekt transpilacji nie jest czymś, co musisz zrozumieć lub utrzymać, to po prostu fakt, który wydałem mi się interesujący.

Wniosek

Nie ma potrzeby pisania setek linii kodu, który jest trudny w utrzymaniu i nie ma potrzeby używania całych bibliotek do tak prostej rzeczy, kiedy dwie linie zwykłego JavaScript mogą z łatwością wykonać zadanie. Jak widać, naprawdę opłaca się korzystać z nowoczesnych funkcji języka, aw przypadkach, gdy potrzebujesz obsługi archaicznych platform bez natywnego wsparcia dla nowoczesnych funkcji, zawsze możesz użyć Babel lub innych narzędzi do przeniesienia nowej składni do starej .

Nie koduj jak w 1995 roku

JavaScript ewoluuje i nie bez powodu. TC39 wykonuje niesamowitą robotę przy projektowaniu języka, dodając nowe funkcje, a dostawcy przeglądarek wykonują niesamowitą robotę, implementując te funkcje.

Aby zobaczyć aktualny stan natywnej obsługi dowolnej funkcji w przeglądarkach, zobacz:

Aby zobaczyć obsługę w wersjach Node, zobacz:

Aby używać nowoczesnej składni na platformach, które nie obsługują jej natywnie, użyj Babel:


Oto wersja maszynopisu z niewielką zmianą uwzględniającą sposób, w jaki maszynopis rozdziela tablice. gist.github.com/ssippe/1f92625532eef28be6974f898efb23ef
Sam

1
@rsp wielkie dzięki za naprawdę dobrą odpowiedź. chociaż chciałbym prosić o trochę ulepszenie, aby uzyskać zestaw ostrzeżeń o zmiennych cieniowanych (2 zmienne lokalne ai 2 zmienne lokalne b)
ZuBB

7
„Nie programuj jak w 1995 roku” - nie musisz być nieprzyjemny, jeszcze nie wszyscy nadrobili zaległości.
Godwhacker

7
Jest to w porządku, ale zawodzi, gdy jest karmione, ['a', 'b'], [1,2], [[9], [10]]co [ [ 'a', 1, 9 ], [ 'a', 1, 10 ], [ 'a', 2, 9 ], [ 'a', 2, 10 ], [ 'b', 1, 9 ], [ 'b', 1, 10 ], [ 'b', 2, 9 ], [ 'b', 2, 10 ] ]w rezultacie daje plon . Mam na myśli, że nie zatrzymam tego typu przedmiotów [[9], [10]].
Redu

1
Skoro ...już używamy , czy nie powinno [].concat(...[array])stać się po prostu [...array]?
Lazar Ljubenović

89

Oto funkcjonalne rozwiązanie problemu (bez żadnej zmiennej zmiennej !) Przy użyciu reducei flatten, zapewniane przez 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>

Uwaga: to rozwiązanie zostało zainspirowane http://cwestblog.com/2011/05/02/cartesian-product-of-multiple-arrays/


W tej odpowiedzi jest literówka, nie powinno być „, prawda” (może lodash zmienił się od czasu, gdy napisałeś ten post?)
Chris Jefferson

@ChrisJefferson drugim parametrem flattenjest spłaszczenie płyt . Tutaj jest to obowiązkowe!
viebel

4
Przepraszamy, to jest niezgodność lodash / podkreślenia, zamienili się flagą.
Chris Jefferson

1
Dlatego podczas spłaszczania używaj truez podkreśleniem i używaj falsez lodash, aby zapewnić płytkie spłaszczenie.
Akseli Palén

Jak zmodyfikować tę funkcję, aby akceptowała tablice tablic?

44

Oto zmodyfikowana wersja kodu @ viebel w zwykłym JavaScript, bez użycia żadnej biblioteki:

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
Błąd dla produktu kartezjańskiego ([[[1], [2], [3]], ['a', 'b'], [['gamma'], [['alpha']]], ['zii', „faa”]]), ponieważ spłaszcza [„gamma”] do „gamma” i [[„alpha”]] do [„alpha”]
Mzn

ponieważ .concat(y)zamiast.concat([ y ])
Dziękuję

@ Dziękuję, możesz edytować odpowiedź bezpośrednio, zamiast komentować, po prostu zrobiłem to, więc nie ma potrzeby teraz: P
Olivier Lalonde

28

wydaje się, że społeczność uważa, że ​​jest to trywialne i / lub łatwe do znalezienia implementacji referencyjnej, po krótkiej inspekcji nie mogłem, a może po prostu lubię wymyślać na nowo koło lub rozwiązywać problemy programistyczne podobne do klasowych, tak czy inaczej to twój szczęśliwy dzień :

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]
   ]

pełna implementacja referencyjna, która jest stosunkowo wydajna ... :-D

na wydajności: możesz trochę zyskać, wyjmując if z pętli i mając 2 oddzielne pętle, ponieważ jest on technicznie stały i pomagałbyś w przewidywaniu gałęzi i całym tym bałaganie, ale ten punkt jest trochę dyskusyjny w javascript

ktokolwiek, ciesz się -ck


1
Dzięki @ckoz za szczegółową odpowiedź. Dlaczego nie miałbyś użyć reducefunkcji tablicy?
viebel

1
@viebel, dlaczego chcesz użyć redukuj? po pierwsze ,redred ma bardzo słabe wsparcie dla starszych przeglądarek (patrz: developer.mozilla.org/en-US/docs/JavaScript/Reference/… ), aw każdym razie czy ten szalony kod z tej drugiej odpowiedzi faktycznie wygląda dla ciebie czytelnie ? mnie to nie dotyczy. pewnie, że jest krótszy, ale po zminimalizowaniu ten kod miałby mniej więcej taką samą długość, łatwiejszy do debugowania / optymalizacji, po drugie wszystkie te rozwiązania „redukuj” rozkładają się na to samo, z wyjątkiem tego, że mają wyszukiwanie zamknięcia (teoretycznie wolniej), jest też trudniejsze zaprojektować tak, aby obsługiwał nieskończone zestawy ...
ckozl

5
Stworzyłem 2+ razy szybszą i (imo) czystszą wersję: pastebin.com/YbhqZuf7 Osiąga wzrost prędkości, nie używając result = result.concat(...)i nie używając args.slice(1). Niestety nie mogłem znaleźć sposobu na pozbycie się curr.slice()i rekursji.
Pauan

2
@Pauan dobra robota, niezła redukcja hot-spotów w sumie w lidze o 10% -50% wzrost wydajności w oparciu o to, co widzę. Nie mogę jednak mówić o „czystości”, uważam, że w rzeczywistości trudniej jest śledzić Twoją wersję ze względu na użycie zmiennych zakresu domknięcia. Ale ogólnie rzecz biorąc, bardziej wydajny kod jest trudniejszy do naśladowania. Oryginalną wersję napisałem dla czytelności, żałuję, że nie miałem więcej czasu, abym mógł cię wciągnąć w przedstawienie;) może później ...
ckozl

to naprawdę jest jeden z tych problemów
James

26

Następująca wydajna funkcja generatora zwraca iloczyn kartezjański wszystkich podanych iterable :

// 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]));

Akceptuje tablice, ciągi znaków, zestawy i wszystkie inne obiekty implementujące iterowalny protokół .

Zgodnie ze specyfikacją z kartezjańskim produktu N-ary zwracającej

  • []jeśli jedna lub więcej podanych iterable jest pustych, np. []lub''
  • [[a]]jeśli apodana jest pojedyncza iterowalna zawierająca jedną wartość .

Wszystkie inne przypadki są obsługiwane zgodnie z oczekiwaniami, co pokazują następujące przypadki testowe:


Czy możesz wyjaśnić, co się na nim dzieje? Wielkie dzięki!
LeandroP

Dzięki za nauczenie nas cudownego przykładu użycia funkcji generatora + rekurencji ogonowej + dwuwarstwowych pętli! Ale pozycja pierwszej pętli for w kodzie musi zostać zmieniona, aby kolejność wyjściowych tablic podrzędnych była poprawna. Naprawiono kod: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 Jeśli chcesz odtworzyć kolejność krotek produktów kartezjańskich podaną w komentarzu OP, to Twoja modyfikacja jest poprawna. Jednak kolejność krotek w produkcie zwykle nie ma znaczenia, np. Matematycznie wynikiem jest nieuporządkowany zbiór. Wybrałem tę kolejność, ponieważ wymaga znacznie mniej wywołań rekurencyjnych i dlatego jest nieco bardziej wydajna - jednak nie uruchomiłem testu porównawczego.
le_m

Erratum: W moim komentarzu powyżej, „rekurencja ogona” powinna być „rekurencją” (nie jest to wywołanie ogonowe w tym przypadku).
ooo

21

Oto proste, proste rozwiązanie rekurencyjne:

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
Ten okazuje się być najbardziej wydajnym czystym kodem JS w tym temacie. Wykończenie macierzy 3 x 100 elementów w celu utworzenia tablicy o długości 1M zajmuje około 600 ms.
Redu

1
Działa dla produktu kartezjańskiego ([[[1], [2], [3]], ['a', 'b'], [['gamma'], [['alpha']]], ['zii', „faa”]]); bez spłaszczania oryginalnych wartości
Mzn

10

Oto rekurencyjny sposób korzystania z funkcji generatora ECMAScript 2015 , dzięki czemu nie musisz tworzyć wszystkich krotek naraz:

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


To nie zadziała, gdy jedna z tablic ma elementy tablicy, takie jakcartesian([[1],[2]],[10,20],[100,200,300])
Redu

@Redu Odpowiedź została zaktualizowana, aby obsługiwać argumenty tablicowe.
heenenee

Tak, .concat()wbudowany operator rozprzestrzeniania się czasami może stać się oszustwem.
Redu

10

Oto jedna linijka korzystająca z natywnego ES2019 flatMap. Żadne biblioteki nie są potrzebne, tylko nowoczesna przeglądarka (lub transpiler):

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

Zasadniczo jest to nowoczesna wersja odpowiedzi Viebel, bez lodash.


Z pewnością żadna biblioteka nie była potrzebna. Ale to też nie jest najbardziej czytelny kod w historii. To kompromis.
Arturo Hernandez

Czytelność ma w tym przypadku więcej wspólnego z moim wyborem użycia operatora spreadu, jak sądzę, a nie z wyborem nieużywania biblioteki. Nie sądzę, aby lodash w ogóle prowadził do bardziej czytelnego kodu.
Fred Kleuver

9

Używając typowego backtrackingu z generatorami 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%; }

Poniżej podobna wersja kompatybilna ze starszymi przeglądarkami.


9

To jest czyste rozwiązanie ES6 wykorzystujące funkcje strzałek

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

var arr = [[1, 2], [10, 20], [100, 200, 300]];
console.log(JSON.stringify(cartesianProduct(arr)));


7

Wersja Coffeescript z lodash:

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

7

Podejście jednoliniowe dla lepszego czytania z wcięciami.

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

Zajmuje pojedynczą tablicę z tablicami poszukiwanych elementów kartezjańskich.

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


1
Musiałem dodać instrukcję guard, aby poprawnie obsłużyć przypadek, w którym tablica ma pojedynczy element:if (arr.length === 1) return arr[0].map(el => [el]);
JacobEvelyn

5

To jest oznaczone jako programowanie funkcjonalne, więc spójrzmy na monadę List :

Jedna aplikacja dla tej monadycznej listy reprezentuje obliczenia niedeterministyczne. List może przechowywać wyniki dla wszystkich ścieżek wykonywania w algorytmie ...

Cóż, to brzmi jak idealne dopasowanie cartesian. JavaScript daje nam Arrayi monadyczną funkcją wiążącą jest Array.prototype.flatMap, więc użyjmy ich -

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]))

Zamiast looppowyższego tmożna dodać jako parametr curried -

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

Niektóre odpowiedzi w tym temacie kończą się niepowodzeniem, gdy którakolwiek z tablic wejściowych zawiera element tablicy. Lepiej to sprawdź.

W każdym razie nie ma potrzeby podkreślania, chleba w ogóle. Uważam, że ten powinien zrobić to z czystym JS ES6, tak funkcjonalnym, jak to tylko możliwe.

Ten fragment kodu używa mapy redukującej i zagnieżdżonej, aby po prostu uzyskać iloczyn kartezjański dwóch tablic, jednak druga tablica pochodzi z rekurencyjnego wywołania tej samej funkcji z jedną tablicą mniej; W związku z tym.. 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

W moim konkretnym miejscu podejście „staromodne” wydawało się skuteczniejsze niż metody oparte na bardziej nowoczesnych funkcjach. Poniżej znajduje się kod (w tym małe porównanie z innymi rozwiązaniami zamieszczonymi w tym wątku przez @rsp i @sebnukem), jeśli okaże się przydatny także komuś innemu.

Pomysł jest następujący. Powiedzmy, że tworzymy iloczyn zewnętrzny Ntablic, a_1,...,a_Nz których każda ma m_ikomponenty. Iloczyn zewnętrzny tych tablic ma M=m_1*m_2*...*m_Nelementy i każdy z nich możemy zidentyfikować za pomocą N-wektora wymiarowego, którego składowymi są dodatnie liczby całkowite, a i-ty składnik jest ściśle ograniczony od góry przez m_i. Na przykład wektor (0, 0, ..., 0)odpowiadałby określonej kombinacji, w której bierze się pierwszy element z każdej tablicy, podczas gdy (m_1-1, m_2-1, ..., m_N-1)jest identyfikowany za pomocą kombinacji, w której pobiera się ostatni element z każdej tablicy. Tak więc, aby zbudować wszystkoM kombinacje, poniższa funkcja konstruuje kolejno wszystkie takie wektory i dla każdego z nich identyfikuje odpowiednią kombinację elementów tablic wejściowych.

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

z node v6.12.2, otrzymuję następujące czasy:

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

3

Dla tych, którzy potrzebują TypeScript (reimplemented @ Danny's answer)

/**
 * 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

Dla wyboru prawdziwa prosta implementacja przy użyciu tablic 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

Nowoczesny JavaScript w zaledwie kilku wierszach. Żadnych bibliotek zewnętrznych ani zależności, takich jak Lodash.

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

Możesz mieć reducetablicę 2D. Użyj flatMapna tablicy akumulatorów, aby uzyskać acc.length x curr.lengthliczbę kombinacji w każdej pętli. [].concat(c, n)jest używany, ponieważ cjest liczbą w pierwszej iteracji, a następnie tablicą.

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

(To jest oparte na odpowiedzi Niny Scholz )


1

Podejście nierekurencyjne, które dodaje możliwość filtrowania i modyfikowania produktów przed faktycznym dodaniem ich do zestawu wyników. Zwróć uwagę na użycie .map zamiast .forEach. W niektórych przeglądarkach .map działa szybciej.

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

Proste rozwiązanie „przyjazne dla umysłu i wizualne”.

wprowadź opis obrazu tutaj


// 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

Prosta, zmodyfikowana wersja kodu @ viebel w prostym języku 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

Bardziej czytelna implementacja

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])))

Dotyczy to 3 tablic.
Niektóre odpowiedzi ustąpiły miejsca dowolnej liczbie tablic.
Może to łatwo skurczyć się lub rozszerzyć do mniejszej lub większej liczby tablic.
Potrzebowałem kombinacji jednego zestawu z powtórzeniami, więc mogłem użyć:

f(a,a,a)

ale używany:

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

0

Zauważyłem, że nikt nie opublikował rozwiązania, które umożliwia przekazanie funkcji do przetwarzania każdej kombinacji, więc oto moje rozwiązanie:

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

Wynik:

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

0

Zwykłe podejście brutalnej siły w JS, które przyjmuje tablicę tablic jako dane wejściowe.

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>

Właśnie przekonwertowałem odpowiedź @ dummersl z CoffeScript na JavaScript. Po prostu działa.

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

Jeszcze inna realizacja. Nie najkrótszy ani wyszukany, ale szybki:

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

Żadne biblioteki nie są potrzebne! :)

Potrzebuje jednak funkcji strzałek i prawdopodobnie nie jest tak wydajna. : /

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

Tak dla porządku

Oto moja wersja. Zrobiłem to przy użyciu najprostszego iteratora javascript „for ()”, więc jest kompatybilny w każdym przypadku i ma najlepszą wydajność.

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

Z poważaniem.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.