我的讲师今天提到,可以在Java中“标记”循环,以便在处理嵌套循环时可以引用它们。因此,我在不了解该功能的情况下查找了该功能,并在很多地方对该功能进行了解释,然后警告并阻止嵌套循环。
我真的不明白为什么吗?是否因为它影响代码的可读性?还是更具“技术性”?
我的讲师今天提到,可以在Java中“标记”循环,以便在处理嵌套循环时可以引用它们。因此,我在不了解该功能的情况下查找了该功能,并在很多地方对该功能进行了解释,然后警告并阻止嵌套循环。
我真的不明白为什么吗?是否因为它影响代码的可读性?还是更具“技术性”?
Answers:
嵌套循环很好,只要它们描述正确的算法即可。
嵌套循环具有性能方面的考虑(请参见@ Travis-Pesetto的答案),但是有时它是完全正确的算法,例如,当您需要访问矩阵中的每个值时。
Java中的标记循环允许在其他嵌套循环繁琐的时候过早地脱离几个嵌套循环。例如,某些游戏可能具有如下代码:
Player chosen_one = null;
...
outer: // this is a label
for (Player player : party.getPlayers()) {
for (Cell cell : player.getVisibleMapCells()) {
for (Item artefact : cell.getItemsOnTheFloor())
if (artefact == HOLY_GRAIL) {
chosen_one = player;
break outer; // everyone stop looking, we found it
}
}
}
尽管上述示例中的代码有时可能是表达某种算法的最佳方式,但通常最好将此代码分解为较小的函数,并最好使用return
代替break
。所以break
带标签的气味是微弱的; 看到它时要多加注意。
嵌套循环经常(但并非总是)是不好的做法,因为嵌套循环经常(但并非总是)对您尝试执行的操作产生过大的杀伤力。在许多情况下,有一种更快,更省钱的方法来实现您要实现的目标。
例如,如果列表A中有100个项目,列表B中有100个项目,并且您知道列表A中的每个项目都与之匹配,则列表B中有一个项目(“ match”的定义故意含糊不清)在这里),并且您想要生成一个对列表,执行此操作的简单方法如下:
for each item X in list A:
for each item Y in list B:
if X matches Y then
add (X, Y) to results
break
每个列表中有100个项目,这将平均需要100 * 100/2(5,000)个matches
操作。如果有更多的物品,或者如果不能确保1:1的相关性,则价格会更高。
另一方面,有一种更快的方法来执行这样的操作:
sort list A
sort list B (according to the same sort order)
I = 0
J = 0
repeat
X = A[I]
Y = B[J]
if X matches Y then
add (X, Y) to results
increment I
increment J
else if X < Y then
increment I
else increment J
until either index reaches the end of its list
如果您采用这种方式,则不再matches
基于进行操作length(A) * length(B)
,而是基于进行操作length(A) + length(B)
,这意味着您的代码将运行得更快。
O(n log n)
,如果使用Quicksort ,则排序设置将花费很短的时间(两次)。
O(n^2)
对N的非微小值
<
运算符是可比较的,通常不能从matches
运算符中得出。其次,即使X和Y都是数字,第二种算法仍然可能产生错误的结果,例如X matches Y
is X + Y == 100
。
避免嵌套循环的一个原因是,将块结构嵌套得太深是个坏主意,无论它们是否为循环。
每个函数或方法都应该易于理解,既要达到目的(名称应表示其功能),也要使维护者易于理解(应该易于理解内部原理)。如果函数太复杂而难以理解,通常意味着应该将某些内部因素分解为单独的函数,以便可以在名称(现在更小)的主函数中引用它们。
嵌套循环可能很难相对较快地理解,尽管某些循环嵌套很好-正如其他人指出的那样,这并不意味着您正在使用非常(且不必要)的慢速算法造成性能问题。
实际上,您不需要嵌套循环即可获得非常慢的性能范围。例如,考虑一个循环,该循环在每次迭代中从队列中取出一个项目,然后可能放回多个项目-例如,迷宫的广度优先搜索。性能不是由循环嵌套的深度(仅为1)决定的,而是由队列最终耗尽(如果已经耗尽)之前放入该队列的项目数决定的-可达部分的大小迷宫是。
考虑到许多嵌套循环的情况,您最终将需要多项式时间。例如,给出此伪代码:
set i equal to 1
while i is not equal to 100
increment i
set j equal to 1
while j is not equal to i
increment j
end
end
这将被视为O(n ^ 2)时间,该时间图类似于:
其中y轴是程序终止时间,而x轴是数据量。
如果获取的数据过多,您的程序将变得如此缓慢,没人会等待它。而且我相信花不了太多的大约1000个数据条目。
驾驶三十吨卡车而不是小型乘用车是不好的做法。除非您需要运输20或30吨的东西。
使用嵌套循环时,这不是一个坏习惯。这要么完全是愚蠢的,要么恰恰是所需要的。你决定。
但是,有人抱怨标记循环。答案是:如果必须提出问题,则不要使用标签。如果您足够了解自己的决定权,那么您就可以自己决定。
嵌套循环本质上没有错误,甚至没有坏处。但是,它们确实具有某些考虑因素和陷阱。
导致您浏览的文章可能是出于简短的目的,或者是由于被称为“被烧毁”的心理过程所致,因此跳过了细节。
被烧毁是当您对某事有负面的经历,并且暗示要避免时。例如,我可能用锋利的刀切蔬菜并割伤自己。然后我可能会说,锋利的刀不好,不要用它们切菜,以免再次发生这种糟糕的经历。这显然是不切实际的。实际上,您只需要小心。如果您要告诉别人切蔬菜,那么您对此会有更强烈的认识。如果我指示孩子们切蔬菜,我会非常强烈地告诉他们不要使用锋利的刀,特别是如果我不能密切监督他们的话。
编程中的问题是,如果您始终优先选择安全性,那么您将无法达到峰值效率。在这种情况下,孩子们只能切软菜。面对其他任何东西,他们只会用钝刀弄乱它。重要的是要学习正确使用循环(包括嵌套循环)的方法,如果它们被认为是不好的并且永远不要尝试使用它们,则不能这样做。
正如这里的许多答案所指出的,嵌套的for循环表明程序的性能特征,每次嵌套可能使指数性能恶化。也就是说,O(n),O(n ^ 2),O(n ^ 3)等包含O(n ^ depth),其中depth表示您嵌套了多少个循环。随着嵌套的增长,所需的时间呈指数增长。问题在于,不确定时间或空间的复杂性(通常是* * b * c,但并非所有嵌套循环都可以一直运行)还是不确定性即使存在性能问题。
对于许多人来说,尤其是坦率的学生,作家和讲师,很少为生活而编程,或者每天为循环而编程,这也可能是他们不习惯的,并且在初次接触时会引起过多的认知负担。这是一个有问题的方面,因为总会有一条学习曲线,要避免将学习曲线转化为程序员不会有效。
嵌套循环会变得很疯狂,也就是说,它们最终可能嵌套得非常深。如果我遍历每个大洲,然后遍历每个国家,然后遍历每个城市,遍历每个商店,然后遍历每个货架,然后遍历每种产品(如果是一罐豆子,遍历每种豆子,并测量其大小即可得出平均值)可以看到它会非常深地嵌套。您将拥有一个金字塔,并且左边缘有很多浪费的空间。您甚至可能最终离开页面。
在屏幕较小且分辨率较低的情况下,这个问题在历史上会变得更加重要。在那种情况下,即使是多层嵌套也可能真正占用大量空间。今天,这是一个较小的问题,尽管如果有足够的嵌套它仍然会出现问题,但是阈值较高。
相关的是美学论点。与具有更一致的对齐方式的布局相比,许多人发现嵌套环在美学上并不令人愉悦,这可能会或可能不会与人们习惯使用的东西,眼睛跟踪和其他问题相关联。然而,这是有问题的,因为它倾向于自我增强并且最终可能使代码更难阅读,因为破坏了代码块,并且封装了诸如函数之类的抽象后面的循环也冒着破坏代码到执行流的映射的风险。
人们习惯了自然的倾向。如果您以最简单的方式进行编程,则不需要嵌套的概率最高,需要一个级别的概率下降一个数量级,而另一个级别的概率又下降。频率下降,从本质上讲,意味着嵌套越深,人类的感知就越缺乏训练。
与此相关的是,在任何可以考虑嵌套循环的复杂构造中,您都应该总是问:最简单的解决方案,因为遗漏的解决方案可能需要更少的循环。具有讽刺意味的是,嵌套解决方案通常是最简单的方法,可以以最小的工作量,复杂性和认知负荷来工作。嵌套循环通常是很自然的。例如,如果您考虑上面的答案之一,那么比嵌套的for循环更快的方法也要复杂得多,并且包含更多的代码。
需要进行大量护理,因为通常有可能将环路抽象化或弄平,但最终结果是治愈的结果比疾病还差,尤其是如果您没有例如从这项工作中获得可衡量的显着性能提升。
人们经常会遇到与循环相关的性能问题,这些循环会告诉计算机重复执行多次操作,并固有地会牵涉到性能瓶颈。不幸的是,对此的回应可能是非常肤浅的。人们通常会看到一个循环,看到一个不存在的性能问题,然后将循环隐藏起来,看不到任何实际效果。该代码“看起来”很快,但将其投放在道路上,键入点火开关,放下加速器并查看车速表,您可能会发现它仍然和老太太走齐默车架一样快。
这种隐藏类似于您的路线上有十个抢劫犯。如果不是沿着一条直线走到您要去的地方,而是安排它,以便在每个拐角后面都有一个抢劫犯,那么当您开始旅程时,就给人一种幻觉:没有抢劫犯。眼不见,心不烦。您仍然会被抢劫十次,但现在您看不到它来了。
您所提出的问题的答案是两者都存在,但没有一个问题是绝对的。它们要么完全是主观的,要么只是上下文客观的。不幸的是,有时候,完全主观的或相当主观的意见占据主导地位。
根据经验,如果它需要嵌套循环,或者看起来像是下一个显而易见的步骤,那么最好不要仔细研究并简单地做到这一点。但是,如果仍有任何疑问,则应稍后进行审查。
另一个经验法则是,您应始终检查基数,并问自己这个循环是否会成为问题。在前面的示例中,我经历了城市。为了进行测试,我可能只会经历十个城市,但是在实际使用中可以预期的合理最大城市数量是多少?然后,对于大陆,我可能会将其乘以相同。始终考虑使用循环是一个经验法则,尤其是要迭代动态(可变)的次数,这可能会转化为下一行。
不管总是先做什么。当您确实看到优化机会时,您可以将优化的解决方案与最容易上手的解决方案进行比较,并确认它产生了预期的收益。在进行测量之前,您还可能花费太长时间进行过早的优化,这会导致YAGNI或大量的时间浪费,以及错过了截止日期。
n
,它的几何或多项式。