`
mingliangfeng
  • 浏览: 45690 次
  • 性别: Icon_minigender_1
  • 来自: 墨尔本
社区版块
存档分类
最新评论

递归计算向非递归计算转换模板 -- 续

阅读更多

上一篇文章对递归向非递归转换的原理和过程作了介绍,本篇谈谈具体的代码实现。还是考虑上一篇文章中的递归例子:f(x) = f(x-1) + f(x-3), f(x) = 10 (x < 3)。用上文分析出来的规律,其实现如下:

  public static double nonRecursion(double x) {
    double initValue = x;
    final int endFlag = 3; // count of branches plus 1   
    Map<Double, Double> values = new HashMap<Double, Double>();
    StackItem ci = new StackItem(initValue);
    while (ci != null) {
      switch (ci.flag) {
      case 0:
        x = ci.number;
        if (x < 3) { // exit of recursion   
          values.put(x, 10.0);
          ci.flag = endFlag;
        } else {
          ci.flag = ci.flag + 1;
          StackItem sub;
          if (ci.next == null) {
            sub = new StackItem();
            sub.pre = ci;
            ci.next = sub;
          } else {
            sub = ci.next;
          }
          sub.number = x - 1; // branch one   
          if (values.containsKey(sub.number)) {
            sub.flag = endFlag;
          } else {
            sub.flag = 0;
          }
          ci = sub;
        }
        break;
      case 1:
        x = ci.number;
        ci.flag = ci.flag + 1;
        StackItem sub;
        if (ci.next.next == null) {
          sub = new StackItem();
          sub.pre = ci.next;
          ci.next.next = sub;
        } else {
          sub = ci.next.next;
        }
        sub.number = x - 3; // branch two   
        if (values.containsKey(sub.number)) {
          sub.flag = endFlag;
        } else {
          sub.flag = 0;
        }
        ci = sub;
        break;
      case 2: // two sub items are ready, calculating using sub items   
        x = ci.number;
        double f1 = values.get(ci.next.number);
        double f2 = values.get(ci.next.next.number);
        double result = f1 + f2; // recursive body
        values.put(x, result);
        ci.flag = endFlag;
        break;
      case endFlag: // current item is ready, back to previous item           
        ci = ci.pre;
        break;
      }
    }
    return values.get(initValue);
  }

 

其中本地的Map类型的变量values用来存放递归因子和它对应的计算出来的值。堆栈是用双向链表来实现的,双向链表非常方便向前回溯和向后取得父节点的子节点。双向链表的item类型为StackItem,它除了拥有向前和向后的指针外,还包含当前递归因子的值和状态。其结构如下:

public class StackItem {
  
  public double number;
  
  public int flag;
  
  public StackItem pre = null;
  
  public StackItem next = null;
  
  public StackItem() {
    this(0);
  }
  
  public StackItem(double number) {
    this.number = number;
    this.flag = 0;
  }
  
}

 

在本机上运行了一下,由于转换后的非递归程序保存了中间递归因子的计算值,它比直接用java语言实现递归在时间上要快不少。递归参数越大时,其优势越明显。

 

上一篇文章提到要把分析出来的规律总结成一个通用模板,其实从上面的实现代码中可以看出,模板的框架已经浮现出来了。根据递归函数中的递归调用点,可以确定计算完成时的状态值,进而确定循环体中case的个数。其中每个case里面的代码块都具备可以模板化的特点。总体来说,将一个递归函数分解成如下几块,安插至case框架里面即可:

  1. 递归调用出口(exit of recursion):在当前节点状态为0,即初次遍历至该节点时,会检查递归因子的值。如果能直接计算出来,则计算出来并将状态置为完成;否则,状态递增1,开始计算第一个子节点的值;
  2. 递归调用点(branch),包括递归因子的收敛公式,根据其在递归体中出现的顺序安插至case代码体中去;
  3. 递归体(recursive body),当节点的所有子节点都完成计算后,根据递归体计算当前节点的值。代码的位置位于倒数第二个case中。

根据上面的分解,递归至非递归的转换模板就已经很清晰了,可以轻易的用模板技术,比如velocity或freemarker来实现。

 

下面再用上面的规律对一个递归函数进行非递归转换,以验证模板的通用性:

递归函数:f(x) = ( f(x-1) + f(x-3) + x) / f(x-2), f(x) = 10 (x < 3)

附上的代码是对以上函数的递归和非递归的java实现:

  public static double recursion(double x) {
    if (x < 3)
      return 10.0;

    double f1 = recursion(x - 1);
    double f2 = recursion(x - 3);
    double f3 = recursion(x - 2);
    return (f1 + f2 + x) / f3;
  }

  public static double nonRecursion(double x) {
    double initValue = x;
    final int endFlag = 4; // count of branches plus 1
    Map<Double, Double> values = new HashMap<Double, Double>();
    StackItem ci = new StackItem(initValue);
    while (ci != null) {
      switch (ci.flag) {
      case 0:
        x = ci.number;
        if (x < 3) { // exit of recursion
          values.put(x, 10.0);
          ci.flag = endFlag;
        } else {
          ci.flag = ci.flag + 1;
          StackItem sub;
          if (ci.next == null) {
            sub = new StackItem();
            sub.pre = ci;
            ci.next = sub;
          } else {
            sub = ci.next;
          }
          sub.number = x - 1; // branch one
          if (values.containsKey(sub.number)) {
            sub.flag = endFlag;
          } else {
            sub.flag = 0;
          }
          ci = sub;
        }
        break;
      case 1:
        x = ci.number;
        ci.flag = ci.flag + 1;
        StackItem sub1;
        if (ci.next.next == null) {
          sub1 = new StackItem();
          sub1.pre = ci.next;
          ci.next.next = sub1;
        } else {
          sub1 = ci.next.next;
        }
        sub1.number = x - 3; // branch two
        if (values.containsKey(sub1.number)) {
          sub1.flag = endFlag;
        } else {
          sub1.flag = 0;
        }
        ci = sub1;
        break;
      case 2:
        x = ci.number;
        ci.flag = ci.flag + 1;
        StackItem sub2;
        if (ci.next.next.next == null) {
          sub2 = new StackItem();
          sub2.pre = ci.next.next;
          ci.next.next.next = sub2;
        } else {
          sub2 = ci.next.next.next;
        }
        sub2.number = x - 2; // branch three
        if (values.containsKey(sub2.number)) {
          sub2.flag = endFlag;
        } else {
          sub2.flag = 0;
        }
        ci = sub2;
        break;
      case 3: // three sub items are ready, calculating using sub items
        x = ci.number;
        double f1 = values.get(ci.next.number);
        double f2 = values.get(ci.next.next.number);
        double f3 = values.get(ci.next.next.next.number);
        values.put(x, (f1 + f2 + x) / f3); // recursive body
        ci.flag = endFlag;
        break;
      case endFlag: // current item is ready, back to previous item           
        ci = ci.pre;
        break;
      }
    }
    return values.get(initValue);
  }

 

 

分享到:
评论
57 楼 kimmking 2008-08-21  
有些时候
使用递归是因为他们更适合人类的思维方式

不使用递归更像是机器的方式

编译器或解释器来做这个翻译工作
56 楼 javaeyebird 2008-08-14  
mingliangfeng 写道

举个简单的例子,如果结束条件是f(x) = 1 (x < end),end是一个调用递归时才能求出的值,你如何确定递推的前几个因子?退一步讲,就算是end是一个明确的值,由于递归形式没有确定,递推的前几个因子也不容易求出。

如果end的值依赖于f的参数以外的变量(unbound),那就不能缓存f(x)的结果,因为f(x)不是幂等的,多次求值会因为end的不同而得到不同的结果
另外如果f除了求值还有side effect,例如修改一个图的结点,那一般也不是幂等的,也不能缓存


对于某个幂等的递归计算,如果要充分优化时间和空间的效率,最好还是深入研究这个递归,变成递推
例如楼主的f(x) = f(x-1)+f(x-3) if x >= 3, = 10 if x < 3
其实只要缓存3个值,再用一个for就可以递推出来了,因为f(x-4)、f(x-5)等等值都可以丢弃
无论x多大,内存占用都是固定的,而使用一般的缓存方法,x很大时,缓存也会out of memory

实际应用中,如果栈足够用(可以把栈设置得很大),那直接在函数中判断并缓存值就可以省去重复计算了
同样是缓存,但cpu的栈操作还是比自己实现的栈更快,而且代码更简单

如果栈即使设置很大还是不够用,才需要自己在堆上实现栈
而这种规模的计算,往往内存也不够容纳整个缓存
,最后还是需要时间、空间一起优化,
就像上面说的只缓存3个值然后递推
55 楼 csf177 2008-06-25  
晕 才看明白 原来有个这个东西......
new HashMap<Double, Double>

那样的话 你可以把HashMap放到类成员级 递归的时候先检查是否在HashMap中  然后return之前把值存进HashMap里面 应该比你这样展开快得多

跟着起了半天哄 才发现就是备忘录加速的......不过如果楼主纯粹是自己想出来的 也挺不错
不玩了 回去罚站......
54 楼 csf177 2008-06-25  
mingliangfeng 写道


你没有仔细的看帖子,上面非递归比递归快,是因为非递归缓存了计算值,而直接的递归重复了许多不必要的计算,导致速度慢,并不是什么Java的函数调用栈比链表栈慢。



哦 我没认真看......
这难道就是传说中的备忘录 那好像你不是第一个哦......
而且你干嘛把递归展开到Java栈上再用备忘录?
53 楼 mingliangfeng 2008-06-24  
引用
一个问题是。
如果楼主只是为了提高时间效率,只需要用查表法(结果缓存)本身就够了。
反正都是缓存结果,递归形式和非递归形式的时间效率和空间效率是一样的。
为什么还需要用那个复杂的模型转换呢?


1.递归调用栈溢出比out of memory容易发生得多,这个在实际计算中常常发生。比如帖子中最后一个例子,用递归缓存结果的方法,递归因子上5000就有可能溢出,仅这一点就可以将递归实现排除。转换后的模型依赖内存大小,一般的PC机上x=50万都可以:
public static double recursion2(double x, Map<Double, Double> values) {
    if (values.get(x) != null) {
      return values.get(x);
    }
    if (x < 3) {
      values.put(x, 10.0);
      return 10.0;
    }

    double f1 = recursion2(x - 1, values);
    double f2 = recursion2(x - 3, values);
    double f3 = recursion2(x - 2, values);
    double result = (f1 + f2 + x) / f3;
    values.put(x, result);
    return result;
  }


2.非递归形式语言独立,而且是一个代码块,可以嵌入至任何计算过程中,这个可能只能算项目的特殊需求吧
3.这个转换模型复杂吗?可能是帖子标题有点唬人,这并不是什么数学理论的论证,只是我花了约一天时间总结出来的规律。各位讨论时列举的情况可是比这个深广得多,复杂得多,令人大开眼界啊


引用
简单的说 如果能写出这种公式了
这类问题直接用for从小到大就可以了
f[0]=XXX;f[1]=XXX;f[3]=XXX;
for(x=3;x<n;x++)
{
f[x]=(f[x/4-1]+f[x/6-2])/f[x-2];
}


你这只能算是针对特定递归的转化。如果是针对某个特定的递归,优化方法很多,能找到非常优越的方案。但是如果在转换之前,你连递归的形式都不知道,怎么能得出递推公式进而写出for循环呢?举个简单的例子,如果结束条件是f(x) = 1 (x < end),end是一个调用递归时才能求出的值,你如何确定递推的前几个因子?退一步讲,就算是end是一个明确的值,由于递归形式没有确定,递推的前几个因子也不容易求出。前面buaawhl给出的用move方法来实现的方案就是一例。

引用
另外 楼主的这个模板有个地方让我很震惊
Java的函数调用栈居然比链表栈还慢?


你没有仔细的看帖子,上面非递归比递归快,是因为非递归缓存了计算值,而直接的递归重复了许多不必要的计算,导致速度慢,并不是什么Java的函数调用栈比链表栈慢。

52 楼 csf177 2008-06-24  
另外 楼主的这个模板有个地方让我很震惊
Java的函数调用栈居然比链表栈还慢?
51 楼 csf177 2008-06-24  
mingliangfeng 写道
引用
剩下楼主这个模板适用的 也就剩下尾递归了。而在算法比较熟练的人手里尾递归几乎不可能出现(要是能写出递推公式来,有多少人傻到直接翻成递归去算),且不少编译器能识别并转换尾递归 所以说 这有多大意义?


不仅仅是尾部递归吧?比如说如下递归:
f(x)=(f(x/4-1) + f(x/6-2))/f(x-2)
这个可比尾部递归复杂多了,但是能通过上面的模板进行转换。不过我不知道这属于哪一类递归,其实我也不知道该怎样给递归公式分类。

这个帖子是我从当前应用开发中总结出来的一条规律,出自实际业务中的需求,现在程序运行的非常好。其实我没想过要在数学理论上研究一番然后发表一篇类似论文的东西。数学的发展远远的超过了时代,复杂的数学模型在自然界都很难找到对应的存在,更何况在当前的商业应用中。比如自从毕业后,我还从来没有使用过如复数的计算等东东。

当然了,如果各位是数学方面的专业人才,是应该研究研究,数学理论的发展就看你们的了。


简单的说 如果能写出这种公式了
这类问题直接用for从小到大就可以了
f[0]=XXX;f[1]=XXX;f[3]=XXX;
for(x=3;x<n;x++)
{
    f[x]=(f[x/4-1]+f[x/6-2])/f[x-2];
}
50 楼 buaawhl 2008-06-24  

一个问题是。
如果楼主只是为了提高时间效率,只需要用查表法(结果缓存)本身就够了。
反正都是缓存结果,递归形式和非递归形式的时间效率和空间效率是一样的。
为什么还需要用那个复杂的模型转换呢?
49 楼 mingliangfeng 2008-06-24  
引用
剩下楼主这个模板适用的 也就剩下尾递归了。而在算法比较熟练的人手里尾递归几乎不可能出现(要是能写出递推公式来,有多少人傻到直接翻成递归去算),且不少编译器能识别并转换尾递归 所以说 这有多大意义?


不仅仅是尾部递归吧?比如说如下递归:
f(x)=(f(x/4-1) + f(x/6-2))/f(x-2)
这个可比尾部递归复杂多了,但是能通过上面的模板进行转换。不过我不知道这属于哪一类递归,其实我也不知道该怎样给递归公式分类。

这个帖子是我从当前应用开发中总结出来的一条规律,出自实际业务中的需求,现在程序运行的非常好。其实我没想过要在数学理论上研究一番然后发表一篇类似论文的东西。数学的发展远远的超过了时代,复杂的数学模型在自然界都很难找到对应的存在,更何况在当前的商业应用中。比如自从毕业后,我还从来没有使用过如复数的计算等东东。

当然了,如果各位是数学方面的专业人才,是应该研究研究,数学理论的发展就看你们的了。
48 楼 csf177 2008-06-23  
Trustno1 写道
csf178 写道
Trustno1 写道

引用
要做这个东西其实麻烦在依赖数学比较多 复数运算+高次方程+多元方程组
如果能找到相应的lib 就很容易搞定了

与这些东西都无关,
关键的部分在于,你要能证明针对某一类型的递归函数推导出它的通项公式的这一过程是可计算的.
自己手动推导通项公式,在这里没有多大意义.


推导过程组合数学已经有了 不过把它用计算机描述出来需要 复数运算+高次方程+多元方程组 这几个东西

线形/差分递推式的求解当然是有的.
但是我这里强调的是,推导的过程是可计算的.
比如Fib,你首先是要找到生成函数.x/(1-x-x^2) ,然后根据生成函数找到通项公式.
当然通过特征方程找通项公式也很容易,但是特征方程如何找到呢?这样一种找法是不是可计算得?
比如说,A(n)=1+1/A(n-1),A(n) 你能不能找到一种机械的算法找到他的同项公式?


引用
n次方硬件浮点运算很快的 你试试Fibonacci的通项公式就知道了 比递推快多了......

即便是幂求解很快,问题是通项公式都是那么简单的吗?
比如找硬币问题的通项公式,给你1,2,5分的硬币.就算你找到了特征方程,你仍然要解8个待定系数的,5次方程才能找到通项公式.你觉得这个解法是搞通项公式快,还是动态规划法快?

所以我说复杂搜索楼主的模板处理不了 简单的(比如我说的复数运算+高次方程+多元方程组 就是指常系数线性的递推关系 所有常系数线性齐次的差分方程解都是通项公式都是幂运算没错吧)又有快速计算的通项公式

比如说你又如何从1,2,5分问题的搜索算法找出DP算法呢?其实这个跟递归的形式没有关系 重点在于搜索跟DP算法的差距。Functional语言同样要用递归来描述DP算法。

剩下楼主这个模板适用的 也就剩下尾递归了。而在算法比较熟练的人手里尾递归几乎不可能出现(要是能写出递推公式来,有多少人傻到直接翻成递归去算),且不少编译器能识别并转换尾递归 所以说 这有多大意义?

我觉得在语法树上识别尾递归并且加以优化才算比较有意义的研究 建议楼主往这方向发展发展

PS.要是我我能找到任何一个递推的机械计算方法,我第一个就找出
A(n)=A(n-1)+1/n,A(1)=1来 哥德巴赫猜想基本就差不多了 hiahia
47 楼 Trustno1 2008-06-23  
csf178 写道
Trustno1 写道

引用
要做这个东西其实麻烦在依赖数学比较多 复数运算+高次方程+多元方程组
如果能找到相应的lib 就很容易搞定了

与这些东西都无关,
关键的部分在于,你要能证明针对某一类型的递归函数推导出它的通项公式的这一过程是可计算的.
自己手动推导通项公式,在这里没有多大意义.


推导过程组合数学已经有了 不过把它用计算机描述出来需要 复数运算+高次方程+多元方程组 这几个东西

线形/差分递推式的求解当然是有的.
但是我这里强调的是,推导的过程是可计算的.
比如Fib,你首先是要找到生成函数.x/(1-x-x^2) ,然后根据生成函数找到通项公式.
当然通过特征方程找通项公式也很容易,但是特征方程如何找到呢?这样一种找法是不是可计算得?
比如说,A(n)=1+1/A(n-1),A(n) 你能不能找到一种机械的算法找到他的同项公式?


引用
n次方硬件浮点运算很快的 你试试Fibonacci的通项公式就知道了 比递推快多了......

即便是幂求解很快,问题是通项公式都是那么简单的吗?
比如找硬币问题的通项公式,给你1,2,5分的硬币.就算你找到了特征方程,你仍然要解8个待定系数的,5次方程才能找到通项公式.你觉得这个解法是搞通项公式快,还是动态规划法快?







46 楼 csf178 2008-06-23  
Trustno1 写道
引用
通项公式大部分时候是比较便宜的 CPU那句话说的就是这个意思...... 时间复杂度降了

这个问题,要看你计算n的有多少大,(1+√5)/2]^n,n是25的时候就已经超出65535了.再往上算,Int型就不够往上的大整数计算都是靠快速傅立叶变换做的,FFT 的主要的瓶颈是蝶形运算,这就需要用到非常好的矩阵算法库.如果是大浮点数的话可能还需要快速数论变换.这种计算量就非常之大了.

整型范围不是0-65535呀 那是16位机的整型......
n次方硬件浮点运算很快的 你试试Fibonacci的通项公式就知道了 比递推快多了......

FFT的话......只适用于特定领域的BT需求>_< 一般是超过机器浮点运算表示范围或者精度范围才会用到 像这种组合数学游戏是不需要的 一般语言本身提供的幂运算足够了......

另外 不管怎么变换 幂运算只能变得更快不是么..... 要不还不如直接乘了 复杂度最多也就O(logN)吧?O(logN)跟O(N)相差可不少

汗 学数学的思维就是强大 提到幂运算立刻就想到FFT了
45 楼 csf178 2008-06-23  
Trustno1 写道

引用
要做这个东西其实麻烦在依赖数学比较多 复数运算+高次方程+多元方程组
如果能找到相应的lib 就很容易搞定了

与这些东西都无关,
关键的部分在于,你要能证明针对某一类型的递归函数推导出它的通项公式的这一过程是可计算的.
自己手动推导通项公式,在这里没有多大意义.


推导过程组合数学已经有了 不过把它用计算机描述出来需要 复数运算+高次方程+多元方程组 这几个东西
44 楼 Trustno1 2008-06-23  
引用
通项公式大部分时候是比较便宜的 CPU那句话说的就是这个意思...... 时间复杂度降了

这个问题,要看你计算n的有多少大,(1+√5)/2]^n,n是25的时候就已经超出65535了.再往上算,Int型就不够往上的大整数计算都是靠快速傅立叶变换做的,FFT 的主要的瓶颈是蝶形运算,这就需要用到非常好的矩阵算法库.如果是大浮点数的话可能还需要快速数论变换.这种计算量就非常之大了.


引用
要做这个东西其实麻烦在依赖数学比较多 复数运算+高次方程+多元方程组
如果能找到相应的lib 就很容易搞定了

与这些东西都无关,
关键的部分在于,你要能证明针对某一类型的递归函数推导出它的通项公式的这一过程是可计算的.
自己手动推导通项公式,在这里没有多大意义.



43 楼 csf178 2008-06-23  
Trustno1 写道
引用
Sorry Sorry 我扯远了

我的意思是 就好像菲波那契数列 f(n+2)=f(n+1)+f(n),f(0)=0,f(1)=1可以推导出
f(n)=(1/√5)*{[(1+√5)/2]^n - [(1-√5)/2]^n}【√5表示根号5,m^n表示m的n次幂】

你处理的那类递归 组合数学里其实全都有办法化成通项公式的 而更复杂的递归你这个也处理不了吧

还有 有些简单的尾递归编译器会识别出来并且优化

所以你还是在别的地方下下功夫吧

关于CPU嘛 就是说 一个浮点数的n次方这种运算 有些CPU是可以直接算的 有些需要用程序算
不管哪种 都要比递推还快很多

要论速度,通项公式也不便宜.
我觉得LZ需要的是,能够根据一个递归公式自动推算出同项公式的程序.
这个东西,能够作出来就很NB哦.哪怕只是针对某一类递归函数.

通项公式大部分时候是比较便宜的 CPU那句话说的就是这个意思...... 时间复杂度降了

引用
能够根据一个递归公式自动推算出同项公式的程序

要做这个东西其实麻烦在依赖数学比较多 复数运算+高次方程+多元方程组
如果能找到相应的lib 就很容易搞定了

所以说实话 楼主这个把递归转换成递推意义不大 情况太特定了 仅仅针对实数域上的递归函数有效
42 楼 Trustno1 2008-06-23  
引用
Sorry Sorry 我扯远了

我的意思是 就好像菲波那契数列 f(n+2)=f(n+1)+f(n),f(0)=0,f(1)=1可以推导出
f(n)=(1/√5)*{[(1+√5)/2]^n - [(1-√5)/2]^n}【√5表示根号5,m^n表示m的n次幂】

你处理的那类递归 组合数学里其实全都有办法化成通项公式的 而更复杂的递归你这个也处理不了吧

还有 有些简单的尾递归编译器会识别出来并且优化

所以你还是在别的地方下下功夫吧

关于CPU嘛 就是说 一个浮点数的n次方这种运算 有些CPU是可以直接算的 有些需要用程序算
不管哪种 都要比递推还快很多

要论速度,通项公式也不便宜.
我觉得LZ需要的是,能够根据一个递归公式自动推算出同项公式的程序.
这个东西,能够作出来就很NB哦.哪怕只是针对某一类递归函数.
41 楼 csf178 2008-06-21  
mingliangfeng 写道

不太明白你想说什么!这只是一个很简单的算法转换问题,怎么扯到CPU上去了。

Sorry Sorry 我扯远了

我的意思是 就好像菲波那契数列 f(n+2)=f(n+1)+f(n),f(0)=0,f(1)=1可以推导出
f(n)=(1/√5)*{[(1+√5)/2]^n - [(1-√5)/2]^n}【√5表示根号5,m^n表示m的n次幂】

你处理的那类递归 组合数学里其实全都有办法化成通项公式的 而更复杂的递归你这个也处理不了吧

还有 有些简单的尾递归编译器会识别出来并且优化

所以你还是在别的地方下下功夫吧

关于CPU嘛 就是说 一个浮点数的n次方这种运算 有些CPU是可以直接算的 有些需要用程序算
不管哪种 都要比递推还快很多
40 楼 mingliangfeng 2008-06-21  
引用
例如Lua,就是stackless的. Python也有stackless的实现. 原则上,基于虚拟机的语言都可以实现stackless.


嗯,但是也有很多语言,比如当前做应用系统开发的主流语言JAVA和C#上,都没有类似的实现。我的帖子正源自一个JAVA的应用项目,而且是里面非常小的一小块,不可能改用其他语言或嵌入其他语言,那样会越搞越麻烦。


引用
不是我打击楼主的想法

这种只含有加法和移位算子的函数已经有数学公式了 连循环都不用 如果CPU的浮点运算器支持幂运算 那么时间复杂度O(1)就可以搞定 就算不支持浮点幂运算 也只是O(log(n))的时间

递归算法更多是用在搜索或者访问树等复杂结构的时候......


不太明白你想说什么!这只是一个很简单的算法转换问题,怎么扯到CPU上去了。
39 楼 csf178 2008-06-21  
不是我打击楼主的想法

这种只含有加法和移位算子的函数已经有数学公式了 连循环都不用 如果CPU的浮点运算器支持幂运算 那么时间复杂度O(1)就可以搞定 就算不支持浮点幂运算 也只是O(log(n))的时间

递归算法更多是用在搜索或者访问树等复杂结构的时候......
38 楼 rubynroll 2008-06-13  
引用
不知道你指的有些语言是指哪些语言?在java, c#, PLSQL等语言下,栈的溢出比out of memory经常得多。


例如Lua,就是stackless的. Python也有stackless的实现. 原则上,基于虚拟机的语言都可以实现stackless.

所谓栈的溢出,实际上是栈的使用超出了操作系统给你的配额. 可以用ulimit -s来增加这个上限.
大多数操作系统都是实际用到多少stack分配多少,不会按配额预分配stack,因此不会浪费.

对于用语言自身的递归特性实现的递归而言,运行时间是瓶颈所在。试试例子中的递归实现就知道了:

 
Java代码 复制代码

   1. public static double recursion(double x) {     
   2.   if (x < 3)     
   3.     return 10.0;     
   4.     
   5.   double f1 = recursion(x - 1);     
   6.   double f2 = recursion(x - 3);     
   7.   double f3 = recursion(x - 2);     
   8.   return (f1 + f2 + x) / f3;     
   9. }    

public static double recursion(double x) {   
  if (x < 3)   
    return 10.0;   
  
  double f1 = recursion(x - 1);   
  double f2 = recursion(x - 3);   
  double f3 = recursion(x - 2);   
  return (f1 + f2 + x) / f3;   
}  



不经cache优化的话,大部分时间花在重复计算中间结果,当然慢了.以下是ruby的优化实现:

# x < 3: f(x) = 10.0, else: f(x) = (f(x-1) + f(x-3) + x) / f(x-2)

def f(x, cache = {})
  unless cache[x]
    cache[x] = (x < 3 ? 10.0 : (f(x-1, cache) + f(x-3, cache) + x) / f(x-2, cache))
  end
  cache[x]
end

x = 10000.0
puts "f(#{x}) = #{f(x)}"




在我的机器上(老爷机了),ruby1.8.6不到1秒. 如果是ruby 1.9相信会更快.

* 此例子默认的8192 stack不够用,需增加操作系统的stack上限.我用ulimit -s 100000测试通过.


引用
就空间来讲确实没有优化;但是用来cache的空间会优化运行时间。同时优化空间和时间,对于特定的递归有可能,但不可能找到通用的方法。


对于所有类型的递归,同时优化时间和空间的通用方法似乎不存在...用cache来优化时间却是一个通用的方法.




相关推荐

    n后问题---递归回溯法 n后问题---递归回溯法

    n后问题---递归回溯法 n后问题---递归回溯法 n后问题---递归回溯法 n后问题---递归回溯法 n后问题---递归回溯法 n后问题---递归回溯法 n后问题---递归回溯法 n后问题---递归回溯法 n后问题---递归回溯法 n后问题---...

    递归算法到非递归算法的转换.ppt

    例如,计算阶乘的递归算法会根据n的值调用自身计算n-1的阶乘,直到n等于0,这时递归停止,返回1。这就是递归的终止条件。 递归通常在以下三种情况下被使用: 1. 定义是递归的:比如,Fibonacci数列的定义就涉及到...

    递归向非递归的转换问题

    递归向非递归的转换问题一直是一个容易忽视的问题,所以重新整理了一下供大家批评指教

    编译原理课设:属性计算-递归下降语法分析器

    参考例6.2构造一个翻译模式,并由此构造一个递归下降翻译器,把每个标识符的类型存入符号表。 功能拓展: 对于输入的一串执行语句,其中包括:赋值语句、选择语句和循环语句。设计递归下降翻译器,完成语法分析和...

    《大学计算机-计算思维导论》19-20学时-递归-ding-20201

    《大学计算机-计算思维导论》课程中,第19-20学时重点讨论了递归和程序构造的基本概念。递归是程序构造的重要手段之一,它涉及到计算系统与程序的关系,以及如何通过基本动作的组合来实现复杂的计算。 递归是一种自...

    递归实现的表达式计算

    在这个主题“递归实现的表达式计算”中,我们将深入探讨如何使用递归方法解析和计算数学表达式。递归的核心思想是解决问题的子问题与原问题具有相同的形式,因此可以通过调用自身来解决。 首先,我们要理解表达式...

    Python语言程序设计教程 北理工Python课程第6章-函数与递归-4-程序结构和递归 共20页.pdf

    0-1-课程内容和安排介绍1-1-计算机的概念1-2-程序设计语言概述1-3-Python语言1-4-Python开发环境配置1-5-基本程序设计方法1-6-理解问题的计算部分1-7-温度转换程序实例2-1-Python程序元素分析2-2-程序编写模板2-3-...

    二叉树的操作--递归非递归遍历、结点个数、树深度

    计算出深度 结点数 /* 运行结果: ------------------------ 请先序输入二叉树(如:ab三个空格表示a为根节点,b为左子树的二叉树) ab c 先序递归遍历二叉树: a b c 先序非递归遍历二叉树: a b c 中序递归遍历...

    递归计算Ackermann函数的实现.zip

    递归计算Ackermann函数的实现.zip 递归计算Ackermann函数的实现.zip 递归计算Ackermann函数的实现.zip 递归计算Ackermann函数的实现.zip 递归计算Ackermann函数的实现.zip 递归计算Ackermann函数的实现.zip 递归计算...

    递归-----动态树实现递归

    标题“递归-----动态树实现递归”暗示我们将探讨如何利用递归方法来操作动态树。 首先,让我们理解什么是动态树。动态树,顾名思义,是一种可以随程序运行而改变结构的树形数据结构。与静态树不同,它的节点可以在...

    算法基础与递归-百积问题-递归求公约数-求阶乘-斐波那契数列

    * 通过递归和非递归方法实现最大公约数的计算 * 通过递归和非递归方法实现阶乘的计算 * 实现斐波那契数列的计算 * 实现汉诺塔问题的解决 四、代码实现 下面是实验的代码实现: 1. 汉诺塔问题的解决 ```c void ...

    计算器递归----c++

    该程序能够处理基本的数学运算,包括加法、减法、乘法和除法,并通过递归的方式解析和计算表达式。 #### 主要知识点 ##### 1. **递归算法在计算器中的应用** - **递归的基本概念**:递归是一种通过函数调用自身...

    基础算法-递归-杨鑫20191010.pptx

    基础算法-递归-杨鑫20191010.pptx,基础算法-递归-杨鑫20191010.pptx,基础算法-递归-杨鑫20191010.pptx

    java-c语法7---method-递归---马克-to-win java视频

    java语法 method 递归 马克-to-win java视频 方法 重载

    计算机数学-刘新宇-递归

    在某些情况下,递归函数可以转换为迭代形式来优化性能,例如通过循环替换递归,从而降低空间复杂度。 递归可以用于解决各种数学问题,比如数论中探讨素数、完全数和数列的求和。其中,完全数是一个古老的数学概念,...

    NOIP普及讲座1-递归与分治(C++版).pdf

    根据给定文件的信息,我们可以总结出以下关于递归与分治技术的重要知识点: ### 一、递归的基本概念 #### 定义: 递归是一种在程序设计中非常重要的方法,它指的是在一个函数或过程中直接或间接地调用自身来解决...

    进制转换-递归.py

    进制转换-递归.py

    c语言 二叉树应用:创建、递归非递归遍历、计算结点、分支、交换子树

    递归计算单分支结点: 递归计算双分支结点: 递归计算叶子数: 二叉数的深度: 交换二叉树的左右子树: 二叉树已左右交换。 递归先序遍历二叉树: 递归中序遍历二叉树: 递归后序遍历二叉树:

    使用递归计算阶乘

    java中使用递归方法计算阶乘的代码示例

    数据结构:栈的应用-递归函数转非递归函数

    在这个主题中,我们将深入探讨如何利用栈来转换递归函数,将其优化为非递归形式,以提高计算效率。 首先,让我们了解递归函数。递归是一种解决问题的方法,函数通过调用自身来解决更小规模的问题。例如,阶乘是一个...

Global site tag (gtag.js) - Google Analytics