JavaScript 中多个数组的笛卡儿积

如何在 JavaScript 中实现多个数组的笛卡儿积?

举个例子,

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

应该会回来

[
[1, 10, 100],
[1, 10, 200],
[1, 10, 300],
[2, 10, 100],
[2, 10, 200]
...
]
74801 次浏览

社区似乎认为这是微不足道的,并且/或者很容易找到一个参考实现。然而,经过简单的检查,我找不到一个,... 要么是这样,要么可能只是因为我喜欢重新发明轮子或解决类似课堂编程的问题。不管怎样,今天都是你的幸运日:

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

相对有效的完全参考实现..。

关于效率: 你可以通过把 if 从循环中去掉,并且有两个独立的循环来获得一些效率,因为它在技术上是常量,你可以帮助分支预测和所有那些混乱的东西,但是这一点在 JavaScript 中是没有实际意义的。

下面是 underscore.js提供的使用 reduceflatten的功能性解决方案(没有任何 变量!) :

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>

Remark: This solution was inspired by http://cwestblog.com/2011/05/02/cartesian-product-of-multiple-arrays/

咖啡手稿版本:

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

这里有一个简单的递归解决方案:

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

使用 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%; }

Below there is a similar version compatible with older browsers.

function cartesianProduct(arrays) {
var result = [],
current = new Array(arrays.length);
(function backtracking(index) {
if(index == arrays.length) return result.push(current.slice());
for(var i=0; i<arrays[index].length; ++i) {
current[index] = arrays[index][i];
backtracking(index+1);
}
})(0);
return result;
}
cartesianProduct([[1,2],[10,20],[100,200,300]]).forEach(function(item) {
console.log('[' + item.join(', ') + ']');
});
div.as-console-wrapper { max-height: 100%; }

一种非递归方法,它增加了在实际将产品添加到结果集之前对其进行过滤和修改的能力。

注意: 使用 .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;
}


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

下面是@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));

下面是一种使用 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]))));

当任何输入数组包含数组项时,本主题下的一些答案都会失败。你最好检查一下。

不管怎么说,不需要下划线,不管怎样都不要浪费时间。我相信这个应该用纯 JSES6来实现,尽可能的实用。

这段代码使用 reduce 和嵌套映射,只是为了得到两个数组的笛卡儿积,然而第二个数组来自对同一个函数的递归调用,因此。.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))); 

这是一个使用 箭头函数的纯 ES6解决方案

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

2020年更新: 一行(!)答案与香草 JS

2017年原始答案: 两行答案与香草 JS: (见下文更新)

这里所有的答案都是 太复杂了,大多数需要20行甚至更多的代码。

这个例子只使用 两行普通的 JavaScript 代码,没有 loash,下划线或其他库:

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;

更新:

这与上述方法相同,但改进后严格遵循 Airbnb JavaScript 样式指南验证,使用 埃斯林特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让我知道原始代码的行问题。

2020年更新:

由于我写了这个答案,我们得到了更好的内置,这最终可以让我们减少(没有双关意图)的代码只有1行!

const cartesian =
(...a) => a.reduce((a, b) => a.flatMap(d => b.map(e => [d, e].flat())));

特别感谢 墨水建议使用减少。

特别感谢 Bergi建议使用新增加的单位地图。

特别感谢 ECMAScript 2019 为语言添加了扁平化和扁平化映射!

例子

这正是你问题中的例子:

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

演示

请看演示:

语法

我在这里使用的语法并不新鲜。 我的示例使用了传播操作符和其他参数——在2015年6月发布的第6版 ECMA-262标准中定义的 JavaScript 特性,这些特性开发得更早,更广为人知的是 ES6或 ES2015。参见:

更新2020示例中的新方法是在 ES2019中添加的:

它使得这样的代码如此简单,以至于不使用它是一种罪过。对于那些原生不支持它的旧平台,你可以使用 Babel 或者其他工具将它翻译成旧语法——事实上,我的 Babel 翻译的例子仍然比这里的大多数例子更短更简单,但是这并不重要,因为翻译的输出并不是你需要理解或者维护的东西,它只是一个我觉得有趣的事实。

结论

没有必要编写数百行难以维护的代码,也没有必要为这样一个简单的事情使用整个库,而两行普通的 JavaScript 就可以轻松完成这项工作。正如你所看到的,使用这种语言的现代特性是值得的,在需要支持没有现代特性的原生支持的古老平台的情况下,你总是可以使用 巴别塔打字机或其他工具将新的语法转换成旧的语法。

别像1995年那样编程

JavaScript 的发展是有原因的。TC39在语言设计方面做得非常出色,增加了新的特性,而浏览器厂商在实现这些特性方面做得非常出色。

要查看浏览器中任何给定特性的本机支持的当前状态,请参见:

若要查看 Node 版本中的支持,请参见:

要在本地不支持的平台上使用现代语法,可以使用 Babel 或 TypeScript:

以下有效的 abc0返回所有给定的 abc1的笛卡儿积:

// 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-ary 笛卡儿积的规范,它产生

  • 如果一个或多个给定的迭代器为空,例如 []''
  • 如果给定一个包含单个值 a的单个迭代器,则为 [[a]]

所有其他情况都按照以下测试用例所示的预期进行处理:

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


// Test cases:
console.log([...cartesian([])]);              // []
console.log([...cartesian([1])]);             // [[1]]
console.log([...cartesian([1, 2])]);          // [[1], [2]]


console.log([...cartesian([1], [])]);         // []
console.log([...cartesian([1, 2], [])]);      // []


console.log([...cartesian([1], [2])]);        // [[1, 2]]
console.log([...cartesian([1], [2], [3])]);   // [[1, 2, 3]]
console.log([...cartesian([1, 2], [3, 4])]);  // [[1, 3], [2, 3], [1, 4], [2, 4]]


console.log([...cartesian('')]);              // []
console.log([...cartesian('ab', 'c')]);       // [['a','c'], ['b', 'c']]
console.log([...cartesian([1, 2], 'ab')]);    // [[1, 'a'], [2, 'a'], [1, 'b'], [2, 'b']]


console.log([...cartesian(new Set())]);       // []
console.log([...cartesian(new Set([1]))]);    // [[1]]
console.log([...cartesian(new Set([1, 1]))]); // [[1]]

我注意到,没有人提出一个解决方案,允许传递一个函数来处理每个组合,所以这里是我的解决方案:

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?

在我的特定环境中,“老式”方法似乎比基于更现代特性的方法更有效。下面是代码(包括与@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

仅供选择,一个真正简单的实现使用数组的 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))), []);

简单的 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]
]);

一个简单的“头脑和视觉友好”的解决方案。

enter image description here


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

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>

Just converted @dummersl's answer from CoffeScript to JavaScript. It just works.

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

单行方法,更好地阅读缩进。

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

又一个实现,不是最短或最花哨的,而是最快的:

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

@ 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 ] ];
*/

对于那些需要 TypeScript 的人(重新实现@Danny 的回答)

/**
* 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[][])
}

不需要库! :)

但是需要箭头函数,而且效率可能不高。 :/

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

郑重声明

这是我的版本。我使用了最简单的 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;
}

最好的问候。

函数式程序设计

这个问题被标记为 函数式编程,所以让我们来看看 列表单子:

这个一进制列表的一个应用程序是代表不确定性计算。

听起来像是适合 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]))

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

更多的递归

其他递归实现包括-

const cartesian = (a, ...more) =>
a == null
? [[]]
: cartesian(...more).flatMap(c => a.map(v => [v,...c]))


for (const p of cartesian([1,2], [10,20], [100,200,300]))
console.log(JSON.stringify(p))
.as-console-wrapper { min-height: 100%; top: 0; }

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

注意以上不同的顺序。你可以通过反转两个循环得到 字典编纂顺序字典编纂顺序。小心不要通过在循环内调用 cartesian(如 尼克的回答 -)来避免重复工作

const bind = (x, f) => f(x)


const cartesian = (a, ...more) =>
a == null
? [[]]
: bind(cartesian(...more), r => a.flatMap(v => r.map(c => [v,...c])))


for (const p of cartesian([1,2], [10,20], [100,200,300]))
console.log(JSON.stringify(p))
.as-console-wrapper { min-height: 100%; top: 0; }

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

发电机

另一种选择是使用生成器。生成器非常适合组合数学,因为解空间可以变得非常大。生成器提供延迟计算,因此它们可以在任何时候暂停/恢复/取消-

function* cartesian(a, ...more) {
if (a == null) return yield []
for (const v of a)
for (const c of cartesian(...more)) // ⚠️
yield [v, ...c]
}
  

for (const p of cartesian([1,2], [10,20], [100,200,300]))
console.log(JSON.stringify(p))
.as-console-wrapper { min-height: 100%; top: 0; }

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

也许你看到了我们在发电机里循环调用 cartesian。如果你怀疑可以优化,它可以!这里我们使用一个通用的 tee函数,它分叉任何迭代器 n次-

function* cartesian(a, ...more) {
if (a == null) return yield []
for (const t of tee(cartesian(...more), a.length)) // ✅
for (const v of a)
for (const c of t) // ✅
yield [v, ...c]
}

在哪里实施 tee-

function tee(g, n = 2) {
const memo = []
function* iter(i) {
while (true) {
if (i >= memo.length) {
const w = g.next()
if (w.done) return
memo.push(w.value)
}
else yield memo[i++]
}
}
return Array.from(Array(n), _ => iter(0))
}

即使在小型测试中,使用 tee实现的 cartesian发生器也能以两倍的速度执行。

现代的 JavaScript 只需要几行代码,没有像 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')
);

你可以 reduce二维数组。在累加器阵列上使用 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))

(这是基于 Nina Scholz 的回答)

更具可读性的实现

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

下面是一个使用本地 ES2019flatMap的一行程序。不需要库,只需要一个现代的浏览器(或传输器) :

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

这本质上是一个现代版的 Viebel 的答案,没有冗长。

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

对于那些满意兰达解决方案的人来说:

import { xprod, flatten } from 'ramda';


const cartessian = (...xs) => xs.reduce(xprod).map(flatten)


或者同样没有依赖性和两个乐高积木块免费(xprodflatten) :

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


const xprod = (xs, ys) => xs.flatMap(x => ys.map(y => [x, y]));


const cartessian = (...xs) => xs.reduce(xprod).map(flatten);


2021年版的邓永锵大作 回答
也受到了 Neil Mountford 的回答的启发

  • 快(比接受答案快95%)
  • 使用任何数据类型

const getAllCombinations = (arraysToCombine) => {
const divisors = [];
let combinationsCount = 1;
for (let i = arraysToCombine.length - 1; i >= 0; i--) {
divisors[i] = divisors[i + 1] ? divisors[i + 1] * arraysToCombine[i + 1].length : 1;
combinationsCount *= (arraysToCombine[i].length || 1);
}


const getCombination = (n, arrays, divisors) => arrays.reduce((acc, arr, i) => {
acc.push(arr[Math.floor(n / divisors[i]) % arr.length]);
return acc;
}, []);


const combinations = [];
for (let i = 0; i < combinationsCount; i++) {
combinations.push(getCombination(i, arraysToCombine, divisors));
}
return combinations;
};


console.log(getAllCombinations([['a', 'b'], ['c', 'z'], ['d', 'e', 'f']]));

基准: https://jsbench.me/gdkmxhm36d/1

另一个更加简化的2021年风格的答案,只使用 reduce、 map 和 concat 方法:

const cartesian = (...arr) => arr.reduce((a,c) => a.map(e => c.map(f => e.concat([f]))).reduce((a,c) => a.concat(c), []), [[]]);


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

下面是一个只使用 flatMapmap的递归一行程序:

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


const cartesian = (first, ...rest) =>
rest.length ? first.flatMap(v => cartesian(...rest).map(c => [v].concat(c)))
: first;


console.log(cartesian(...inp));