【问题标题】:Alternative to Nested Loop For Comparison用于比较的嵌套循环的替代方案
【发布时间】:2011-02-11 17:38:18
【问题描述】:

我目前正在编写一个程序,该程序需要比较可变大小的 ArrayList 中的每个文件。现在,我这样做的方式是通过嵌套代码循环:

         if(tempList.size()>1){
            for(int i=0;i<=tempList.size()-1;i++)
                //Nested loops.  I should feel dirty?
                for(int j=i+1;j<=tempList.size()-1;j++){
                    //*Gets sorted.
                    System.out.println(checkBytes(tempList.get(i), tempList.get(j)));
                }
            }

我已经阅读了一些关于嵌套循环必要性的不同意见,我想知道是否有人有更有效的选择。

一目了然,无论哪种方式,每次比较都需要进行,因此性能应该相当稳定,但我有理由相信有一种更清洁的方法可以做到这一点。有什么指点吗?

EDIT:: 为清楚起见,这只是功能的一部分。文件已根据长度进行比较并放入存储桶中 - 在遍历集合的映射并找到长度大于 1 的存储桶后,它会运行它。所以 - 这些都是相同大小的文件。在获取字节之前,我也会进行校验和比较,但现在我只是在尝试清理循环。

另外,天牛这个网站反应很快。谢谢各位。

EDIT2:: 抱歉,为了进一步澄清:我认为文件处理部分我已经掌握了 - 首先,我按长度比较和排序,然后按校验和,然后按字节 - 我遇到的问题是假设它们都需要比较,如何正确处理需要有效比较 ArrayList 中的所有文件。如果嵌套循环就足够了,那很酷,我只是想检查一下这是否是一种合适的方法,符合约定。

【问题讨论】:

  • 我会保持这种方式。我没有看到进行 n(n-1)/2 比较的更简洁的方法。
  • 看起来你可能每次比较都做两次,因为 checkBytes(a,b) 与 checkBytes(b,a) 相同。
  • 如果你真的需要嵌套循环,使用它们真的没有什么问题。比较不同的数组列表对当然应该是其中一种情况。如果不进一步了解 checkBytes 函数,您的代码就无法真正得到改进。
  • @jvilalta:不,因为他知道内循环的下界。
  • @KGVT - @Jack 和 @Donal Fellows 的答案已经解决了您问题的“EDIT2”部分。你不了解他们什么?

标签: java loops nested nested-loops


【解决方案1】:

将所有内容与其他所有内容进行比较必然是 O(n²)。但是你可以尝试一些技巧。主要是使比较更便宜;这可以通过为每个文件生成一个哈希码并首先比较它们来完成,这至少可以避免大多数比较(使用足够好的算法,你几乎可以避免每一个)。如果您不需要保留有关哪些文件相等的信息,您也可以加快速度;生成每个文件的哈希码Set,并在最后测试以查看集合的大小是否与文件列表的大小相同。

【讨论】:

  • 请注意,我假设您在这里比较平等。如果没有,并且您无法在哈希中捕捉要比较的内容的本质,那么您已经拥有了最好的基本算法。
  • 根据文件的内容,这实际上可能会更慢(当它们很长并且内容随机时会这样)。因为比较可以提前终止,而典型的 hashCode() 实现会查看整个文件。当然你可以只散列文件的一部分,但是你可能会遇到很多冲突——而且比较也不一定是顺序的。
【解决方案2】:

一个很好的优化是首先计算文件的所有哈希值,然后对列表进行一次循环。

这基本上是因为无论如何您都必须检查列表中的每一对文件,但这意味着每对文件的复杂度为 O(1),而不是为您要检查的每个文件计算很多东西.

你可以这样做:

HashSet<YourFile> fileSet = new HashSet<YourFile>();
ArrayList<YourFile> files = new ArrayList<YourFile>();

class YourFile
{
  int hashcode = -1;

  public int hashCode()
  {
     // override it to provide an hashcode based on file contents
     // you can also cache it to avoid recalculating anything

     if (hashcode == -1)
       hashcode = calculateIt();

     return hashcode;
  }
}

// fill up files
files.add(...);

// do comparisons
for (YourFile f : files)
{
  if (fileSet.contains(f))
    // f and fileSet.get(f) are equal: this is a tricky utilization of the hashCode() method so be careful about it!
  else
  {
    fileSet.put(f);
    // since there's not a file with same hashcode you just add this one
  }
}

这实际上会删除内部循环,因为当您使用 hashSet.contains 时,它会检查所有已添加的文件,但复杂度为 O(1)。

正如 doublep 所述,您必须注意性能,因为当您明确检查字节时,一旦发现两个不同的字节就会停止,同时计算哈希需要检查整个文件。当您有很多文件或文件很小时,这将很有效。最好的办法是对这两种方法进行基准测试,看看是否存在显着差异。

【讨论】:

  • 这个算法有点不对。您的代码不处理两个文件的 hashCode 相等但文件不相等的情况。由于您使用的 hashCode 仅返回 2**32 不同的值,因此不能忽略发生这种情况的可能性。
  • 根据生日悖论,您至少需要 2*(2**16) 个文件才能以相当大的概率发生冲突。但是实际上,由于实际上您最终会拥有少量文件(或者至少我想我们不是在谈论数百万个文件),因此如果结果相等,我们可以使用常规方法检查文件。这不应该影响性能。
【解决方案3】:

根据您的具体操作,您可能会通过从不比较不同大小的文件来获得相当大的加速。正如其他答案中所建议的,在相同大小的文件中,仅比较具有相同哈希值的文件(通过任何算法)。

编辑:

不过,计算哈希值可能会产生反作用。首先,如果您只是将文件与另一个文件进行比较,则永远不要这样做:您需要完全读取文件以构建哈希,并且一次读取就足以进行比较,因此您将一无所获。

其次,如果您很少期望匹配并且实际上文件会有很大差异(早期),那么无论要比较的文件数量如何,计算哈希值都可能适得其反。这是因为在这种情况下失败的比较会提前失败(即不读取整个文件),而对于哈希构建,您将需要完整读取。或者,您可以构建“部分”哈希(例如,文件的前 10 kb 的哈希),但请记住使用所有文件的相等块。

【讨论】:

    【解决方案4】:

    一个小的清理方法是删除初始大小测试 - 如果大小小于 2,它会简单地掉出来而无需进行任何比较。更好地遵守 Java 编码约定将是在循环中比较 i &lt; tempList.size() 而不是 i &lt;= tempList.size() - 1 - 这只会使您的代码更容易被其他程序员理解。这些更改都不会对性能产生任何影响。

    for (int i = 0; i < tempList.size(); i++)
        for (int j = i + 1; j < tempList.size(); j++) {
            //*Gets sorted.
            System.out.println(checkBytes(tempList.get(i), tempList.get(j)));
        }
    

    【讨论】:

    • 谢谢,我有点傻。
    • 子问题:这个函数在程序执行过程中会执行多次,我预计大多数 ArrayLists 的大小都是 1,因为这个程序正在检查重复文件和大多数文件将(希望)是唯一的 - 删除 if 语句意味着它检查并进入第一个 for 循环,然后检查并失败第二个 for 循环,这意味着它执行了两次比较而不是一次。它相对较小,但这仍然是一个适当的行动吗?还是期望它在大多数情况下都失败会否定改变它的需要?
    • @KGVT:我认为这不会对任何现代系统产生可衡量的影响。是的,从技术上讲,这是常见情况下的一个额外比较,但它还不够重要。如果您的程序太慢,请对其进行检测并找出瓶颈;我不相信这会是其中之一。
    • 第一个循环实际上应该有条件i &lt; tempList.size() - 1,或者最后一次通过第一个循环第二个循环永远不会运行。
    • @Gabe:循环运行零次没有害处。
    【解决方案5】:

    我对您的 EDIT2 问题的回答分为两部分

    部分是如果您有少量文件,那么您的嵌套循环方法应该没问题。性能为O(N**2),最优解为O(N)。但是,如果N 足够小,则使用哪种方法不会有太大区别。如果您确定 N 可以很大,则只需要考虑替代解决方案。

    第二部分阐述了一种利用文件哈希获得O(N) 解决方案来检测重复项的算法。这就是前面的答案所暗示的。

    1. 创建一个FileHash 类来表示文件哈希值。这需要定义实现文件哈希的字节相等的equals(Object)hashCode() 方法。

    2. 创建一个HashMap&lt;FileHash, List&lt;File&gt;&gt; 地图实例。

    3. 对于您输入ArrayList 中的每个File

      1. 计算文件的哈希值,并为其创建一个FileHash 对象。
      2. 在地图中查找FileHash
      3. 如果找到条目,请对当前文件与从地图中获得的列表中的每个文件进行逐字节比较。如果您在列表中发现重复文件,BINGO!否则将当前文件添加到列表中。
      4. 如果您没有找到条目,请创建一个新的映射条目,其中“FileHash”作为键,当前文件作为值列表的第一个元素。

    (请注意,上面的地图实际上是一个多地图,并且有可用的 3rd 方实现;例如,在 Apache commons 集合和 Google 集合中。为了简单起见,我在上面的表格中展示了算法。 )

    一些性能问题:

      1234563文件的比较不会说文件是平等的,也小得离谱。但是,计算加密哈希的成本将大于计算质量较低的哈希的成本。
    • 如果您确实使用了质量较低的哈希,则可以通过在进行逐字节比较之前查看文件大小来降低比较更多文件的潜在成本。如果你这样做,你可以将地图类型设为HashMap&lt;FileHash, List&lt;FileTuple&gt;&gt;,其中FileTuple 是一个包含File 及其长度的类。

    • 您可以通过仅使用(例如)每个文件的第一个块的散列来降低散列的成本。但这增加了两个文件可能具有相同哈希但仍然不同的可能性;例如在第二个街区。这是否重要取决于文件的性质。 (但例如,如果您只是对源代码文件集合的前 256 个字节进行校验和,则可能会发生大量冲突……由于存在相同的版权标头!)

    【讨论】:

      猜你喜欢
      • 2013-08-23
      • 2021-05-25
      • 2017-09-03
      • 2015-06-29
      • 2018-04-15
      • 1970-01-01
      • 2020-03-09
      • 2020-01-15
      • 1970-01-01
      相关资源
      最近更新 更多