有没有一种“聪明”的方式来打破嵌套循环
本文关键字:方式 嵌套循环 聪明 一种 有没有 | 更新日期: 2023-09-27 17:49:33
(现在我主要使用C#。欢迎使用其他语言的想法,但如果可以,请将它们翻译成 C#,并且要明确。
我一次又一次遇到的是嵌套循环,搜索一些 2D 数组以找到一个元素(通常是某个对象(,然后必须对其进行操作。所以当然,一旦你找到了那个对象,你应该打破两个循环,这样你就不会不必要地继续搜索你已经找到的东西(特别是在可以遍历指数级巨大数组的嵌套循环中(。
以下代码是我目前首选的方法:
Obj O = null;
bool KeepLooping = true;
for (int j = 0; j < height && KeepLooping; j++)
{
for (int i = 0; i < width; i++)
{
if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value)
{
O = ObjArray[i, j]; // you found it, now remember it
KeepLooping = false; // clear the flag so the outer loop will break too
break;
}
}
}
感谢Erik Funkenbusch,如果我们这样做,它会变得更加优雅:
Obj O = null;
for (int j = 0; j < height && O == null; j++) // much, much better idea to check O for null in the outer loop
{
for (int i = 0; i < width; i++)
{
if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value)
{
O = ObjArray[i, j]; // you found it, now remember it
break;
}
}
}
不再需要那个讨厌的额外布尔值!
然而,寻找替代或更好的解决方案仍在继续。多年来,我尝试了许多其他方法,但由于某种原因发现它们不是那么好:
- 将
j
(外循环迭代器(设置为高于height
的值,这将触发它自动中断。不理想,因为有时您想记住找到它的i
和j
值。 - 在 2D 阵列上使用
foreach
。不理想,因为foreach
不会让你操作集合(删除或添加到它,这通常是我搜索对象的原因(。 - 只需将 2 个循环放在一个函数中,该函数除了查找并返回
O
之外什么都不做。return
有效地打破了这两个循环。很多时候这是可以的,但并非总是如此。我这样做是为了非常通用的搜索,但也有相当多的"组搜索">,我想将遍历集体化。在这些情况下,我找到 2 个或更多对象(有时在同一个 2D 数组中(,记住它们,然后才打破两个循环。 - 使用
goto
?(哇,这可能是goto的唯一合法用途吗?令人惊讶的是,它比KeepLooping
标志更具可读性,特别是如果我们有 3 个或更多循环。不理想,因为同事会尖叫血腥的谋杀。而在 C# 中,goto
后会有正确的垃圾清理吗? - 引发自定义异常? idk,我从未尝试过,但它看起来不如我目前的首选方式可读。
- 找到正确的对象后,在内部循环中执行所有对象操作,然后
return;
这可能会很快变得混乱。有时,对象操作涉及其自己的循环。
还有一个非常聪明的第 7 条路,这要归功于User_PWY:
int size = width*height; // save this so you dont have to keep remultiplying it every iteration
for (int i = 0; i < size; i++)
{
int x = i % width; // ingenious method here
int y = i / width; // ingenious method here
O = ObjArray[x, y];
if (O != null)
break; // woohoo!
}
这有效地将 2D 数组压缩成一个for
循环以进行迭代。然而,一些批评指出,与 i++ 或 j++ 相比,mod 和除法运算符非常慢,因此可能会更慢(请记住,我们正在处理谁知道大小的 2D 数组(。就像我评论的那样,应该有一种方法可以在一个操作中获取除法和余数,因为我很确定 x86 汇编代码具有 DIV 操作码,这些操作码将商和余数存储在单独的寄存器中,所有这些都在一个 DIV 指令中。但是如何在 C# 中做/使用它,idk。
如果 C# 允许你命名循环,例如 L1
和 L2
,然后执行类似 L1.break()
的操作,那就太好了;不管你在哪个循环中。 唉......它不能用这种语言完成。(是否有一些使用宏的秘密方法?是否会有实现此功能的 C# 6.0?
编辑:在我看来,我根据它们的优雅和速度来判断解决方案。请记住,我们正在处理嵌套循环,它可能会呈指数级增长。额外的操作或比较可能会有所作为。
好吧,好吧,告诉我你喜欢的方式,特别是如果它没有在这里列出。
>goto
是一个很好的解决方案,Microsoft甚至推荐它:
goto 语句将程序控制直接转移到标记的 陈述。
goto 的常见用途是将控制权转移到特定的开关盒 标签或开关语句中的默认标签。
goto 语句对于摆脱深度嵌套循环也很有用。
至于你关于对象销毁的问题,一旦你超出了作用域对象的范围,垃圾回收器应该将其标记为销毁,但我不能确定行为是否相同,例如,如果你在for
循环周围使用using
指令并goto
它而不是正常退出范围。
for (int i = 0; i < width*height; i++)
{
int x=i%width
,y=i/width;
//dostuff
}
我喜欢这种访问 2D 数组的方式。
评论 1(
有很多评论担心mod(%(运算符可能很昂贵,但这是我们正在谈论的整数运算,我认为它应该没有其他解决方案的区别。
评论 2(
关于宏。我找不到代码,但设法简短地生成了一个。
#define FOR2DARRAY(WIDTH,HEIGHT)
'for (int i = 0, x = 0,y = 0; i < (WIDTH)*(HEIGHT); i++, x=i%(WIDTH),y=i/(HEIGHT))
重构以避免深度嵌套,可能使用 LINQ 的切肉刀可能是更好的解决方案。
如果您不能想出更好的方法,在这种情况下Goto
是可能的(这基本上只是使用它的情况(。使用可读名称设置标志可能会导致更少的问题/尖叫。
不要
- 对流控制使用例外
- 更改循环变量。在这种情况下,弄清楚会发生什么是非常困难的。我敢打赌,大多数人会接受
goto
更好的方法。
差别不大,但我更喜欢以这种方式使用布尔标志:
Obj O = null;
bool found = false;
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
if (ObjArray[i, j] != null && ObjArray[i, j].property = search_value)
{
O = ObjArray[i, j]; // you found it, now remember it
found = true; // clear the flag so the outer loop will break too
break;
}
}
if (found) break;
}
if (!found)
{
// The loop finished with no values found
}
else
{
// Do stuff here with the value found
}
<</div>
div class="answers"> 试试这个:
Obj O = ObjArray.Cast<Obj>().FirstOrDefault(obj => obj != null && obj.property == search_value);
这会将 2D Obj
数组转换为IEnumerable<Obj>
,并为您提供第一个符合您条件的数组。如果它们都不匹配,则Obj O
设置为 null
。
在这里查看: https://dotnetfiddle.net/dQTJbU
也许我在您的选项列表中错过了它,但是为什么您不能将搜索重构为返回对象的方法?
private object FindObject(....)
{
for (int j = 0; j < height && KeepLooping; j++)
{
for (int i = 0; i < width; i++)
{
if (ObjArray[i, j] != null && ObjArray[i, j].property = search_value)
{
return ObjArray[i, j]; // you found it, now remember it
}
}
}
return null;
}
方法可以做到这一点。 在您的示例中,我会这样做:
Obj O = null;
for (int j = 0; j < height && O == null; j++)
{
for (int i = 0; i < width; i++)
{
if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value)
{
O = ObjArray[i, j]; // you found it, now remember it
break;
}
}
}
在这种情况下,KeepLooping 变量是不必要的,因此简单的 break 语句非常优雅地工作。
您实际上甚至可以进一步简化它:
Obj O = null;
for (int j = 0; j < height && O == null; j++)
{
for (int i = 0; i < width && O == null; i++)
{
if (ObjArray[i, j] != null && ObjArray[i, j].property == search_value)
O = ObjArray[i, j]; // you found it, now remember it
}
}
现在,甚至不需要休息。
当然,这可能不适用于所有情况,但通常您可以将结果类型用作状态值。
仅供参考,如果您操作正确,在 foreach 中修改集合的问题并不是真正的问题。 例如:
// Assumes ObjArray is [,] and not [][]
foreach (int item in ObjArray.ToList()) // makes a copy to iterate over
{
if (item != null && item.property == search_value) {
ObjArray[0,0] = item; // I can now modify this for no reason
break;
}
}