查找 JavaScript 数组值的所有组合(笛卡尔积)

Posted

技术标签:

【中文标题】查找 JavaScript 数组值的所有组合(笛卡尔积)【英文标题】:Finding All Combinations (Cartesian product) of JavaScript array values 【发布时间】:2011-05-18 21:09:37 【问题描述】:

如何在 N 个可变长度的 javascript 数组中生成所有值的组合?

假设我有 N 个 JavaScript 数组,例如

var first = ['a', 'b', 'c', 'd'];
var second = ['e'];
var third =  ['f', 'g', 'h', 'i', 'j'];

(本例中是三个数组,但问题是它的 N 个数组。)

我想输出它们的值的所有组合,以产生

aef
aeg
aeh
aei
aej
bef
beg
....
dej

编辑:这是我使用 ffriend 接受的答案作为基础的版本。

var allArrays = [['a', 'b'], ['c', 'z'], ['d', 'e', 'f']];

 function allPossibleCases(arr) 
  if (arr.length === 0) 
    return [];
   
  else if (arr.length ===1)
    return arr[0];
  
  else 
    var result = [];
    var allCasesOfRest = allPossibleCases(arr.slice(1));  // recur with the rest of array
    for (var c in allCasesOfRest) 
      for (var i = 0; i < arr[0].length; i++) 
        result.push(arr[0][i] + allCasesOfRest[c]);
      
    
    return result;
  


var results = allPossibleCases(allArrays);
 //outputs ["acd", "bcd", "azd", "bzd", "ace", "bce", "aze", "bze", "acf", "bcf", "azf", "bzf"]

【问题讨论】:

【参考方案1】:

这不是排列,请参阅来自***的permutations definitions。

但是您可以通过 递归 实现这一点:

var allArrays = [
  ['a', 'b'],
  ['c'],
  ['d', 'e', 'f']
]

function allPossibleCases(arr) 
  if (arr.length == 1) 
    return arr[0];
   else 
    var result = [];
    var allCasesOfRest = allPossibleCases(arr.slice(1)); // recur with the rest of array
    for (var i = 0; i < allCasesOfRest.length; i++) 
      for (var j = 0; j < arr[0].length; j++) 
        result.push(arr[0][j] + allCasesOfRest[i]);
      
    
    return result;
  



console.log(allPossibleCases(allArrays))

您也可以使用循环来实现,但这会有点棘手,并且需要实现您自己的堆栈模拟。

【讨论】:

【参考方案2】:

我建议一个简单的递归generator function如下:

// Generate cartesian product of given iterables:
function* cartesian(head, ...tail) 
  let remainder = tail.length ? cartesian(...tail) : [[]];
  for (let r of remainder) for (let h of head) yield [h, ...r];


// Example:
const first  = ['a', 'b', 'c', 'd'];
const second = ['e'];
const third  = ['f', 'g', 'h', 'i', 'j'];

console.log(...cartesian(first, second, third));

【讨论】:

这确实很漂亮。对于那些想要使用未知数量的输入并将结果存储在数组中的人,您可以这样做: const product = [...cartesian.apply(this, [first, second, third, Fourth, etc]) ]; @Andrew 为什么.apply 而不是向笛卡尔表达论点? const product = [...cartesian(...unknownNumberOfInputs)]; 美丽。如何修改以满足this version需要单、双等字符串排列的地方【参考方案3】:

您不需要递归,也不需要大量嵌套循环,甚至不需要在内存中生成/存储整个排列数组。

由于排列的数量是每个数组长度的乘积(称为numPerms),您可以创建一个函数getPermutation(n),它通过以下方式返回索引0numPerms - 1 之间的唯一排列根据n计算它需要从中检索其字符的索引。

这是怎么做到的?如果您考虑在每个包含以下内容的数组上创建排列: [0, 1, 2, ... 9] 这很简单...第 245 个排列 (n=245) 是“245”,相当直观,或者:

arrayHundreds[Math.floor(n / 100) % 10]
+ arrayTens[Math.floor(n / 10) % 10]
+ arrayOnes[Math.floor(n / 1) % 10]

问题的复杂之处在于数组大小不同。我们可以通过将n/100n/10 等替换为其他除数来解决此问题。为此,我们可以轻松地预先计算一组除数。在上面的例子中,100 的除数等于arrayTens.length * arrayOnes.length。因此,我们可以将给定数组的除数计算为剩余数组长度的乘积。最后一个数组的除数总是 1。此外,我们不是按 10 修改,而是按当前数组的长度修改。

示例代码如下:

var allArrays = [first, second, third, ...];

// Pre-calculate divisors
var divisors = [];
for (var i = allArrays.length - 1; i >= 0; i--) 
   divisors[i] = divisors[i + 1] ? divisors[i + 1] * allArrays[i + 1].length : 1;


function getPermutation(n) 
   var result = "", curArray;

   for (var i = 0; i < allArrays.length; i++) 
      curArray = allArrays[i];
      result += curArray[Math.floor(n / divisors[i]) % curArray.length];
   

   return result;

【讨论】:

非常好。不过这里有一个错字,results 应该显示result——我注意到你向后循环计算除数,我认为除数在数组中的位置很重要? @Gary,感谢您接听。除数的顺序很重要,因为第一个取决于第二个,第二个取决于第三个,等等......所以通过向后循环,我可以更轻松地构建它。 @Box9:这个函数是否适用于 1 个数组?不是 (n*n) - (n-1) 吗? @epitaph,它应该仍然适用于 1 个数组。 divisors 将只有一个元素:[1],因此它总是除以 1,然后除以数组长度 - 实际上,什么都不做。 如果它适用于 1 个数组并且结果 (n*n)-(n-1) 我可以用它来制作成本矩阵吗?例如对于旅行推销员问题?【参考方案4】:

提供的答案对我来说太难了。所以我的解决方案是:

var allArrays = new Array(['a', 'b'], ['c', 'z'], ['d', 'e', 'f']);

function getPermutation(array, prefix) 
  prefix = prefix || '';
  if (!array.length) 
    return prefix;
  

  var result = array[0].reduce(function(result, value) 
    return result.concat(getPermutation(array.slice(1), prefix + value));
  , []);
  return result;


console.log(getPermutation(allArrays));

【讨论】:

嗨。如何修改它以返回数组数组而不是字符串数组?所以不是 ["acd","ace","acf" ...] 而是返回 [["a","c",d"], ["a","c","e"] .. ..]【参考方案5】:

您可以通过生成笛卡尔积来采用单行方法。

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

var items = [['a', 'b', 'c', 'd'], ['e'], ['f', 'g', 'h', 'i', 'j']],
    result = items.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; 

【讨论】:

怎么用回答this【参考方案6】:

复制le_m的答案直接取Array of Arrays:

function *combinations(arrOfArr) 
  let [head, ...tail] = arrOfArr
  let remainder = tail.length ? combinations(tail) : [[]];
  for (let r of remainder) for (let h of head) yield [h, ...r];

希望它能节省别人的时间。

【讨论】:

你可以这样使用 le_m:cartesian(...arrOfArr)【参考方案7】:

您可以使用典型的回溯:

function cartesianProductConcatenate(arr) 
  var data = new Array(arr.length);
  return (function* recursive(pos) 
    if(pos === arr.length) yield data.join('');
    else for(var i=0; i<arr[pos].length; ++i) 
      data[pos] = arr[pos][i];
      yield* recursive(pos+1);
    
  )(0);

我使用生成器函数来避免同时分配所有结果,但如果你愿意,你可以

[...cartesianProductConcatenate([['a', 'b'], ['c', 'z'], ['d', 'e', 'f']])];
// ["acd","ace","acf","azd","aze","azf","bcd","bce","bcf","bzd","bze","bzf"]

【讨论】:

【参考方案8】:

找到组合的最简单方法

const arr1= [ 'a', 'b', 'c', 'd' ];
const arr2= [ '1', '2', '3' ];
const arr3= [ 'x', 'y', ];

const all = [arr1, arr2, arr3];

const output = all.reduce((acc, cu) =>  
    let ret = [];
      acc.map(obj => 
        cu.map(obj_1 => 
          ret.push(obj + '-' + obj_1) 
        );
      );
      return ret;
   )

console.log(output);

【讨论】:

这在空数组上失败。【参考方案9】:

如果您正在寻找可以处理具有任何项目类型的二维数组的流兼容函数,您可以使用下面的函数。

const getUniqueCombinations = <T>(items : Array<Array<T>>, prepend : Array<T> = []) : Array<Array<T>> => 
    if(!items || items.length === 0) return [prepend];

    let out = [];

    for(let i = 0; i < items[0].length; i++)
        out = [...out, ...getUniqueCombinations(items.slice(1), [...prepend, items[0][i]])];
    

    return out;

操作的可视化:

在:

[
    [Obj1, Obj2, Obj3],
    [Obj4, Obj5],
    [Obj6, Obj7]
]

出局:

[
    [Obj1, Obj4, Obj6 ],
    [Obj1, Obj4, Obj7 ],
    [Obj1, Obj5, Obj6 ],
    [Obj1, Obj5, Obj7 ],
    [Obj2, Obj4, Obj6 ],
    [Obj2, Obj4, Obj7 ],
    [Obj2, Obj5, Obj6 ],
    [Obj2, Obj5, Obj7 ],
    [Obj3, Obj4, Obj6 ],
    [Obj3, Obj4, Obj7 ],
    [Obj3, Obj5, Obj6 ],
    [Obj3, Obj5, Obj7 ]
]

【讨论】:

【参考方案10】:

您可以创建一个二维数组并reduce 它。然后使用flatMap 在累加器数组和当前正在迭代的数组中创建字符串组合并将它们连接起来。

const data = [ ['a', 'b', 'c', 'd'], ['e'], ['f', 'g', 'h', 'i', 'j'] ]

const output = data.reduce((acc, cur) => acc.flatMap(c => cur.map(n => c + n)) )

console.log(output)

【讨论】:

如何获取 ARRAY 格式的列表而不是字符串?所以不是 "abc" 让它作为数组返回 ["a","b","c"] @BlasterGod 这是笛卡尔积。我在这里用类似的方法回答了这个问题:***.com/a/57597533/3082296【参考方案11】:

2021版大卫唐的伟大answer 还受到 Neil Mountford 的 answer

的启发

const getAllCombinations = (arraysToCombine) => 
  const divisors = [];
  let permsCount = 1;
  for (let i = arraysToCombine.length - 1; i >= 0; i--) 
      divisors[i] = divisors[i + 1] ? divisors[i + 1] * arraysToCombine[i + 1].length : 1;
      permsCount *= (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 < permsCount; i++) 
      combinations.push(getCombination(i, arraysToCombine, divisors));
  
  return combinations;
;

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

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

【讨论】:

【参考方案12】:

这是改编自上述几个答案的版本,它按照 OP 中指定的顺序生成结果,并返回字符串而不是数组:

function *cartesianProduct(...arrays) 
  if (!arrays.length) yield [];
  else 
    const [tail, ...head] = arrays.reverse();
    const beginning = cartesianProduct(...head.reverse());
    for (let b of beginning) for (let t of tail) yield b + t;
  


const first = ['a', 'b', 'c', 'd'];
const second = ['e'];
const third =  ['f', 'g', 'h', 'i', 'j'];
console.log([...cartesianProduct(first, second, third)])

【讨论】:

【参考方案13】:

你也可以使用这个函数:

const result = (arrayOfArrays) => arrayOfArrays.reduce((t, i) =>  let ac = []; for (const ti of t)  for (const ii of i)  ac.push(ti + '/' + ii)   return ac )

result([['a', 'b', 'c', 'd'], ['e'], ['f', 'g', 'h', 'i', 'j']])
// which will output [ 'a/e/f', 'a/e/g', 'a/e/h','a/e/i','a/e/j','b/e/f','b/e/g','b/e/h','b/e/i','b/e/j','c/e/f','c/e/g','c/e/h','c/e/i','c/e/j','d/e/f','d/e/g','d/e/h','d/e/i','d/e/j']

当然,您可以删除ac.push(ti + '/' + ii) 中的+ '/' 以消除最终结果中的斜线。你可以用 forEach 函数替换那些for (... of ...)(加上return ac之前的相应分号),无论你更喜欢什么。

【讨论】:

您也可以缩小其他答案之一:) 目标不是将其放在一行上。可读的代码本身就是一个目标。【参考方案14】:

没有递归的数组方法:

const combinations = [['1', '2', '3'], ['4', '5', '6'], ['7', '8']];
let outputCombinations = combinations[0]

combinations.slice(1).forEach(row => 
  outputCombinations = outputCombinations.reduce((acc, existing) =>
    acc.concat(row.map(item => existing + item))
  , []);
);

console.log(outputCombinations);

【讨论】:

【参考方案15】:
let arr1 = [`a`, `b`, `c`];
let arr2 = [`p`, `q`, `r`];
let arr3 = [`x`, `y`, `z`];
let result = [];

arr1.forEach(e1 => 
    arr2.forEach(e2 => 
        arr3.forEach(e3 => 
            result[result.length] = e1 + e2 + e3;
        );
    );
);


console.log(result);
/*
output:
[
  'apx', 'apy', 'apz', 'aqx',
  'aqy', 'aqz', 'arx', 'ary',
  'arz', 'bpx', 'bpy', 'bpz',
  'bqx', 'bqy', 'bqz', 'brx',
  'bry', 'brz', 'cpx', 'cpy',
  'cpz', 'cqx', 'cqy', 'cqz',
  'crx', 'cry', 'crz'
]
*/

【讨论】:

感谢您提供此代码 sn-p,它可能会提供一些有限的即时帮助。 proper explanation 将通过展示为什么这是解决问题的好方法,并使其对有其他类似问题的未来读者更有用,从而大大提高其长期价值。请edit您的回答添加一些解释,包括您所做的假设。【参考方案16】:

一种没有递归的解决方案,它还包括一个通过 id 检索单个组合的函数:

function getCombination(data, i) 
    return data.map(group => 
        let choice = group[i % group.length]
        i = (i / group.length) | 0;
        return choice;
    );


function* combinations(data) 
    let count = data.reduce((sum, length) => sum * length, 1);
    for (let i = 0; i < count; i++) 
        yield getCombination(data, i);
    


let data = [['a', 'b', 'c', 'd'], ['e'], ['f', 'g', 'h', 'i', 'j']];

for (let combination of combinations(data)) 
    console.log(...combination);

【讨论】:

以上是关于查找 JavaScript 数组值的所有组合(笛卡尔积)的主要内容,如果未能解决你的问题,请参考以下文章

如何使用Javascript从特定的单个数组中查找所有不重复的组合

如何在javascript中找到数组的所有组合

使用 Javascript 的嵌套数组循环在所有象限中生成和排序 (0,0) 附近的笛卡尔坐标

JQ:查找具有特定属性值的对象的数组索引

在javascript中的数组中查找多个最小值的索引

关系数据库