【问题标题】:Make nested for loop algorithm - dynamic使嵌套 for 循环算法 - 动态
【发布时间】:2011-03-26 15:00:45
【问题描述】:

我有一个类似这样的算法:

对于 m = 1 到 2 初始化(工作项(m)) 对于 l = 1 到 2 初始化(工作项(l)) 对于 k = 1 到 2 初始化(工作项(k)) 对于 j = 1 到 2 初始化(工作项(j)) 对于 i = 1 到 2 初始化(work_item(i)) doSomething(work_item(i)) 下一个 做某事(工作项目(j)) 下一个 做某事(工作项目(k)) 下一个 做某事(工作项目(l)) 下一个 doSomething(work_item(m)) 下一个

如何迭代地编写它,使其动态化,这样我就不会将自己限制在固定数量的 for 循环 (i, j, k, l, m) 中(即我可以执行 (i) 或 ( i, j) or (i, j, k) or (i, j, k, l) etc...)?

(我严格要求用动态的、迭代的解决方案来回答。如果你不明白这一点,请继续阅读,从上一句开始。)

【问题讨论】:

  • Erm...从外观上看,您要么在设置变量之前使用它们,要么将它们设置为您永远不会使用的值,具体取决于它们是否通过引用传递或按价值。我知道它们在第一次迭代时应该是 0,但是该死的,这太丑了。
  • @roygbiv:没有上下文表明迭代版本比递归版本更简单或更不容易出错。没有。时期。您可能从中获得多少速度(如果有的话)并不重要 - doSomethinginitialize 占用的时间将使实际花费在迭代和/或递归。所以,是的,你会选择一种不必要的复杂、难以遵循的方法,而不是一种简单、清晰、简洁的方法,而几乎没有收获。上下文无关紧要——没有可能实际发生的上下文会产生不同的结果。

标签: .net algorithm dynamic loops iteration


【解决方案1】:

完全按照使用递归的方式编写算法,但使用显式堆栈对象而不是递归。即:

var stack = new Stack();
stack.Push(InitialThingy);
while(stack.Count != 0)
{
    var currentItem = stack.Pop();
    //Do things to current item and add things to stack as we go.
}

【讨论】:

  • 就像我在你投反对票的问题中所说的那样,由于问题的性质,递归解决方案会更好。为什么不想使用递归?
  • @roygbiv 您在原始代码中所做的 sn-p 与使用堆栈同构。递归也是如此。您不会找到使用堆栈的迭代解决方案。我支持@Nathan:为什么使用递归?
  • @roygbiv:没有任何逻辑上的理由,你愿意分享,你断然否定了为这样的任务制造的概念。直到你能解释清楚,是的——你害怕它。
  • 就此而言,“迭代、动态解决方案”听起来像是使用显式堆栈来描述的好方法。
  • @roygbiv:关于态度不好,你的评论表明你态度不好,拒绝回应@cHao的完全正确的观点。
【解决方案2】:
private sub doStuff(depth as integer)
    for i as integer = 1 to 2
        initialize(work_item(i))
        if depth > 1 then doStuff(depth - 1)
        doSomething(work_item(i))
    next
end sub

比您想出的任何迭代解决方案都更优雅、更短、更简单。你想给我投反对票,但这并不会降低它的真实性。

【讨论】:

  • 同意。不过,如果它不是用 VB 编写的,它会更加优雅。 ;)
  • 嘘。原始代码在 VB 中。只是想提供帮助。 :) 此外,我有点喜欢 VB。如今,它与 C# 的最大区别在于它更冗长(我喜欢;end if : end while : end sub}}} 更容易检查正确性)。哦,当您通过引用传递它们时,它的属性可以满足您的期望。
  • @roygbiv:编译的伪代码不再是伪代码——它是代码。您的代码不仅仅类似于 VB——它编译为 VB。考虑到使用的关键字在 BASIC 之外不太常见,因此可以合理地预期它是什么。
【解决方案3】:

你可能想要recursion

【讨论】:

  • 你已经有了一个迭代解决方案,因此@Jonathan 建议递归。
  • 如果有特定原因导致递归对您不起作用,请说明。这正是递归最能解决的问题。任何名义上的迭代解决方案仍然是递归的;它最终会用一个堆栈结构(例如 C++ vector)代替调用堆栈。
【解决方案4】:

我会赞成 deinst 的解决方案,但我是新手,没有 15 个代表......在跟踪每个算法之后,我相信 deinst 具有正确的动态迭代方法。然而,deinst 在开始时一次性初始化所有,而 roygbiv 在循环嵌套中重复初始化。 (我想我之前看过正确的 deinst 编辑。)

【讨论】:

  • 我相信现在是正确的。初始化在每一步完成。循环向后看,但那是因为我是在喝了几杯啤酒之后写的。做while循环会更好。我确信计数器算法是我们想要的。
  • 你现在应该有 15 个代表了。 :)
【解决方案5】:

您需要跟踪depth 变量,其中depth 是嵌套的总深度。您可以为此使用数组或堆栈,也可以使用递归的强大功能,一次只分配一个局部变量

function doEverything(depth) {
  if (depth == 0) return;

  var i; // a new local variable on each call
  for i = 1 to 2 {
    initialize(work_item(i));
    doEverything(depth-1);
    doSomething(work_item(i));
  }
}

由于i,j,k,... 的范围从 1 到 2,因此也可以将它们解释为单个整数变量的位。

请注意,您的嵌套循环总共执行 2^depth 操作。

【讨论】:

  • 这将在每次迭代时初始化最里面的项目,这与给定的代码不同。初始化()需要在循环之外。是的,它没有意义——i 还没有使用,所以在循环之外使用它有点奇怪——但这就是你需要做的与原来相同的事情。这和一些内部计数器的最新值的记忆。
  • 我冒昧地使用“正确”的顺序进行初始化。 ;-) OP 的代码一次只对doSomething 使用一个计数器,因此无需显式存储它们。外部计数器(ml、..)隐含在调用堆栈中,因为 var i 在每次调用时都是一个新变量。
  • 在问题的原始代码中,变量在各自的循环之外使用。那时的变量将是 0(在第一次通过相应循环之前)或 3(之后)。复制该功能需要记住计数器的值。但是,您的代码不需要这样做,因为在变量具有已知值之前不会使用它们。
【解决方案6】:

调用变量 i[0] ... i[n] 将它们全部初始化为 1。要在每一步递增

int j, k;
for(j = n - 1; j >= 0; j--){
  initialize(i[j]);
}

while (true) {
  j = 0;
  while (i[j] == 2) {
    doSomething(i[j]);
    j++;
  }
  if (j < n) {
    doSomething(i[j]);
  } else {
    break;
  }
  for (j = 0; j < n; j++) {
     if (i[j] == 1) {
       i[j] = 2;
       break;
     }
     else {
       i[j] = 1;
     }
  }
  if (j == n) break;
  for (k = j; k >= 0; k--) {
    initialize(i[k]);
  }
}

本质上,您正在实现一个二进制计数器,清除所有设置位小于第一个清除位,然后设置第一个清除位。这将以与给定循环相同的顺序运行执行操作。

你可以用不同的范围做类似的事情,每个 i[j] 的范围甚至不必相同。

编辑添加了初始化。

注意 递归在这里可能有点过头了,并且比平面实现的灵活性稍差(考虑从内部循环中止。)。这是一个在实践中经常出现的问题,并不是那么复杂。我们想要做的是让每个循环看起来像

for i = 1 to 2
   do beginning stuff with i
   do inner loop
   do ending stuff with i
next

如果我们只考虑索引变量,我们就会得到看起来像二进制计数器的东西。如果我们在执行最内层循环时查看索引变量的值。

 i j k l
 1 1 1 1
 1 1 1 2
 1 1 2 1

实现一个计数器很容易。如果我们只是标记我们的变量,首先是最里面的,这样i -&gt; i[3]j -&gt; i[2]k -&gt; i[1]l -&gt; [0] 那么单个递增步骤包括

j = 0
while i[j] == 2
  i[j] = 1
  j++
if j == maxindex
  break out, we're done with all the loops
else
  i[j] = 2

现在,让我们在循环结束时做一些事情。这很容易,循环结束发生在我们递增之前。所以我们的增量器看起来像

j = 0
while i[j] == 2
  do ending stuff with i[j]
  i[j] = 1
  j++
if j == maxindex
  break out, we're done with all the loops
else
  do ending stuff with i[j]
  i[j] = 2

现在,棘手的部分。乍一看,我们只是在递增之后才开始做一些事情,但这有两个问题。

  1. 它错过了初始增量(当我们将初始变量设置为 1 时发生的增量
  2. 它为最终增量中的所有内容调用初始增量例程。

(这些本质上是同一个问题)

第一个的解决方案很简单,我们只是在主循环之外调用开始的东西。

for j = maxindex - 1 downto 0
  do beginning stuff with i[j]
while true
  j = 0
  while i[j] == 2
    do ending stuff with i[j]
    i[j] = 1
    j++
  if j == maxindex
    break out, we're done with all the loops
  else
    do ending stuff with i[j]
    i[j] = 2

现在要在增加计数后获取其他初始化程序。

for j = maxindex - 1 downto 0
  do beginning stuff with i[j]
while true
  j = 0
  while i[j] == 2
    do ending stuff with i[j]
    i[j] = 1
    j++
  if j == maxindex
    break out, we're done with all the loops
  else
    do ending stuff with i[j]
    i[j] = 2
  for k = j downto 0
    do beginning stuff with i[k]

与嵌套循环版本相比,这不会产生任何开销(调用堆栈等)。它可以很容易地从巢穴深处中止。这有点棘手,但如果它比具有显式堆栈的递归版本更复杂,我会感到惊讶。 De gustibus non disputandum est.

【讨论】:

  • @roygbiv 谢谢。但是,我会说,您应该在问题的陈述中更清楚地表达您对递归的厌恶。对于这个特定的问题,我不认为递归是一个特别好的解决方案,但我显然是少数。
  • 这确实是一个非常好的帖子,就其本身而言。不幸的是,它是对一种方法的描述,如此复杂,需要两屏半的文字来解释它。我反对任何人在他们的脑海中从头开始重新实现代码,即使在向他们解释之后也是如此。
  • @cHao 喝了几杯啤酒后,我在编辑前管理了这篇文章(我承认,我尝试了几次)。描述很长,但想法很简单,只需制作一个计数器并随着计数器的增加而工作。如果代码比具有显式堆栈的递归版本更复杂,我会感到非常惊讶(使用显式堆栈编写代码说起来容易做起来难)。我怀疑可以通过以连续传递形式编写递归版本并简化来获得计数器版本,但我怀疑描述会更简单。
  • @deinst:使用显式堆栈...是的。不会好很多。我打算做一个迭代版本,实际上很像你的……然后整个等待-何时做-我-做-这一步?部分。递归版本几乎立即出现在我面前——对于这种事情 IMO 来说,递归更加自然,特别是当它是真正的递归而不是嘿-我-循环-和-推动-和-手动-所以时-这不是递归!方法。
  • @deinst:此解决方案使用显式堆栈 - 但您在开始时为其设置了固定大小,并且在每个级别仅存储一个变量。这比它需要的更复杂。
【解决方案7】:
void doStuff(int depth)
{
    int i[depth];
    int sp = depth - 1;
    i[sp] = 0;

    while (1)
    {
        if (++i[sp] <= 2)              // "next".  This is why we init to 0.
        {                              // At this point, i[sp] is 1 or 2.
            // init item i[sp];

            if (sp)                    // If there's space to nest another loop
            {
                i[--sp] = 0;           // Push a 0
                continue;              // Start iterating with the new 0 as TOS
            }
        } 
        else if (++sp >= depth)        // Here, i[sp] is 3.  Leave the inner loop
            break;                     // and exit if stack's now empty

        // use item i[sp];
    }
}

仍然比递归更棘手。除非万不得已,否则不要使用它。

【讨论】:

    【解决方案8】:

    您可以构造一个序列集合,其中每个序列都是循环变量 i、j、k、...的排列。

    一旦你有了这个集合,你就可以迭代:

    foreach (var seq in myCollection)
    {
      initialize(seq[0]);
      initialize(seq[1]);
      initialize(seq[2]);
      ...
      doSomething(...);
    }
    

    一种通过 LINQ 生成序列集合的方法,由 Eric Lippert 提供:

    static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences) 
    { 
      IEnumerable<IEnumerable<T>> emptyProduct = new[] { Enumerable.Empty<T>() }; 
      return sequences.Aggregate( 
        emptyProduct, 
        (accumulator, sequence) =>  
          from accseq in accumulator  
          from item in sequence  
          select accseq.Concat(new[] {item}));                
    }
    

    您传递任意长度的序列 {{1,2},{1,2},...{1,2}},其中每个内部 {1,2} 序列替换您的循环变量之一,并且你会得到所有值排列的序列。

    【讨论】:

      【解决方案9】:

      您的问题归结为变量n 集合数的排列问题的解决方案。这可以通过递归来解决(Python):

      a = ('ab', 'AB', '12')
      
      def permutate(a, s='', nLevel=0):
          aResult = []
          if nLevel < len(a):
              for n in range(0, len(a[nLevel])):
                  aResult += permutate(a, s + a[nLevel][n], nLevel + 1)
          else:
             aResult = [s]
      
          return aResult
      
      print(permutate(a))
      

      生产:

      ['aA1', 'aA2', 'aB1', 'aB2', 'bA1', 'bA2', 'bB1', 'bB2']
      

      【讨论】:

        猜你喜欢
        • 2016-04-22
        • 1970-01-01
        • 2012-12-01
        • 2018-07-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-11
        • 2012-03-09
        相关资源
        最近更新 更多