【问题标题】:JavaScript - Generating combinations from n arrays with m elements [duplicate]JavaScript - 从具有 m 个元素的 n 个数组生成组合
【发布时间】:2013-03-08 16:37:29
【问题描述】:

我在编写代码以在 JavaScript 中从包含 m 个元素的 n 个数组中生成组合时遇到了麻烦。我在其他语言中看到过类似的问题,但答案包含我不确定如何翻译的句法或库魔法。

考虑这些数据:

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

3 个数组,其中包含不同数量的元素。我想要做的是通过组合每个数组中的一个项目来获得所有组合。

例如:

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

任何帮助将不胜感激。

【问题讨论】:

标签: javascript permutation combinations


【解决方案1】:

这是一个使用递归辅助函数的非常简单简短的例子:

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),而不是使用rest参数语法。

【讨论】:

  • 太棒了,谢谢。基准可在此处获得:jsfiddle.net/9uvfP。您的解决方案需要 0.14 秒才能运行 100,000 次,使其成为迄今为止提交的最快实现。 :)
  • 啊,我注意到基准测试中有错误。在这里更新:jsfiddle.net/2xt5F。大约需要 0.6 秒。
  • 这与我最初采用的方法相似,但无法到达那里......刚出生的婴儿有点睡眠不足,但很高兴有人这样做让我能看到!
  • 看来,我要成为你的粉丝了。你真是个天才。
  • 虽然 fiddles benchmark @Neob91 的答案对我来说是最快的,但这个 jsperf 似乎暗示这个答案是最快的:jsperf.com/array-combos
【解决方案2】:

您可以通过构建子数组来采用迭代方法。

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

【讨论】:

  • 零件是[[0, 1], [0, 1, 2, 3], [[0], [1], [2]]]..时怎么样?
  • 虽然似乎有一场将多行塞进一行(缩小)的竞争,但这段代码无疑是相当优雅的。
【解决方案3】:

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

// Generate all combinations of array elements:
function* cartesian(head, ...tail) {
  let remainder = tail.length ? cartesian(...tail) : [[]];
  for (let r of remainder) for (let h of head) yield [h, ...r];
}


// Example:
for (let c of cartesian([0,1], [0,1,2,3], [0,1,2])) {
  console.log(...c);
}

【讨论】:

  • 这在有大量组合时特别有用。无需一次性全部实现。
【解决方案4】:

在做了一些研究后,我发现了一个之前的相关问题: Finding All Combinations of JavaScript array values

我已经修改了那里的一些代码,以便它返回一个包含所有排列的数组数组:

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]]) 和输出结果到浏览器控制台。

【讨论】:

  • 效果很好。我做了一个基准测试:jsfiddle.net/kLfq9。您的解决方案大约需要 0.5 秒才能在我的计算机上的 Chrome 中运行 100,000 次。
【解决方案5】:

只是为了好玩,这是我第一个答案中解决方案的一个更实用的变体:

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

【讨论】:

  • 太棒了!谢谢@Bergi,这个“全速”运行良好(我还没有测试过另一个)
【解决方案6】:

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)),['']))

【讨论】:

    【解决方案7】:
    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;
    };
    

    【讨论】:

    • 谢谢。可在此处获取基准:jsfiddle.net/6cxEH。您的解决方案运行 100,000 次大约需要 0.6 秒。
    【解决方案8】:

    这是另一种方法。我将所有数组的索引视为一个数字,其数字都是不同的基数(如时间和日期),使用数组的长度作为基数。

    因此,使用您的第一组数据,第一个数字是以 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]]));
    

    【讨论】:

    • 感谢您的解决方案。基准可在此处获得:jsfiddle.net/XgyPC。它运行你的函数 100,000 次。在我的计算机上使用 Chrome 大约需要 1 秒。
    • 太棒了!感谢您运行基准测试。我想知道它会如何表现,并没有在这方面考虑太多。这是一个有趣的小问题要解决,所以我可能会再试一次。
    【解决方案9】:

    另一种 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]])));

    【讨论】:

      【解决方案10】:

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

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

      【讨论】:

        猜你喜欢
        • 2013-02-24
        • 2021-12-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-12-15
        • 2017-08-31
        相关资源
        最近更新 更多