【问题标题】:Double for loop performance issues双循环性能问题
【发布时间】:2014-06-18 10:03:26
【问题描述】:

对于含糊的标题表示歉意,真的不知道如何描述这个问题。

我最近遇到了一种情况,我必须遍历一个对象数组来比较多个值,我选择在 for 循环中使用 for 循环来比较每个对象与其他每个对象。

虽然这适用于小型阵列,但一旦我的阵列变得更大(比如 10,000 个对象),性能往往会成为一个大问题。

这个数组包含这些类型的对象:

[{ char: '_', from: 0, to: 2, chrLength: 2 },
{ char: '_', from: 0, to: 7, chrLength: 7 },
{ char: 'a', from: 1, to: 3, chrLength: 2 },
{ char: 'a', from: 1, to: 6, chrLength: 5 },
{ char: '_', from: 2, to: 7, chrLength: 5 },
{ char: 'a', from: 3, to: 6, chrLength: 3 }]

我的想法是我只能选择fromto 不与任何其他对象重叠的对象。 (fromto 是另一个数组中的索引)

所以对于示例数组,可能的结果是:

[{ char: '_', from: 0, to: 2, chrLength: 2 },
 { char: 'a', from: 1, to: 3, chrLength: 2 },
 { char: 'a', from: 1, to: 6, chrLength: 5 },
 { char: 'a', from: 3, to: 6, chrLength: 3 }]

我的处理方式如下:

var canUse = true,
    posibilities = [];
for(i = 0; i < l; i++) {
    canUse = true;
    for(var j = 0; j < l; j++) {
        if((results[i].from < results[j].from && results[i].to > results[j].to)) {
            canUse = false;
            break;
        }
    }

    if(canUse) posibilities.push(results[i]);
}

看到较大数组的性能非常糟糕,我想知道是否有更好的解决方案来做到这一点?

【问题讨论】:

  • { char: '_', from: 2, to: 7, chrLength: 5 } 不应该出现在结果中吗?
  • @LIUFA:不,因为{ char: 'a', from: 3, to: 6, chrLength: 3 } 适合它。
  • @Jashwant 不太清楚你的意思,你能提供一个小例子吗?
  • @woutr_be,抱歉误解了您的问题。删除了我的评论。
  • 正确的数据结构会有所帮助。 en.wikipedia.org/wiki/Interval_tree

标签: javascript arrays performance object for-loop


【解决方案1】:

这是想法(Demo):

  1. 您需要某种支持 O(log n) 的插入和删除操作的自平衡树。为了简单起见,我使用了红黑树。
  2. 您需要使用间隔的中点作为键,即(from + to)/2
  3. 假设您已经将k 项目“插入”到您的树中并且即将插入k+1。您的步骤是:
  4. 如果k+1 覆盖了根 - 忽略它。
  5. 如果 k+1 被根覆盖 - 从树中移除根并再次尝试。
  6. 通过比较 k+1 的中点与根的中点,继续向左或右子树。
  7. 插入所有内容后,遍历生成的树,收集每个节点。
  8. 利润...通过将其与自身连接,我使用了您的数组的 4 倍。我的机器在 Chrome 下的结果是 116 毫秒(冷启动)和 64 毫秒(预热后)。

代码

 function process() {
    console.log('Processing results of length: ' + l);

    console.time('Processing');

    var comparator = function(a, b) { //Comparator to build a tree
           return a.mid - b.mid;
        },
        isAinB = function(a, b) { //util function to check if a is inside b
            return b.from < a.from && b.to > a.to;    
        },
        rbtree = new RBTree(comparator), //Build an empty tree
        i = results.length - 1, item, posibilities = [];

    function check(root, x) { //Recursive checker
        var data;        

        if(!root) { //Either tree is empty or we've reached a leaf
            rbtree.insert(x);
            return;
        }

        data = root.data;

        if(isAinB(data, x)) { //4
            return;    
        }
        if(isAinB(x, data)) { //5
            rbtree.remove(data);
            check(rbtree._root, x);
            return;
        }    

        check(root[comparator(data, x) > 0 ? 'left' : 'right'], x); //6
    }

    for(; i >= 0; i--) { 
        item = results[i];
        item.mid = (item.from + item.to)/2; //2
        check(rbtree._root, item); //3
    }

    rbtree.each(function(item) { //7
        posibilities.push(item);
    });
    console.timeEnd('Processing');

    console.log(posibilities.length);
}

顺便说一句,我用过这个RBTree implementation。不知道是不是最好的:)

【讨论】:

  • 非常感谢您的示例,我对树没有任何经验,现在查看 RBTree 看看是否可以重新创建您的演示。
  • @woutr_be 当您有某种基于比较的查询时,自平衡树是非常有用的数据结构。 mitpress.mit.edu/books/introduction-algorithms第12-13章
【解决方案2】:

首先对chrLength 属性上的对象进行排序。当您查找阻止对象被包含的对象时,您只需检查至少短两个字符的对象。

results.sort(function(x, y){ return x.chrLength - y.chrLength; });

var posibilities = [];
for (var i = 0; i < l; i++) {
  var canUse = true, len = results[i].chrLength - 2;
  for (var j = 0; results[j].chrLength <= len; j++) {
    if((results[i].from < results[j].from && results[i].to > results[j].to)) {
      canUse = false;
      break;
    }
  }
  if(canUse) posibilities.push(results[i]);
}

使用您的示例数据,这会将检查次数从原始代码中的 36 次减少到仅 8 次。

比较:http://jsfiddle.net/Guffa/5jsSb/

编辑:

您可以创建一个数组,其中每个项目都是具有相同chrLength 的对象数组,然后根据from 属性对每个数组进行排序。这样您就可以轻松地跳到对象开始重叠的点,并在它们不再重叠时停止比较:

var map = [];
for (var i = 0; i < l; i++) {
  var ch = results[i].chrLength;
  while (map.length <= ch) map.push([]);
  map[ch].push(results[i]);
}
for (var i = 1; i < map.length; i++) {
  map[i].sort(function(x, y){ return x.from - y.from; });
}

var posibilities = [];
for (var i = 0; i < l; i++) {
  var canUse = true, len = results[i].chrLength - 2, from = results[i].from, to = results[i].to;
  for (var j = 1; canUse && j <= len; j++) {
    if (map[j][map[j].length - 1].from > from) {
      var k;
      for (k = 0; map[j][k].from <= from; k++);
      for (;k < map[j].length && map[j][k].from < to; k++) {
        if (map[j][k].to < to) {
          canUse = false;
          break;
        }
      }
    }
  }
  if(canUse) posibilities.push(results[i]);
}

这会将fromto 属性的检查分为两个阶段,因此完整检查的数量(评估map[j][k].to &lt; to)实际上小于对象的总数。

免责声明:您自然需要验证代码是否正确。我检查了结果中的项目数量是否相同,但我没有比较每个项目。

【讨论】:

  • 谢谢,这似乎确实减少了检查的数量,但对于一些更大的数组,它仍然需要相当多的时间。只是想知道在执行 for 循环之前是否有任何方法可以消除项目
  • @woutr_be:对于较大的集合,检查的数量如何变化?考虑到你永远不会比 O(n) 更好,即每个项目至少检查一次。你能展示一个更大的示例集吗?很难在一个小集合中找到任何有用的模式,可以用于消除。
  • 当然,例如,这是一个有 1000 个项目的jsfiddle.net/5jsSb/1 还有一个更极端的情况:jsfiddle.net/5jsSb/3(尽管我的最终数组是两倍大小)
  • @woutr_be:很好,这使得测试一些想法变得更容易。我做了一些改进,并发布了上面的代码。可能会进行更多优化,但很难分析代码中实际发生的情况,它几乎无法使用那么多数据运行。浏览器实际上崩溃了一次。 :)
  • 伙计们,我认为你走错路了。您需要某种数据结构来回答“给定间隔是否重叠”的问题,时间为 O(log n) 次。有一种称为区间树的数据结构(谷歌为它)。构造它需要 O(nlog n) 并且需要 n 次 O(log n) 来检查每个间隔是否有重叠。结果复杂度将是 O(nlog n)。
【解决方案3】:

对于初学者来说,一旦canUsefalse,您就不需要继续使用内部循环了。

您可以添加 break; 或将第二个 for 循环更改为:

for (var j = 0; canUse &amp;&amp; (j &lt; l); j++)

你可能会看到一个有用的加速。

【讨论】:

  • 不错的地方,完全忽略了。
猜你喜欢
  • 2013-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多