JavaScript 中多个数组的笛卡尔积

IT技术 javascript arrays algorithm cartesian-product
2021-01-26 23:36:14

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

举个例子,

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

应该回来

[
  [1, 10, 100],
  [1, 10, 200],
  [1, 10, 300],
  [2, 10, 100],
  [2, 10, 200]
  ...
]
6个回答

2020 年更新:使用 vanilla JS 的 1 行 (!) 答案

原始 2017 答案:带有 vanilla JS 的 2 行答案:( 请参阅下面的更新)

这里所有的答案都过于复杂,大部分都需要20行代码甚至更多。

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

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 风格指南- 使用ESLinteslint-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让我知道原始代码的linter问题。

2020 年更新:

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

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 的特性在 2015 年 6 月发布的 ECMA-262 标准第 6 版中定义并且开发得更早,更广为人知的是 ES6 或 ES2015。看:

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

它使这样的代码变得如此简单,以至于不使用它是一种罪过。对于原生不支持它的旧平台,您始终可以使用 Babel 或其他工具将其转译为旧语法——事实上,我的由 Babel 转译的示例仍然比这里的大多数示例更短更简单,但它没有真的很重要,因为转译的输出不是您需要理解或维护的东西,这只是我发现有趣的一个事实。

结论

没有必要编写数百行难以维护的代码,也没有必要为这么简单的事情使用整个库,两行普通的 JavaScript 就可以轻松完成工作。如您所见,使用该语言的现代功能确实值得,如果您需要支持没有现代功能本机支持的古老平台,您可以随时使用BabelTypeScript或其他工具将新语法转换为旧的。

不要像 1995 年那样编码

JavaScript 的发展是有原因的。TC39 通过添加新功能在语言设计方面做得非常出色,而浏览器供应商在实现这些功能方面做得非常出色。

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

要查看 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]].
2021-03-14 23:36:14
a, b, d, e, 把这些名字留给你最喜欢的 JS mangler,有意义的名字可以帮助理解这里的逻辑 :) 另外,哪里c去了?不错,但令人印象深刻的解决方案!
2021-03-15 23:36:14
“不要像 1995 年那样编写代码”——不必感到不快,并不是每个人都已经赶上了。
2021-03-20 23:36:14
不要像 2017 年那样编码。使用.flatMap而不是concat+ map:-)
2021-04-05 23:36:14
我注意到您的最新版本(...a) => a.reduce((a, b) => a.flatMap(d => b.map(e => [d, e].flat())));在一个参数的退化情况下不起作用 - 而不是返回列表列表,它只返回原始输入列表。
2021-04-07 23:36:14

这是使用and提供的问题的功能解决方案(没有任何可变变量!)reduceflattenunderscore.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>

备注:此解决方案的灵感来自http://cwestblog.com/2011/05/02/cartesian-product-of-multiple-arrays/

抱歉,这是 lodash / underscore 不兼容,他们交换了标志。
2021-03-11 23:36:14
这个答案有一个错字,不应该有“,真的”(也许 lodash 自从你发表这篇文章后已经改变了?)
2021-03-14 23:36:14
所以压扁时,使用true下划线和使用falselodash确保浅变平。
2021-03-26 23:36:14
如何修改此函数以使其接受数组数组?
2021-03-30 23:36:14
@ChrisJefferson 第二个参数flatten是使扁平化变浅。这里是必须的!
2021-04-03 23:36:14

这是@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 ])
2021-03-13 23:36:14
cartesianProduct([[[1],[2],[3]], ['a', 'b'], [['gamma'], [['alpha']]], ['zii', 'faa']]) 因为它将 ['gamma'] 展平为 'gamma' 并将 [['alpha']] 展平为 ['alpha']
2021-03-24 23:36:14
@Thankyou 你可以直接编辑答案而不是评论,只是这样做所以现在不需要:P
2021-03-27 23:36:14

以下有效的生成器函数返回所有给定迭代的笛卡尔积

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

所有其他案例都按预期处理,如以下测试案例所示:

勘误:在我上面的评论中,“尾递归”应该是“递归”(在这种情况下不是尾调用)。
2021-03-13 23:36:14
感谢您教我们使用生成器函数 + 尾递归 + 双层循环的绝妙示例!但是需要改变代码中第一个for循环的位置,使输出子数组的顺序正确。固定代码:function* cartesian(head, ...tail) { for (let h of head) { const remainder = tail.length > 0 ? cartesian(...tail) : [[]]; for (let r of remainder) yield [h, ...r] } }
2021-03-17 23:36:14
我在 Map 中得到了不正确的结果,除非我事先用Array.from克隆了可迭代对象[...arg]也许问题出在我身上。
2021-03-18 23:36:14
你介意解释一下发生了什么吗?非常感谢!
2021-03-28 23:36:14
@ooo 如果您想重现 OP 评论给出的笛卡尔积元组的顺序,那么您的修改是正确的。然而,乘积中元组的顺序通常是不相关的,例如,在数学上,结果是一个无序集合。我选择这个顺序是因为它需要的递归调用要少得多,因此性能更高——不过我没有运行基准测试。
2021-04-07 23:36:14

社区似乎认为这是微不足道的和/或很容易找到参考实现。然而,经过简短的检查,我找不到一个,......要么是这样,要么只是我喜欢重新发明轮子或解决课堂式的编程问题。无论哪种方式,这是您的幸运日:

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 移出循环并使用 2 个单独的循环来获得一些收益,因为它在技术上是恒定的,并且您将帮助进行分支预测和所有这些混乱,但这一点在 JavaScript 中没有实际意义。

我创建了一个快 2 倍以上且 (imo) 更干净的版本:pastebin.com/YbhqZuf7它通过不使用result = result.concat(...)和不使用args.slice(1). 不幸的是,我无法找到摆脱curr.slice()和递归的方法。
2021-03-13 23:36:14
感谢@ckoz 的详细回答。为什么不使用reduce数组功能呢?
2021-03-19 23:36:14
@Pauan 干得好,根据我所看到的,在联盟中整体上减少了 10%-50% 的性能提升。不过,我无法谈论“清洁度”,我觉得由于使用了闭包作用域变量,您的版本实际上更难以遵循。但一般来说,更高性能的代码更难遵循。我写了原始版本的可读性,我希望我有更多的时间,这样我就可以让你参与一场表演;)也许以后……
2021-03-23 23:36:14
这确实是这些问题之一
2021-03-24 23:36:14
@viebel 为什么要使用reduce?一方面,reduce 对旧浏览器的支持非常差(请参阅:developer.mozilla.org/en-US/docs/JavaScript/Reference/...),并且无论如何,来自其他答案的疯狂代码实际上看起来对您来说是可读的? 对我来说不是。确定它更短,但是一旦缩小此代码的长度将大致相同,更容易调试/优化,其次所有这些“减少”解决方案分解为相同的东西,除了它们具有闭包查找(理论上较慢),这也更难设计以便处理无限集......
2021-04-08 23:36:14