JavaScript - 从 n 个数组中生成包含 m 个元素的组合

84

我在JavaScript中遇到了一个问题,无法编写代码从n个包含m个元素的数组中生成组合。我看到过类似于其他语言的这种问题,但答案涉及我不知道如何翻译的语法或库魔法。

考虑以下数据:

[[0,1], [0,1,2,3], [0,1,2]]

有三个数组,它们中的元素数量不同。我的目标是通过组合每个数组中的一个元素来获取所有组合。

例如:

0,0,0 // item 0 from array 0, item 0 from array 1, item 0 from array 2
0,0,1
0,0,2
0,1,0
0,1,1
0,1,2
0,2,0
0,2,1
0,2,2

等等等等。

如果数组的数量是固定的,那么编写硬编码实现就很容易。但是数组的数量可能会变化:

[[0,1], [0,1]]
[[0,1,3,4], [0,1], [0], [0,1]]

任何帮助将不胜感激。


找到组合的最简单方法 https://dev59.com/k2855IYBdhLWcg3wc0Dy#52098701 - kathir
似乎将JavaScript中多个数组的笛卡尔积链接为重复目标是合理的。唯一的区别似乎在于cartesian([ [ 1, 2 ], [ "a", "b" ] ])cartesian([ 1, 2 ], [ "a", "b" ]),但函数签名显然需要分别为cartesian(arrays)cartesian(...arrays) - Sebastian Simon
10个回答

169

以下是一个非常简单和短小的示例,使用递归辅助函数:

function cartesian(...args) {
    var r = [], max = args.length-1;
    function helper(arr, i) {
        for (var j=0, l=args[i].length; j<l; j++) {
            var a = arr.slice(0); // clone arr
            a.push(args[i][j]);
            if (i==max)
                r.push(a);
            else
                helper(a, i+1);
        }
    }
    helper([], 0);
    return r;
}

使用方法:

cartesian([0,1], [0,1,2,3], [0,1,2]);

如果想让该函数接受一个数组的数组作为参数,只需将函数签名更改为function cartesian(args),而不是使用剩余参数语法。


1
太棒了,谢谢。基准测试在这里:http://jsfiddle.net/9uvfP/。您的解决方案运行100,000次只需要0.14秒,是目前提交的最快实现。 :) - quano
啊,我注意到基准测试中有一个错误。更新在这里:http://jsfiddle.net/2xt5F/。它需要大约0.6秒。 - quano
这与我最初采取的方法类似,但我无法做到...由于有了新生儿而有点睡眠不足,但很高兴有人做到了,这样我就可以看到了! - Tom Pietrosanti
看起来,我将成为你的粉丝。你太聪明了。 - BlitZ
尽管小提琴基准测试@Neob91的答案对我来说是最快的,但这个jsperf似乎表明这个答案是最快的:http://jsperf.com/array-combos - maxedison
显示剩余2条评论

20
我建议使用一个简单的递归生成器函数
// JS
function* cartesianIterator(head, ...tail) {
  const remainder = tail.length ? cartesianIterator(...tail) : [[]];
  for (let r of remainder) for (let h of head) yield [h, ...r];
}

// get values:
const cartesian = items => [...cartesianIterator(items)];
console.log(cartesian(input));

// TS
function* cartesianIterator<T>(items: T[][]): Generator<T[]> {
  const remainder = items.length > 1 ? cartesianIterator(items.slice(1)) : [[]];
  for (let r of remainder) for (let h of items.at(0)!) yield [h, ...r];
}

// get values:
const cartesian = <T>(items: T[][]) => [...cartesianIterator(items)];
console.log(cartesian(input));


这在处理大量组合时非常有用,无需一次性将它们全部实现。 - bmacnaughton

19

你可以采用迭代的方式构建子数组。

var parts = [[0, 1], [0, 1, 2, 3], [0, 1, 2]],
    result = parts.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; }


2
那么当部分是 [[0, 1], [0, 1, 2, 3], [[0], [1], [2]]] 时呢? - Redu
尽管有人在竞相将多行代码压缩成一行(缩小),但这段代码无疑是非常优雅的。 - DarkNeuron

7

const charSet = [["A", "B"],["C", "D", "E"],["F", "G", "H", "I"]];
console.log(charSet.reduce((a,b)=>a.flatMap(x=>b.map(y=>x+y)),['']))


6

经过一些研究,我发现了一个相关的先前问题:找到JavaScript数组值的所有组合

我改编了那里的一些代码,使其返回包含所有排列的数组的数组:

function(arraysToCombine) {
    var divisors = [];
    for (var i = arraysToCombine.length - 1; i >= 0; i--) {
       divisors[i] = divisors[i + 1] ? divisors[i + 1] * arraysToCombine[i + 1].length : 1;
    }

    function getPermutation(n, arraysToCombine) {
       var result = [], 
           curArray;    
       for (var i = 0; i < arraysToCombine.length; i++) {
          curArray = arraysToCombine[i];
          result.push(curArray[Math.floor(n / divisors[i]) % curArray.length]);
       }    
       return result;
    }

    var numPerms = arraysToCombine[0].length;
    for(var i = 1; i < arraysToCombine.length; i++) {
        numPerms *= arraysToCombine[i].length;
    }

    var combinations = [];
    for(var i = 0; i < numPerms; i++) {
        combinations.push(getPermutation(i, arraysToCombine));
    }
    return combinations;
}

我已经在http://jsfiddle.net/7EakX/上放置了一个可工作的副本,它接受你之前提供的数组([[0,1], [0,1,2,3], [0,1,2]]),并将结果输出到浏览器控制台。


非常好用。我做了一个基准测试:http://jsfiddle.net/kLfq9/。在我的电脑上,您的解决方案在Chrome浏览器中运行100,000次大约需要0.5秒。 - quano

3

仅供娱乐,这是我第一个答案中解决方案的更多功能变体:

function cartesian() {
    var r = [], args = Array.from(arguments);
    args.reduceRight(function(cont, factor, i) {
        return function(arr) {
            for (var j=0, l=factor.length; j<l; j++) {
                var a = arr.slice(); // clone arr
                a[i] = factor[j];
                cont(a);
            }
        };
    }, Array.prototype.push.bind(r))(new Array(args.length));
    return r;
}

为了达到最佳速度,我们可以动态编��自己的循环:

function cartesian() {
    return (cartesian.cache[arguments.length] || cartesian.compile(arguments.length)).apply(null, arguments);
}
cartesian.cache = [];
cartesian.compile = function compile(n) {
    var args = [],
        indent = "",
        up = "",
        down = "";
    for (var i=0; i<n; i++) {
        var arr = "$"+String.fromCharCode(97+i),
            ind = String.fromCharCode(105+i);
        args.push(arr);
        up += indent+"for (var "+ind+"=0, l"+arr+"="+arr+".length; "+ind+"<l"+arr+"; "+ind+"++) {\n";
        down = indent+"}\n"+down;
        indent += "  ";
        up += indent+"arr["+i+"] = "+arr+"["+ind+"];\n";
    }
    var body = "var res=[],\n    arr=[];\n"+up+indent+"res.push(arr.slice());\n"+down+"return res;";
    return cartesian.cache[n] = new Function(args, body);
}

1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - David Tew

2
您可以使用递归函数来获取所有的组合。

const charSet = [["A", "B"],["C", "D", "E"],["F", "G", "H", "I"]];

let loopOver = (arr, str = '', final = []) => {
  if (arr.length > 1) {
    arr[0].forEach(v => loopOver(arr.slice(1), str + v, final))
  } else {
    arr[0].forEach(v => final.push(str + v))
  }
  return final
}

console.log(loopOver(charSet))


这段代码仍然可以使用三元运算符缩短,但为了可读性,我更喜欢第一个版本。

const charSet = [["A", "B"],["C", "D", "E"],["F", "G", "H", "I"]];

let loopOver = (arr, str = '') => arr[0].map(v => arr.length > 1 ? loopOver(arr.slice(1), str + v) : str + v).flat()

console.log(loopOver(charSet))


2
var f = function(arr){
    if(typeof arr !== 'object'){
        return false;
    }

    arr = arr.filter(function(elem){ return (elem !== null); }); // remove empty elements - make sure length is correct
    var len = arr.length;

    var nextPerm = function(){ // increase the counter(s)
        var i = 0;

        while(i < len)
        {
            arr[i].counter++;

            if(arr[i].counter >= arr[i].length){
                arr[i].counter = 0;
                i++;
            }else{
                return false;
            }
        }

        return true;
    };

    var getPerm = function(){ // get the current permutation
        var perm_arr = [];

        for(var i = 0; i < len; i++)
        {
            perm_arr.push(arr[i][arr[i].counter]);
        }

        return perm_arr;
    };

    var new_arr = [];

    for(var i = 0; i < len; i++) // set up a counter property inside the arrays
    {
        arr[i].counter = 0;
    }

    while(true)
    {
        new_arr.push(getPerm()); // add current permutation to the new array

        if(nextPerm() === true){ // get next permutation, if returns true, we got them all
            break;
        }
    }

    return new_arr;
};

谢谢。Benchmark 可在此处获得:http://jsfiddle.net/6cxEH/ 。您的解决方案需要约 0.6 秒来运行 100,000 次。 - quano

2
这是另一种方法。我将所有数组的索引视为数字,其各个基数均不相同(如时间和日期),使用数组长度作为基数。
因此,使用您提供的第一组数据,第一个数字是2进制,第二个是4进制,第三个是3进制。计数器从000开始,然后依次变为001、002、010。这些数字对应于数组中的索引,并且由于顺序得到保留,所以没有问题。
我在这里有一个可工作的示例:http://jsfiddle.net/Rykus0/DS9Ea/1/ 以下是代码:
// Arbitrary base x number class 
var BaseX = function(initRadix){
    this.radix     = initRadix ? initRadix : 1;    
    this.value     = 0;
    this.increment = function(){
        return( (this.value = (this.value + 1) % this.radix) === 0);
    }
}

function combinations(input){
    var output    = [],    // Array containing the resulting combinations
        counters  = [],    // Array of counters corresponding to our input arrays
        remainder = false, // Did adding one cause the previous digit to rollover?
        temp;              // Holds one combination to be pushed into the output array

    // Initialize the counters
    for( var i = input.length-1; i >= 0; i-- ){
        counters.unshift(new BaseX(input[i].length));
    }

    // Get all possible combinations
    // Loop through until the first counter rolls over
    while( !remainder ){
        temp      = [];   // Reset the temporary value collection array
        remainder = true; // Always increment the last array counter

        // Process each of the arrays
        for( i = input.length-1; i >= 0; i-- ){
            temp.unshift(input[i][counters[i].value]); // Add this array's value to the result

            // If the counter to the right rolled over, increment this one.
            if( remainder ){
                remainder = counters[i].increment();
            }
        }
        output.push(temp); // Collect the results.
    }

    return output;
}

// Input is an array of arrays
console.log(combinations([[0,1], [0,1,2,3], [0,1,2]]));

1
感谢您提供的解决方案。基准测试可以在这里找到:http://jsfiddle.net/XgyPC/。它会运行您的函数100,000次。在我的电脑上,在Chrome浏览器中大约需要1秒钟的时间。 - quano
太好了!感谢您运行基准测试。我一直在想它的表现如何,对这个方面没有多少考虑。这是一个有趣的小问题需要解决,所以我可能会再试一次。 - Tom Pietrosanti

1
另一种使用ES6递归风格的实现。

Array.prototype.cartesian = function(a,...as){
  return a ? this.reduce((p,c) => (p.push(...a.cartesian(...as).map(e => as.length ? [c,...e] : [c,e])),p),[])
           : this;
};

console.log(JSON.stringify([0,1].cartesian([0,1,2,3], [[0],[1],[2]])));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接