【问题标题】:Two element combinations of an array javascript数组javascript的两个元素组合
【发布时间】:2016-10-16 23:59:04
【问题描述】:

如果我有一个字母数组,例如

['A','C','D','E']

我想找到这个数组的所有 2 个字母组合,不使用 2 个 for 循环的最佳方法是什么。例如:

for (var i=0; i<arr.length;i++) {
  for (var j=i+1; j<arr.length;j++) {
    console.log(arr[i] + " " + arr[j]);
  }
}

这个问题是,如果数组变得庞大(1000 个元素),它通常会超时。还有其他方法(替代数据结构等)吗?

【问题讨论】:

  • 这将是一个数组中的排列,并且有几个答案显示了如何做到这一点,但你必须迭代,可能不止一次。
  • n 选择 k 有一个通用形式的算法,但它在时间复杂度方面并不比你的代码更有效。 1000 选择 2 始终等于相同的数字,因此您无法在此区域进行优化。也许您应该重新考虑是否需要每种组合来解决您的问题?
  • 我问这个的主要原因是因为在 Hackerrank 上,我的算法通常会通过前几次测试,但是当数字太大或数组太大时...... javascript 超时。即使是一个非常简单的算法......
  • 向我们展示问题!简单并不意味着更快

标签: javascript arrays algorithm data-structures ecmascript-6


【解决方案1】:

使用.map()

类似的东西

var arr = ['A','C','D','E'],
    combinations = arr.map((v,i)=>arr.slice(i+1).map(v2=>v+v2));

console.log(combinations);

虽然这段代码也会对元素进行两次迭代。 (它实际上会比您的代码执行得更差,因为map 为每个项目执行一个函数,并且它还使用slice 创建临时数组副本,所以这里只是为了替代方法,而不是更高性能。 )

【讨论】:

  • 谢谢!还有一个问题,如果我们想要完全唯一的,例如 AC === CA,那么我们不想存储 CA 怎么办?
  • @TylerEvans 更新了答案,并添加了关于性能的注释。
  • 我看不出这有什么用!
  • 如果它的时间效率较低,那么使用漂亮的语法如何帮助 OP?他说 他的 代码正在超时,所以这不会更好。我注意到初学者中有时存在一种误解,即语法越简洁执行时间越快,我们都知道根本不是这种情况。
  • 好吧,在你说你在结论性陈述中面临超时问题的问题中,这就是我假设的原因。此解决方案还使用两个循环,它们只是被糖覆盖。
【解决方案2】:

不仅是两个,还有任意数量的元素,您可以执行以下操作;

Array.prototype.combinations = function(n){
  return this.reduce((p,c,i,a) => p.concat(n > 1 ? a.slice(i+1).combinations(n-1).map(e => [].concat(e,c))
                                                 : [[c]]),[]);
};

console.log(JSON.stringify([1,2,3,4,5,6].combinations(2)));
console.log(JSON.stringify([1,2,3,4,5,6].combinations(3)));

根据@Lucas Kot-Zaniewski 的 cmets,我已经重构了我的代码以使用 .push() 操作代替 .concat() 指令,并且在需要扩展操作的地方我确实使用了 Array.prototype.push.apply(context,[args])。当给定 100 个项目数组的输入并请求每个项目的两个组合时,这两个更改使代码的运行速度提高了 2.5 到 3 倍(导致 3.5-7 毫秒对 9.5-19 毫秒)。然而,一旦尝试使用 1000 个项目的 2 种组合,差异会更加显着,例如 400 毫秒与 6000 毫秒。

可以在https://repl.it/DyrU查看测试

Array.prototype.combinations = function(n){
  return this.reduce((p,c,i,a) => (Array.prototype.push.apply(p,n > 1 ? a.slice(i+1).combinations(n-1).map(e => (e.push(c),e))
                                                                      : [[c]]),p),[]);
};

console.log(JSON.stringify([1,2,3,4,5,6].combinations(2)));

【讨论】:

  • 您的解决方案很慢,因为您使用切片更新输入数组。
  • @Lucas Kot-Zaniewski 这不是一个超级执行者,但这不是因为.slice() 这是一种“非常”高效的数组方法。使这段代码变慢的是.concat() 操作。带有扩展运算符的.push() 会做得更好。我稍后会调查。
  • 我在我的解决方案中做了一些非常相似的事情(在一个递归中连接,然后将第 n 个递归路径的最终输出推送到整体解决方案),我得到了更快的结果。我看不出你还能怎么解释。 Slice 可能是有效的,但它仍然是线性的,所以当你不断调用它时肯定会有成本。我猜您正在将大量的数组副本添加到您并不真正需要的内存中,因此这可能是另一种解释。
  • @Lucas Kot-Zaniewski 我希望您再看看重构后的代码版本。通过将 Array 仿函数替换为命令式等效函数,仍然可以进一步提升该算法。
  • 哇,我不知道使用 push 而不是 concat 可以给你这样的优势!我在我的算法中实现了它,它仍然比你的快几倍。我并不是想吹嘘这一点,我只是想指出,FP 方法似乎很难在这里实现。区别现在必须来自拼接。
【解决方案3】:

我真的把这个打到了地上。正如预期的那样,@Louis Durand 对两个嵌套 for 循环的回答在包含 100 个字符串的数组上最快(在我的机器上大约 4 毫秒)。这表明在这种情况下,嵌套循环可能是您最好的选择。

第二快的是我的递归解决方案,它在大约 7-8 毫秒内完成。

第三个是@Redu 的回答,对于相同的任务,它在大约 12-15 毫秒内完成。我怀疑他的实现速度较慢,因为他在算法中使用 slice 方法来更新数组(其他答案只是增加索引而使输入数组保持不变,这要快得多)。此外,此实现导致输入数组的多个副本存储在内存中(每次调用该函数时,它都会从原始数组创建一个新的输入数组,并从中删除第一个元素)。这也可能会影响性能。

所以要回答你的问题:不,除了连接到字符串并在最后打印答案(Louis 建议的)之外,我认为没有更好的方法来处理你正在做的事情。

    var arr = [];
    for (var i = 0; i< 100; i++){
        arr.push(i+"");
      }
 /*
    console.time("test0");
    test0();

    function test0() {
    var s = "";
    for (var i=0; i<arr.length-1;i++) {
      for (var j=i+1; j<arr.length;j++) {
        s += arr[i] + " " + arr[j]+" ; ";
      }
      s += "\n";
    }

    console.log(s);
    }
    console.timeEnd("test0"); 
*/
   
    console.time("test1"); 
    test1();
    function test1() {
    var output = [];
    getCombos(0, 0, [], 2);
    console.log(JSON.stringify(output));

     function getCombos(index, depth, tmp, k){
     if(depth < k){
         for(var i = index; i<arr.length; i++){
             var tmp1 =  [arr[i]];
             Array.prototype.push.apply(tmp1, tmp);
             getCombos(i+1, depth+1,tmp1, k);
           }
        }else{
             output.push(tmp);
          }
        }
    }
    console.timeEnd("test1");

        /*
        console.time("test2"); 
        test2();
        function test2(){
    Array.prototype.combinations = function(n){
      return this.reduce((p,c,i,a) => (Array.prototype.push.apply(p,n > 1 ? a.slice(i+1).combinations(n-1).map(e => (e.push(c),e))
                                                                          : [[c]]),p),[]);

};

console.log(JSON.stringify(arr.combinations(2)));
    }

    console.timeEnd("test2");*/

这是一个递归解决方案,它不能解决您的时间复杂度问题,但可以考虑另一种方式。额外的好处是您可以将其推广到任何 k,这样您就不会只找到两个字母的组合。此外,您只需声明一个循环(尽管您的调用堆栈中将存在多个它的副本)

var arr = ["a", "b", "c", "d", "e"];
var output = "";
getCombos(0, 0, [], 2);
console.log(output);

 function getCombos(index, depth, tmp, k){
 if(depth < k){
     for(var i = index; i<arr.length; i++){
         var tmp1 =  [...tmp, arr[i]];
         getCombos(i+1, depth+1,tmp1, k);
       }
    }else{
         output += tmp.toString() + ";";
      }
    }

【讨论】:

  • 为什么有人会否决这个答案? oO 不错的测试,总结其他帖子。投我一票。
【解决方案4】:

我认为您的代码在这里有错误,永远不会使用 E。 应该是:

var s = "";
for (var i=0; i<arr.length-1;i++) {
  for (var j=i+1; j<arr.length;j++) {
    s += arr[i] + " " + arr[j]+" ; ";
  }
  s += "\n";
}
console.log(s);

请注意,如果您在控制台中记录所有内容,那么超时也就不足为奇了。

【讨论】:

  • 谢谢,我更新了我的算法。是的 - 但我的问题是,这是控制台记录数组中所有 2 个字母组合的最有效方法吗?
  • 这里最昂贵的操作是日志本身。因此,如果您真的想记录所有组合,也许您应该创建一个包含所有组合的字符串,然后打印一次。
猜你喜欢
  • 2019-01-12
  • 2013-07-09
  • 1970-01-01
  • 1970-01-01
  • 2018-12-13
  • 1970-01-01
  • 1970-01-01
  • 2019-08-12
  • 1970-01-01
相关资源
最近更新 更多