【问题标题】:How to convert recursion to iteration? [closed]如何将递归转换为迭代? [关闭]
【发布时间】:2017-02-22 22:21:58
【问题描述】:

这个问题被问过几次,但我仍然发现将易于阅读和直观的代码转换为迭代代码非常困难。例如,我正在练习一个编码问题,我得到了 26 个整数,表示每个字符在字符串中出现的次数。我应该打印所有可能的字符串。以下是我的递归代码

private static void combinatorial(String prefix, ArrayList<Integer> remainingToFill,
        int totalLength) {
    if (prefix.length() == totalLength) {
        System.out.println(prefix);
    }
    for (int i = 0; i < remainingToFill.size(); i++) {
        if (remainingToFill.get(i) > 0) {
            ArrayList<Integer> toFill = new ArrayList<>(remainingToFill);
            toFill.set(i, toFill.get(i) - 1);
            combinatorial(prefix + (char) ('a' + i), toFill, totalLength);
        }
    }
}

我编写了它的迭代版本,但生成的函数要复杂得多且不可读,而且我需要更多时间来编写它。我该如何解决这类问题?有没有我可以遵循的简单技术可以生成简单易读的代码?

【问题讨论】:

  • 我不怀疑存在一个通用模式。 (不过,如果我弄错了,我很想了解它。)任何给定的操作都会以不同的方式执行。有些作为递归实现“更简单”,有些作为迭代实现“更简单”,有些则完全不同。选择的实现很大程度上取决于所用语言的功能和复杂性。如果使用给定模式实现给定操作时“丑陋”,那么很可能使用了错误的模式。我不希望在两者之间进行有意义的翻译。
  • IMO 递归基于 call stack,因此您必须在 iterative pattern 中模拟该堆栈才能达到预期结果,但它可能不是最佳解决方案。
  • 好吧,我认为您的代码不是完美的递归。它是迭代和递归的混合体。这就是为什么首先转换可能很复杂的原因。

标签: java recursion iteration


【解决方案1】:

嗯,编程语言支持程序的递归表达的原因是它通常比显式迭代形式更简单。所以你的问题几乎是自我回答的。

但是,确实有一种方法可以将递归形式转换为始终有效的迭代形式。拥有goto 的语言会有所帮助,而Java 没有。

首先让我们清理一下java。我们希望使用最少数量的参数和局部变量,因为剩下的必须放在我们的显式堆栈上。开始了。除了 iprefix 之外,我们将删除所有内容。

class CombinationLister {
  private final int[] counts;
  private final int length;

  CombinationLister(int[] counts) {
    this.counts = counts.clone();
    this.length = Arrays.stream(counts).sum();
  }

  private void list(String prefix) {
    if (prefix.length() == length) {
      System.out.println(prefix);
    }
    for (int i = 0; i < counts.length; i++) {
      if (counts[i] > 0) {
        --counts[i];
        list(prefix + (char) ('a' + i));
        ++counts[i];
      }
    }
  }

  void run() {
    list("");
  }
}

现在让我们转写成 C,它有 goto。在这里,通过添加全局字符串缓冲区,甚至可以轻松消除 prefix

#include <stdio.h>

char str[100];
int counts[] = { 1, 2, 3 };
int n_counts = 3;
int total_count = 6;
int len = 0;

void list(void) {
  if (len == total_count) printf("%.*s\n", total_count, str);
  for (int i = 0; i < n_counts; i++) {
    if (counts[i] > 0) {
      str[len] = 'a' + i;
      --counts[i];
      ++len;
      list();
      --len;
      ++counts[i];
    }
  }
}

现在,规则是:

  • 为每个局部变量和参数构建一个包含一个字段的记录堆栈。这里我们只剩下i,所以我们甚至不需要记录。一堆ints 就可以了。
  • 将递归调用站点替换为
    • push 放到堆栈上,然后
    • 将参数重置为新值(这里我们没有),然后
    • 跳转到函数的开头,然后
    • 跳转后立即贴上标签rtn:
  • 在函数的最后,添加一个检查堆栈是否为空的结语。如果没有,它会弹出堆栈并跳转到rtn:

这些规则本质上是模仿编译器为处理递归调用而生成的代码。

综上所述,我们有:

int stk[100];
int p = 0; // stack pointer

void list(void) {
  int i;
 start:
  if (len == total_count) printf("%.*s\n", total_count, str);
  for (i = 0; i < n_counts; i++) {
    if (counts[i] > 0) {
      str[len] = 'a' + i;
      --counts[i];
      ++len;
      stk[p++] = i;  // push i on stack
      goto start;
     rtn:
      --len;
      ++counts[i];
    }
  }
  // epilog
  if (p > 0) {
    i = stk[--p];  // restore i from stack
    goto rtn;
  }
}

如果您仔细按照步骤操作,您的代码每次都会先运行一次尝试。唯一的附加提示是,当有多个递归调用站点时,您需要为每个 rtn1:rtn2: 等提供一个返回标签,并在堆栈中额外添加一个 int 字段来表示返回站点, 在结语中使用 switch 语句跳转到正确的语句。

当然,这不是漂亮的代码。我们想摆脱gotos。事实证明,通过“代数”将gotos 转换为循环,这始终是可能的。有几十种转换技术……在这里无法描述。这很像在数学课上简化方程。有时需要添加布尔标志。但是,在这种情况下,我们不需要。我完成了这个:

void list(void) {
  for (int i = 0;;) {
    while (i < n_counts && counts[i] == 0) i++;
    if (i < n_counts) {
      --counts[i];
      str[len] = 'a' + i;
      stk[p++] = i;
      if (++len == total_count) printf("%.*s\n", total_count, str);
      i = 0;
    } else if (p > 0) {
      i = stk[--p];
      --len;
      ++counts[i++];
    }
    else break;
  }
}

只是为了好玩,回到 Java:

class CombinationLister {
  private final int[] counts;
  private final char[] str;
  private final int[] stk;
  private int p = 0;
  private int len = 0;

  CombinationLister(int[] counts) {
    this.counts = counts.clone();
    this.str = new char[Arrays.stream(counts).sum()];
    this.stk = new int[str.length];
  }

  void run() {
    for (int i = 0;;) {
      while (i < counts.length && counts[i] == 0) i++;
      if (i < counts.length) {
        --counts[i];
        str[len] = (char) ('a' + i);
        stk[p++] = i;
        if (++len == str.length) System.out.println(str);
        i = 0;
      } else if (p > 0) {
        i = stk[--p];
        --len;
        ++counts[i++];
      } else break;
    }
  }
}

【讨论】:

    【解决方案2】:
    public static void combinatorial(ArrayList<Integer> remainingToFill, int totalLength) {
        Stack st = new Stack();
        st.push( new Pair<String,Integer>("", 0) );
        while( !st.empty() ){
            Pair<String,Integer> top = (Pair<String,Integer>) st.pop();
            String prefix = top.getKey();
            Integer i = top.getValue();
            if (prefix.length() == totalLength) {
                System.out.println(prefix);
                int index= prefix.charAt(prefix.length()-1 )-'a' ;
                remainingToFill.set( index , remainingToFill.get(index) + 1 );
            }
            else{
                if( i== remainingToFill.size() ){
                    if( prefix.length()>0 ){
                        int index= prefix.charAt(prefix.length()-1 )-'a' ;
                        remainingToFill.set( index , remainingToFill.get(index) + 1 );
                    }
                }
                else{
                    st.push( new Pair<String,Integer>(prefix, i+1) );
                    if( remainingToFill.get(i) > 0 ){
                        remainingToFill.set(i, remainingToFill.get(i) - 1);
                        st.push( new Pair<String,Integer>(prefix+(char)('a'+i), 0) );
                    }
                }
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-11-30
      • 1970-01-01
      • 1970-01-01
      • 2020-10-27
      • 2016-01-26
      • 2017-03-05
      相关资源
      最近更新 更多