跟踪递归方法的进度
Posted
技术标签:
【中文标题】跟踪递归方法的进度【英文标题】:Tracking the progress of a recursive method 【发布时间】:2010-11-16 02:23:39 【问题描述】:我正在编写一个使用树结构的应用程序,所以当然我有一些递归方法可以遍历树的每个节点并做一些事情。问题是有时这些需要一段时间,我宁愿向用户显示某种进度条,而不是程序停止响应一段时间。
如果我正在遍历一个平面列表,我知道列表中有多少项,因此很容易跟踪循环的编号并相应地更新进度条。
但是使用递归方法迭代树结构,我不一定知道树一开始有多少个节点。我是否应该首先递归地阅读树并在运行执行我想做的任何事情的实际递归方法之前计算所有节点?或者也许只是在树中添加或删除节点时跟踪运行总数?有更好的选择吗?
【问题讨论】:
【参考方案1】:解决此问题的常用方法是显示正在更改的进度条,但不更新实际百分比。例如,Firefox 中的小加载图标是一个正在旋转的圆圈——你知道它在做某事,但你不知道它需要多长时间。也许只是这样做并添加一条消息“这可能需要一些时间,具体取决于有多少数据......”
【讨论】:
如此明显,以至于我必须阅读此内容才能考虑它(:谢谢你为我节省了几个小时的头痛,这可能会让我得出这个结论【参考方案2】:遇到同样的问题,如果您不知道有多少项目需要处理,我认为您无法为用户提供准确的已用百分比。相反,文本状态消息可能会有所帮助。
假设您能够知道在树的每个级别有多少对象(这应该是可能的,因为它是一个简单的计数),当它处理第一个对象并且有1 级 50 个对象:
处理:Level1 - 1/50
一旦它被递归调用,并开始查看当前级别 1 对象的级别 2,消息就会扩展以添加第二级状态:
处理:Level1 - 1/50,Level2 - 1/25
当您继续循环遍历给定级别的对象时,属于该级别的输出消息部分将会增加。所以对于第 2 层的第二个对象,你会得到:
处理:Level1 - 1/50,Level2 - 2/25
当您完成当前级别 1 对象的级别 2 后,删除消息的最后一部分并弹回调用级别,然后转到该级别的第二个对象:
处理:Level1 - 2/50
随着您在树上上下移动,您继续添加和删除消息部分,同时看到第 1 级的部分肯定在缓慢上升。它没有给出还剩多少的确切数字,但肯定会给用户一个大致的概念。
如果你的树很深,这条消息可能会很长。可能有更清洁的方法来做到这一点。
另外,我不会一直编辑消息。我会保留一个单例(如果进度指示正在由多个线程更新),其中包含级别及其状态的内部列表,并且只保留 ToString() 以保持整洁。
最后,拥有某种树进度指示器可能会很整洁,其中根据上述概念添加和删除子分支。肯定会比消息看起来更漂亮。
【讨论】:
【参考方案3】:如果树遍历的成本与实际操作的成本相比较低,那么值得预先计算进度数据。
最简单的方法
如您所述,计算所有节点,然后在应用操作时跟踪已完成的节点数。
足以应付 99% 的情况。需要注意的是,如果操作正在进行一些修剪(跳过不必要的节点等),则节点计数不能准确描述进度,因为跳过的节点对计数没有贡献。因此,完成后条形图可能会从 60% 跃升至 100%。 在下面的例子中,进程跳转到B下面的节点
A
/ \
/ \
(B) F
/|\ /|\
C D E G H I
进度条会显示:
[Node] [True Progress] [Displayed]
A 10% 10%
B 20% 20%
F 60% 30%
G 70% 40%
H 80% 50%
I 90% 60%
- 100% 100%
使用地标的更精确方式
您可以在初始设置期间放置地标。跟踪列表中的前 200 个项目。当你达到 200 时,放开列表中的所有其他对象(回到 100 项)。现在跟踪遇到的每个其他节点。当您遇到总共 400 个节点时,该列表将增长回 200 个对象。你再次放开其他人,但从这一点开始跟踪四分之一的物体,等等。
这样,地标列表在 100/200 个节点之间振荡。当树被完全解析时,列表上可能有大约 120 个对象,均匀分布在树周围。现在您可以开始操作了。当遇到一个节点时,检查它是否是列表。如果是,您可以将进度条跳转到该元素的位置(如果元素是#84,则进度为 84 / 120 = 70%
操作可能会跳过一些节点,但进度最终应该会遇到一些地标,所以进度会跳回到合适的进度比率。
所有数字都是可调整的,因此可以增加地标列表的大小以获得更好的精度/反应性。
使用相同的示例,列出 5 个地标
Node Landmarks Steps Comments
A [A----] 1 -
B [AB---] 1 -
A C [ABC--] 1 -
/ \ D [ABCD-] 1 -
/ \ E [ABCDE] 1 -
(B) F F [BDF--] 2 Cleanup of A&C&E
/|\ /|\ G [BDF--] 2 G is skipped
C D E G H I H [BDFH-] 2 -
I [BDFH-] 2 Done
我们得到一个包含 5 个地标的列表,每个地标代表 20%
现在在执行操作时(完全跳过 B),进度条将指示:
[Node] [True Progress] [Displayed]
A 10% 0%
B 20% 20%
F 60% 60%
G 70% 60%
H 80% 80%
I 90% 80%
- 100% 100%
改进
可以跟踪两个连续地标的最低共同祖先 (LCA)。 如果操作退出 LCA,它可以推断其进度已跳过下一个地标,因此即使跳过了地标 #85,也会提供一些进度信息。 进入和退出节点之间的行为会发生变化:您注册进入地标,并退出 LCA
Node Landmarks LCA Steps Comments
A [A----] [----] 1 -
B [AB---] [A---] 1 A is LCA of A&B
A C [ABC--] [AB--] 1 B is LCA of B&C
/ \ D [ABCD-] [ABB-] 1 B is LCA of C&D
/ \ E [ABCDE] [ABBB] 1 B is LCA of D&E
(B) F F [BDF--] [BA--] 2 Cleanup, recompute LCAs
/|\ /|\ G [BDF--] [BA--] 2 Skipped
C D E G H I H [BDFH-] [BAF-] 2 F is LCA of F&H
I [BDFHI] [BAFF] 2 Should be skipped, is Registered as last node of the tree (F is LCA of H&I)
现在在执行操作时(完全跳过 B),进度条将指示:
[Node] [True Progress] [Displayed] [Tracked ] [Tracked] [Comments]
[Landmark] [ LCA ]
->A 10% 0% B - Tracking landmark B...
->B 20% 20% D B Found Tracked Landmark B, Now tracking D (LCA of D,B is B)
(B) 20% 20% D B (Process skips Nodes C/D/E)
<-B 60% 40% F A Exiting LCA B, must have jumped over Landmark D, now tracking F
A 60% 40% F A -
->F 60% 60% H F Entering Landmark F, now tracking H (LCA of F,H is F)
->G 70% 60% H F -
->H 80% 80% I F Found Tracked Landmark H, now tracking I (LCA if H, I is F)
->I 90% 100% Found Tracked Landmark I (last item), operation should be complete
【讨论】:
【参考方案4】:你可以做几件事。您可以调整数据结构,以便每个节点存储以该节点为根的树的大小。除此之外:
如果树通常很大,那么单独运行以确定树的大小在性能方面可能不是一个好主意(尤其是如果树不完全适合缓存)。
如果树通常是相当平衡的,那么在处理完总共 n 个子树的第 k 个子树后,您已经遍历了大约 k /n*100% 的节点。您可以使用这些知识来衡量进度。
【讨论】:
【参考方案5】:如果使用状态标签来显示你所在的位置,那么当循环正在执行时,这些描述符对用户来说是有意义的......
例如,如果我正在遍历组织结构图,我会知道我的最终用户熟悉组织结构图中的大多数人,所以我会使用这些人的名字。例如,如果我向 Bob 汇报,Joe 汇报给 Sue,Joe 汇报给 Sue,当我的记录正在处理时,标签会显示类似...
Currently processing Sue\Joe\Bob\David
因此,您更新每个节点上的状态标签文本。只需确保在更改标签文本后调用 Application.DoEvents() 以便屏幕更新。如果你真的想显示你走了多远,进度条会更好,但这是一个在类似情况下对我有用的选项。它会就正在发生的事情提供一些反馈,而用户体验才是真正重要的。
然而,需要注意的是,更新标签文本并调用 Application.DoEvents() 实际上会减慢处理速度,但通常会得到回报,因为用户可以看到正在发生的事情并知道程序不只是“冻结”。
【讨论】:
【参考方案6】:我认为添加“已处理项目”计数通常比仅使用基本上是美化沙漏的来回进度条更可取。
最好,正如 OP 所建议的那样,让您的树容器在添加或删除项目时进行必要的簿记是报告进度百分比的好选择(可能使用包装类来执行此操作,特别是如果您想存储与在以特定于您的需要的方式处理树时可能被修剪的分支相关的信息)。
如果这不是一个选项,那么下面是我使用的一种算法,它适用于合理均匀分布的树(但否则相当糟糕!)。
ProcessTree 最初将被称为 ProcessTree(tree, 0, 0, 100)。为了更简洁的代码,可以将 itemsProcessed 和 percentDone 参数移到成员变量中。如果您的树允许您在不获取项目的情况下获得子计数,则可以省略子集合位:
Sub ProcessTree(node As Tree, ByRef itemsProcessed As Long, ByRef percentDone As Double, percentThisItem As Double)
Dim oChild As Tree
Dim oChildCollection As Collection
Dim percentPerChild As Double
ProcessNode(node)
itemsProcessed = itemsProcessed + 1
ReportProgress(itemsProcessed, percentDone)
Set oChildCollection = New Collection
For Each oChild In node.Children
oChildCollection.Add oChild
Next
If oChildCollection.count > 0 Then
percentPerChild = percentThisItem / oChildCollection.count
For Each oChild In oChildCollection
ProcessTree oChild, itemsProcessed, percentPerChild, percentDone
Next
Else
percentDone = percentDone + percentThisItem
ReportProgress(itemsProcessed, percentDone)
End If
End Sub
【讨论】:
【参考方案7】:一个好的进度条将基于节点总数(通过递归计数)。一个令人讨厌但可能更有效的进度条将是随着递归的每一步增加 MaxValue 的进度条(使您的进度步骤随着您的前进而变得更小)。可以通过在开始方法之前进行粗略估计来组合这些方法并随时更新。
我想微软的一种方法是使用 running-from-start-to-end-in-a-loop 进度条。
【讨论】:
有趣的方法(随着我们的进展增加最大值),但它会破坏进度条的整个目的,因为用户永远不会真正知道还有多少要做。 @DavidCatriel,如果您不从 MaxValue 1 开始,它不会破坏目的。您可以说:MaxValue 将至少为 30,因此我们将其设置为 30,如果结果是 33 我们可以在递归期间增加它。用户会有进步感。以上是关于跟踪递归方法的进度的主要内容,如果未能解决你的问题,请参考以下文章
QWidget::repaint:更新进度条时检测到递归重绘