【问题标题】:Sort a large collection while showing progress在显示进度的同时对大型集合进行排序
【发布时间】:2010-10-18 01:47:28
【问题描述】:

在更新进度条时对集合进行排序的最佳方法是什么?目前我有这样的代码:

for (int i = 0; i < items.size(); i++)
{
    progressBar.setValue(i);

    // Uses Collections.binarySearch:
    CollectionUtils.insertInOrder(sortedItems, item.get(i));
}

这显示了进度,但随着sortedItems 中项目数量的增加,进度条会变慢。有没有人有更好的方法?理想情况下,我想使用类似于Collections.sort() 的界面,以便尝试不同的排序算法。

任何帮助都会很棒!



作为背景知识,这段代码从 Lucene 拉回大量文档(1-1000 万)并在它们上运行自定义比较器。通过将数据写回磁盘来对它们进行排序将太慢而不实用。大部分成本是从磁盘读取项目,然后在项目上运行比较器。我的 PC 有大量内存,因此不存在与交换到磁盘等相关的问题。

最后我选择了 Stephen 的解决方案,因为它非常干净,让我可以轻松添加多线程排序算法。

【问题讨论】:

  • 您的进度条是否有一些可定义的最大值?因为 9000 长度数组的 30% 与 90 长度数组的 30% 有很大不同。
  • @Anthony 进度条的最大值是items.size()。我通常会排序数百万或数千万。
  • 我不会选择对内存中的数千万个项目进行排序。我更有可能将它们写入磁盘文件并调用操作系统排序。
  • 正如我在回答中所说,问题在于插入新项目的时间取决于集合的大小,因此随着集合变大,它的定义会变慢。请参阅我的答案以获得更详尽的解释。
  • 它可能会因为item.get(i) 而变慢 - 并非所有集合都适合索引访问。如果您可以包含更多源代码,特别是显示项目集合的声明方式,那就太好了。

标签: java sorting progress-bar


【解决方案1】:

你要在这里小心。您已选择使用一种算法来增量构建排序数据结构,以便(我接受)您可以显示进度条。但是,在执行此操作时,您可能选择了一种比最佳排序慢得多的排序方法。 (两种类型都将是 O(NlogN),但性能比大 O 行为更多......)

如果您担心这可能是个问题,请比较使用TreeMapCollections.sort 对典型集合进行排序的时间。后者的工作原理是将输入集合复制到一个数组中,对数组进行排序,然后将其复制回来。 (效果最好 如果输入集合是 ArrayList。如果您不需要将结果作为可变集合,您可以使用 Collection.toArrayArrays.sortArrays.asList 来避免最终复制。)

另一种想法是使用 Comparator 对象来跟踪它被调用的次数,并使用它来跟踪排序的进度。您可以利用比较器通常会被大致调用 N*log(N) 次这一事实,但您可能需要根据实际使用的算法进行校准1

顺便说一下,计算对比较器的调用次数比计算插入次数更能说明进度。当您接近完成排序时,您不会让进度看起来变慢。

(您将有不同的线程读取和写入计数器,因此您需要考虑同步。将计数器声明为volatile 会起作用,但会以额外的内存流量为代价。如果您也可以忽略这个问题很高兴进度条有时会显示陈旧的值...取决于您的平台等)


1 - 这有问题。在某些算法中,比较的次数可能会根据所排序数据的初始顺序而发生巨大变化。对于这样的算法,没有办法校准在“非平均”情况下工作的计数器。

【讨论】:

  • 自计数比较器非常漂亮。
【解决方案2】:

您可以使用indeterminate 进度条吗?这仍然会向用户提供一些正在发生的事情的反馈。您的代码如下所示:

progessbar.setIndeterminate(true);
ArrayList sorted = new ArrayList(items);
Colletions.sort(sorted);

progessBar.setString("Hey you're done!");

我认为您将通过使用内置排序而不是您正在执行的二进制插入排序获得更多更好的性能。

【讨论】:

  • 我可以使用不确定的进度条,但它不是很友好。由于我正在分类的项目的性质,整个过程可能需要 20 多分钟。
【解决方案3】:

为什么不实现自己的合并排序(Collections.sort 正在做的事情)并在算法的关键点更新进度条(例如,在每次合并超过 5% 的数组之后)?

【讨论】:

  • 正要说同样的话 :) 我的数学可能是错误的,但我认为您可以在每次合并后通过((100% / (lg n)) / 2^d 提高标准,其中d 是递归深度。反正就是这样。关键是,如果您跟踪深度,您可以使用它来计算每个单独的合并操作对进度的贡献。
【解决方案4】:

如果您只是比较排序时间,请打印排序前后的时间。

很难预测在野外进行排序需要多长时间。对于某些类型,它取决于输入的顺序。我会使用i/(double) items.size() 来生成已完成工作的比例并称之为美好的一天。您可以选择每items.size()/100 迭代更新栏。没有理由用无用的更新来抨击糟糕的进度条。

【讨论】:

  • 他的 cmets 说他正在使用 Collections.binarySearch,这在 Javadoc 中声明输入必须排序
【解决方案5】:

这里的问题是排序的物理机制 - 随着sortedItems 变大,insertInOrder 根据定义将花费更长的时间,因为它很可能是O(n lg n) + O(n) 操作(使用二进制搜索来查找下一个最小的项目然后插入项目)。不可避免的是,随着您的收藏越来越大,将下一个项目插入到正确的位置会花费更长的时间。

近似一个时间线性增加的进度条的唯一方法是使用类似于 lg 函数的逆函数的近似值,因为排序前 1000 个项目可能需要类似于排序最后 10 个项目的时间(即当然是概括)。

【讨论】:

  • lg 函数的反函数?我认为那将是……一个指数函数! ;)
  • 确实会。投稿后被打脸了,但觉得好笑不应该编辑。
【解决方案6】:

我可能错过了一些东西,因为没有其他人提到它,但听起来你的源 List 对象的运行时类型不是 RandomAccess 的实现者,因此你的 Collections.binarySearch 调用在 O(n ) 时间。当您将要排序的项目数量增加一倍时,这会大大减慢速度,非常明显。

此外,如果您将 LinkedList 用于 sortedItems,则插入也是 O(n)。

如果是这样,那么当您从 100 万件增加到 200 万件时,您的预期时间也将大约翻倍。

诊断 2 个List 对象中的哪一个有问题

  1. 如果进度条从一开始就很慢,那就是items;尝试使用不同的容器,类似于树或哈希的容器
  2. 如果进度条在接近 100% 时越来越慢,则为sortedItems;与上述相同的建议

请注意,导致减速的原因可能是 Lists。这也与进度条无关。您描述的问题是关于排序的算法,而不是进度条的更新。

【讨论】:

    【解决方案7】:

    进度条上的一个简单方法是这样。

    无论项目大小如何,您都可以使用 mod 修复更新进度的调用次数。例如,

    public void run(int total) {
        int updateInterval = total / 10;
        System.out.println("interval = " + updateInterval);
        for(int i = 0; i < total; i++) {
            if(i % updateInterval == 0) {
                printProgress((float)i / total * 100f);
            }
            // do task here
        }
    }
    
    private void printProgress(float value) {
        System.out.println(value + "%");
    }
    

    这将更新进度条 10 次(或 9?检查边界条件),无论大小是 10 还是 1000 万。

    这只是一个例子,请相应地调整值。

    【讨论】:

      猜你喜欢
      • 2011-04-06
      • 2017-02-22
      • 1970-01-01
      • 2018-04-18
      • 1970-01-01
      • 1970-01-01
      • 2021-03-04
      • 2011-10-20
      • 1970-01-01
      相关资源
      最近更新 更多