有没有一种“聪明”的方式来打破嵌套循环

本文关键字:方式 嵌套循环 聪明 一种 有没有 | 更新日期: 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;
        }
    }
}

不再需要那个讨厌的额外布尔值!

然而,寻找替代或更好的解决方案仍在继续。多年来,我尝试了许多其他方法,但由于某种原因发现它们不是那么好:

  1. j(外循环迭代器(设置为高于 height 的值,这将触发它自动中断。不理想,因为有时您想记住找到它的ij值。
  2. 在 2D 阵列上使用foreach。不理想,因为foreach不会让你操作集合(删除或添加到它,这通常是我搜索对象的原因(。
  3. 只需将 2 个循环放在一个函数中,该函数除了查找并返回O之外什么都不做。 return有效地打破了这两个循环。很多时候这是可以的,但并非总是如此。我这样做是为了非常通用的搜索,但也有相当多的"组搜索">,我想将遍历集体化。在这些情况下,我找到 2 个或更多对象(有时在同一个 2D 数组中(,记住它们,然后才打破两个循环。
  4. 使用goto?(哇,这可能是goto的唯一合法用途吗?令人惊讶的是,它比 KeepLooping 标志更具可读性,特别是如果我们有 3 个或更多循环。不理想,因为同事会尖叫血腥的谋杀。而在 C# 中,goto后会有正确的垃圾清理吗?
  5. 引发自定义异常? idk,我从未尝试过,但它看起来不如我目前的首选方式可读。
  6. 找到正确的对象后,在内部循环中执行所有对象操作,然后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# 允许你命名循环,例如 L1L2,然后执行类似 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;
    }
}