【问题标题】:How to use an index variable in a recursion?如何在递归中使用索引变量?
【发布时间】:2012-05-20 23:30:28
【问题描述】:

我想在递归中使用索引变量,而不是在调用函数时将其作为参数发送。但是,如果我在开始时重置它(例如 i = 0),它会在每次运行时重置。我想将它用作计数器(计算函数运行次数)。

【问题讨论】:

  • 只要您承诺在调用顶级函数时重置它,您就可以将您的计数器实现为静态变量。见javatutorialhub.com/java-static-variable-methods.html
  • 我想计算函数调用自身的次数,以便知道何时停止并返回值。我通常将函数 '0' 作为 int 参数发送,并且每次只发送 ++,但我想找到一种更优雅的替代方法。
  • 那么索引就是停止条件?那么我认为你应该将它作为参数传递,然后如果它大于零,则在方法内部减一,然后再次传递它,直到函数接收到的值为零。这就是你现在正在做的事情吗?
  • 哦,你的反而增加了。一样的。我不知道更优雅的方式。也许其他人会这样做。
  • 如果您使用索引来访问列表,也许您可​​以通过少一个元素的列表,直到它为空。

标签: java variables recursion indexing


【解决方案1】:

首先,您显然只想初始化一次。递归中的一个常见模式是:

public void run(int opt) {
  run_helper(opt, 0);
}

private void run(int opt, int depth) {
  if (whatever) { run(opt, depth + 1); }
}

外部方法只做一些初始化。

您会经常看到建议的“解决方案”(例如,在对您问题的第一条评论中)将使用静态变量。这种方法是一种糟糕的风格,一旦添加多线程(例如,通过制作 UI 版本,或在多线程 Web 服务器中运行它),将导致您的程序以各种奇怪的方式失败。最糟糕的是,它可能一开始似乎可以工作,并且只有在有很多用户时才开始巧妙地行为不端。 因此,除了常量之外的所有内容都远离“静态”!

对于“静态”,它通常看起来像这样:

static int counter;

public void start() {
  counter = 0;
  recurse();
}

public void recurse() {
  counter += 1;
  if (whatever) { recurse(); }
}

现在假设两个用户同时调用start他们将覆盖彼此的计数器!因为静态意味着它在线程和用户之间共享。

这是一个非常容易理解的解决方案

class MyTask {
  int counter = 0;

  public void recurse() {
    counter++;
    if (whatever) { recurse(); }
  }

  public int getIterations() {
    return counter;
  }
}

public void run() {
  MyTask task = new MyTask();
  task.run();
  System.out.println("Task took "+task.getIterations()+" itertions.");
}

然后您创建一个任务,运行它,并在最后检索计数器。干净、简单、高效、可靠。 如果你有多个线程/用户,每个都有一个单独的 MyTask 对象,你不会遇到任何问题。

另外,您可以添加额外的统计信息,它们都干净地包装在任务对象中。 “每次迭代的平均时间”? “平均递归深度”?没问题。任务对象也可以用来存储你的结果。

这里建议使用ThreadLocal。我不同意这一点。它根本没有提供任务对象的任何好处。只需尝试使用ThreadLocal 和任务对象来实现它,您就会看到不同之处。另外,ThreadLocal 在经验上比访问堆值慢 10 倍(参见https://stackoverflow.com/a/4756605/1060350)。对于int,情况可能更糟。所以永远不要在性能关键的代码路径中调用ThreadLocal#get。如果您打算使用ThreadLocal,请在此代码路径之外使用它,并使用局部变量(或任务对象)将“局部静态”变量提供给您的关键代码路径。

【讨论】:

  • 你为什么首先建议有一个静态变量?
  • 因为这是人们想出的常见解决方案。它需要讨论,并且需要指出缺点。特别是,由于 Eclipse 喜欢建议将变量设为静态以“修复”访问错误。
  • 我不明白为什么有人会建议将这种类型的变量设为静态。
  • 我只是看不到在类的所有实例之间共享变量的好处。
  • 它一定会坏掉的。这是众所周知的。在 stackoverflow 上搜索“+static +java”,寻找一个关于静态滥用的恐怖柜。但除非你告诉他们为什么不使用 static,否则他们会使用它,因为它似乎是解决问题的最简单方法。
【解决方案2】:

您应该使用两种方法将其分开:一种是公共的,用于启动递归迭代并将计数器初始化为零,另一种是私有的,即进行递归调用的地方。这样每次调用公共方法时,计数器都会被初始化。它会是这样的(在java中):

public class Recursion{
    private int iterations=0;

    private int calcFactorial(int n){
        iterations++;
        if (n==2)
            return 2;
        else
            return n * calcFactorial(n-1);
    }

    public int factorial(int n){
        //initialize the counter
        iterations = 0;
        return calcFactorial(n);
    }

    public int getIterations(){
        return iterations;
    }
}

【讨论】:

    【解决方案3】:

    考虑到索引被用作停止条件,我会采用简单的基于参数的方法。 JavaScript 实现:

    var recurse = function() {
        recurseHelper(0);
    };
    
    var recurseHelper = function(iteration) {
        // run for 100 iterations, 
        if (iterationCount > 100)
            return;
    
        // do stuff...
    
    
        recurseHelper(iteration + 1);
    }
    

    但是,如果您只想调用某个函数一定次数,为什么要特别使用递归呢?你可以只使用一个循环。再次在 JavaScript 中:

    for (var i = 0; i < 100; i++) {
        // do stuff...
    }
    

    编译器使用展开循环比使用递归构造更有趣,具体取决于递归算法的复杂性。

    【讨论】:

    • 他可能没有线性递归。有很多这样的非线性问题。例如,在 XML 树中,找到深度为 3 或更多的前 10 个节点(按文档顺序)。
    【解决方案4】:

    您可以使用属性,但为什么呢?为什么不将值作为参数传递?

    public class Empty {
    
        private int steps = 0;  
    
        public static void main (String args [])
        {
            Empty e = new Empty ();
            System.out.println (e.reverse ("This is a test"));
            System.out.println ("steps: " + e.steps);
        }
    
        public String reverse (String s) {
            ++steps;
            if (s.length () < 2) 
                return s;
            else return reverse (s.substring (1)) + s.charAt (0);
        }
    }
    

    从我得到的 cmets 中,您将其用作计数器,以检测何时结束递归。这看起来不是很有用。难道你没有一个可以从参数派生的条件,比如 list.length () 之类的吗?

    当然,调用该方法两次会进一步增加计数器。如果两个线程不同时使用它,您可以重置它,并且内部对象中的包装方法可能有助于防止在不先重置计数器的情况下调用它。

    但这比在柜台周围传递更多样板。

    参数是计算调用次数的更简洁的解决方案。

    如果您想在调试时防止 stackoverflows,一个古玩的替代方法是调用随机化器,并在 10 000 个案例中返回一个。

    【讨论】:

      【解决方案5】:

      最自然的做法是使用您在帖子中描述的辅助参数,特别是如果它用于确定何时停止递归。如果依赖于成员变量,您可以有一个帮助方法,在调用递归方法之前重置计数器,并在您想要调用递归函数时调用帮助方法。

      如果您更喜欢使用单一方法,您必须执行以下操作:

      创建一个名为insideMethod 的成员变量,默认设置为false。调用该方法时,它会检查此变量。如果它是假的,它是第一次调用,计数器应该被重置,否则计数器应该只是递增。之后,insideMethod 设置为 true。返回时,应将insideMethod 设置回 false,前提是 this 调用将其设置为 true。

      记得将insideMethod 和索引变量设为ThreadLocal。

      伪代码:

      ThreadLocal<Boolean> insideMethod = false
      ThreadLocal<Integer> index = 0
      
      ....
      
      void recMethod(args) {
          boolean topCall = (insideMethod == false)
          insideMethod = true
      
          if (topCall)
              index = 0
          else
              index++
      
          // Body of the method...
      
          if (topCall)
              insideMethod = false
      }
      

      【讨论】:

      • ThreadLocal&lt;Integer&gt; 的性能会很差,因为它需要创建大量的Integer 对象,从而给垃圾收集带来了相当大的负担。
      • 我闻到过早的优化。
      • 有一个更简单的解决方案,请参阅我的回复。将其包装成一个任务对象。在 Java 中频繁更改包装的本机类型时,我的经历非常糟糕。它在太多的情况下扼杀了性能,让我尽可能地远离它。至少,将其更改为不可变的 Counter 对象。事实上,我怀疑你的代码是否真的可以编译。 index++ 不应该在 ThreadLocal&lt;Integer&gt; 对象上工作...
      • 我说的是伪代码。在不知道该方法的其余部分是如何实现的情况下,说 ThreadLocals 将表现不佳是完全可笑。创建一个单独的任务对象是相当不错的,但有点违反直觉。常规方法调用可能看起来更简洁。
      • 如果您从简化的伪代码转为 真实 代码,很容易看出 ThreadLocal 使用起来并不比仅仅创建一个任务对象更“直观” .事实上,它非常混乱。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-03-13
      • 2011-06-16
      • 1970-01-01
      • 1970-01-01
      • 2019-01-04
      • 2014-11-13
      • 2018-03-17
      相关资源
      最近更新 更多