你如何在JavaScript中实现多个数组的笛卡尔积?
以一个例子为例,
cartesian([1, 2], [10, 20], [100, 200, 300])
应该返回
[
[1, 10, 100],
[1, 10, 200],
[1, 10, 300],
[2, 10, 100],
[2, 10, 200]
...
]
你如何在JavaScript中实现多个数组的笛卡尔积?
以一个例子为例,
cartesian([1, 2], [10, 20], [100, 200, 300])
应该返回
[
[1, 10, 100],
[1, 10, 200],
[1, 10, 300],
[2, 10, 100],
[2, 10, 200]
...
]
原始2017年回答:使用纯JS的2行代码:(请参见下面的更新)
这里的所有答案都过于复杂,大多数需要20行甚至更多的代码。
此示例仅使用两行纯JavaScript代码,没有lodash、underscore或其他库:
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 Style Guide - 使用ESLint并使用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);
const cartesian =
(...a) => a.reduce((a, b) => a.flatMap(d => b.map(e => [d, e].flat())));
特别感谢 inker 建议使用 reduce。
特别感谢 Bergi 建议使用新增的 flatMap。
特别感谢 ECMAScript 2019 添加 flat 和 flatMap 到语言中!
这是您问题中的确切示例:
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 ] ]
查看以下演示:
我在这里使用的语法并不新颖。 我的例子使用了展开运算符和剩余参数 - 这些是JavaScript中的特性,定义在ECMA-262标准的第6版中,该标准于2015年6月发布并在更早时期开发,被称为ES6或ES2015。请参见:
要查看浏览器对任何特定功能的本地支持状态,请参见:
要查看Node版本的支持情况,请参见:
如果要在不支持现代语法的平台上使用它,请使用Babel或TypeScript:
['a', 'b'], [1,2], [[9], [10]]
时,这段代码的效果不佳,它会产生以下结果:[ [ 'a', 1, 9 ], [ 'a', 1, 10 ], [ 'a', 2, 9 ], [ 'a', 2, 10 ], [ 'b', 1, 9 ], [ 'b', 1, 10 ], [ 'b', 2, 9 ], [ 'b', 2, 10 ] ]
。也就是说,它不能保持 [[9], [10]]
中每个元素的类型。 - Redu.flatMap
代替concat
+map
:-) - Bergia
、b
、d
、e
这些名称留给你最喜欢的 JS 混淆器,有意义的名称可以帮助理解逻辑 :) 另外,c
去哪里了呢?不过这是一个很棒的解决方案,令人印象深刻! - sp00m(...a) => a.reduce((a, b) => a.flatMap(d => b.map(e => [d, e].flat())));
在只有一个参数的情况下无法正常工作 -- 它不会返回一个嵌套数组,而是只返回原始输入的列表。 - Chris Smowtonfunction 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>
备注:此解决方案受http://cwestblog.com/2011/05/02/cartesian-product-of-multiple-arrays/启发。
flatten
函数的第二个参数是为了使扁平化变得浅层。在这里是必须的! - viebel这是@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));
.concat(y)
而不是.concat([ y ])
。 - Mulan// 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元笛卡尔积的规范,如果一个或多个给定的可迭代对象为空,例如[]
或''
,则会产生以下结果:
[]
[]
或''
[[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]]
function* cartesian(head, ...tail) { for (let h of head) { const remainder = tail.length > 0 ? cartesian(...tail) : [[]]; for (let r of remainder) yield [h, ...r] } }
- oooArray.from
或[...arg]
克隆可迭代对象。也许问题出在我身上。 - ninjagecko看起来社区认为这是微不足道的和/或很容易找到一个参考实现。然而,经过简短的检查,我找不到一个,... 要么是因为我喜欢重复发明轮子或解决课堂式的编程问题。无论哪种方式都是你的幸运日:
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中这个点有点无关紧要。
reduce
函数呢? - viebel和
args.slice(1)来实现速度提升。不幸的是,我无法找到一种方法来摆脱
curr.slice()`和递归。 - Pauanfunction 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]
// ]
使用ES2019原生的flatMap
函数可以轻松实现一行代码。无需任何库,只需使用现代浏览器(或转译器)即可:
data.reduce((a, b) => a.flatMap(x => b.map(y => [...x, y])), [[]]);
它本质上是 viebel 的答案的现代版本,没有使用 lodash。
data = [["hello"]]
,它也可以正确地工作。 - trincota
、b
、x
和y
都是通用的非描述性名称。如果使用实际描述性名称,可读性将会提高。arrays.reduce((product, array) => product.flatMap(combination => array.map(item => [...combination, item])), [[]])
尽管如此,你可以看到它变得有点长了。 - 3limin4t0r函数式编程
这个问题被标记为函数式编程,所以让我们来看一下列表单子:
这个单子列表的一个应用是表示非确定性计算。在算法中,
列表
可以保存所有执行路径的结果...
嗯,听起来很完美适合使用笛卡尔积
。JavaScript给了我们数组
,而单子绑定函数是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
,就像Nick's answer所示-
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
生成器的性能也是原来的两倍。下面是一种递归方法,使用了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]))));
cartesian([[1],[2]],[10,20],[100,200,300])
的数组项时,这将无法工作。 - Redu.concat()
内置的展开运算符有时可能会变得棘手。 - Redu这是一个纯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)));
d3.cross(a, b[, reducer])
。https://github.com/d3/d3-array#cross - Toph