有没有办法通过记住子节点来加速递归?

Posted

技术标签:

【中文标题】有没有办法通过记住子节点来加速递归?【英文标题】:Is there some way to speed up recursion by remembering child nodes? 【发布时间】:2010-09-06 15:23:34 【问题描述】:

例如, 看计算第n个斐波那契数的代码:

fib(int n)

    if(n==0 || n==1)
        return 1;
    return fib(n-1) + fib(n-2);

此代码的问题在于它会为任何大于 15 的数字生成堆栈溢出错误(在大多数计算机中)。

假设我们正在计算 fib(10)。在这个过程中,假设 fib(5) 被计算了很多次。有没有办法将它存储在内存中以便快速检索,从而提高递归速度?

我正在寻找一种可以用于几乎所有问题的通用技术。

【问题讨论】:

【参考方案1】:

@lassevk:

这太棒了,这正是我在阅读了Higher Order Perl 中的记忆后一直在想的。有两件事我认为会是有用的补充:

    一个可选参数,用于指定用于生成缓存键的静态或成员方法。 一种更改缓存对象的可选方法,以便您可以使用磁盘或数据库支持的缓存。

不知道如何用 Attributes 做这种事情(或者如果他们甚至可以用这种实现来做),但我打算试着弄清楚。

(题外话:我试图将此作为评论发布,但我没有意识到 cmets 的允许长度如此短,因此这并不适合作为“答案”)

【讨论】:

【参考方案2】:

顺便说一下,Perl 有一个 memoize 模块,它可以为您指定的代码中的任何函数执行此操作。

# Compute Fibonacci numbers
sub fib 
      my $n = shift;
      return $n if $n < 2;
      fib($n-1) + fib($n-2);

为了记住这个函数,你要做的就是用

启动你的程序
use Memoize;
memoize('fib');
# Rest of the fib function just like the original version.
# Now fib is automagically much faster ;-)

【讨论】:

【参考方案3】:

Wes Dyer 的博客是针对 C# 程序员的递归、部分、柯里化、记忆化等方面的另一个优秀资源,尽管他已经有一段时间没有发布了。他很好地解释了记忆,这里有可靠的代码示例: http://blogs.msdn.com/wesdyer/archive/2007/01/26/function-memoization.aspx

【讨论】:

【参考方案4】:

Mathematica 有一种特别巧妙的方式来做记忆,依赖于哈希和函数调用使用相同的语法这一事实:

fib[0] = 1;
fib[1] = 1;
fib[n_] := fib[n] = fib[n-1] + fib[n-2]

就是这样。它缓存(记忆)fib[0] 和 fib[1] 并根据需要缓存其余部分。模式匹配函数调用的规则是,它总是在更一般的定义之前使用更具体的定义。

【讨论】:

【参考方案5】:

正如其他发帖者所指出的,memoization 是一种用内存换取速度的标准方法,这里有一些伪代码来实现任何函数的记忆(只要该函数没有副作用):

初始功能码:

 function (parameters)
      body (with recursive calls to calculate result)
      return result

这应该转化为

 function (parameters)
      key = serialized parameters to string
      if (cache[key] does not exist)  
           body (with recursive calls to calculate result)
           cache[key] = result
      
      return cache[key]

【讨论】:

【参考方案6】:

其他人已经很好且准确地回答了您的问题 - 您正在寻找记忆。

带有tail call optimization 的编程语言(主要是函数式语言)可以在没有堆栈溢出的情况下执行某些递归情况。它并不直接适用于您对斐波那契的定义,尽管有技巧..

您的问题的措辞让我想到了一个有趣的想法。通过仅存储堆栈帧的子集并在必要时重建来避免纯递归函数的堆栈溢出。仅在少数情况下真正有用。如果您的算法仅有条件地依赖上下文而不是返回,和/或您正在优化内存而不是速度。

【讨论】:

【参考方案7】:

在 C++ 中快速而肮脏的记忆:

type1 foo(type2 bar) ... 的任何递归方法都可以用map&lt;type2, type1&gt; M 轻松记忆。

// your original method
int fib(int n)

    if(n==0 || n==1)
        return 1;
    return fib(n-1) + fib(n-2);


// with memoization
map<int, int> M = map<int, int>();
int fib(int n)

    if(n==0 || n==1)
        return 1;

    // only compute the value for fib(n) if we haven't before
    if(M.count(n) == 0)
        M[n] = fib(n-1) + fib(n-2);

    return M[n];

编辑:@康拉德鲁道夫 Konrad 指出 std::map 不是我们可以在这里使用的最快的数据结构。没错,vector&lt;something&gt; 应该比map&lt;int, something&gt; 快(尽管如果函数的递归调用的输入不是像本例中那样的连续整数,它可能需要更多内存),但映射使用起来很方便一般来说。

【讨论】:

【参考方案8】:

是的,您的见解是正确的。 这称为dynamic programming。这通常是一种常见的内存运行时权衡。

在 fibo 的情况下,您甚至不需要缓存所有内容:

[编辑] 该问题的作者似乎正在寻找一种通用的缓存方法,而不是一种计算斐波那契的方法。搜索***或查看其他海报的代码以获得此答案。这些答案在时间和记忆上是线性的。

**这里是一个线性时间算法O(n),在内存中是常数**

in OCaml:

let rec fibo n = 
    let rec aux = fun
        | 0 -> (1,1)
        | n -> let (cur, prec) = aux (n-1) in (cur+prec, cur)
    let (cur,prec) = aux n in prec;;



in C++:

int fibo(int n) 
    if (n == 0 ) return 1;
    if (n == 1 ) return 1;
    int p = fibo(0);
    int c = fibo(1);
    int buff = 0;
    for (int i=1; i < n; ++i) 
      buff = c;
      c = p+c;
      p = buff;
    ;
    return c;
;

这在线性时间内执行。但是日志实际上是可能的! Roo 的程序也是线性的,但速度较慢,而且会占用内存。

这里是对数算法 O(log(n))

现在对于日志时间算法(方式方式方式更快),这是一个方法: 如果你知道 u(n), u(n-1),计算 u(n+1), u(n) 可以通过应用一个矩阵来完成:

| u(n+1) |  = | 1 1 | | u(n)   |
| u(n)   |    | 1 0 | | u(n-1) |    

这样你就有了:

| u(n)    |  = | 1 1 |^(n-1) | u(1) | = | 1 1 |^(n-1) | 1 |
| u(n-1)  |    | 1 0 |       | u(0) |   | 1 0 |       | 1 |

计算矩阵的指数具有对数复杂度。 只需递归地实现这个想法:

M^(0)    = Id
M^(2p+1) = (M^2p) * M
M^(2p)   = (M^p) * (M^p)  // of course don't compute M^p twice here.

您也可以将其对角化(不难),您会在其特征值中找到黄金数及其共轭,结果将为您提供 u(n) 的精确数学公式。它包含这些特征值的幂,因此复杂度仍然是对数的。

斐波那契波经常被拿来作为例子来说明动态规划,但正如你所见,它并不是很贴切。

@约翰: 我认为这与哈希无关。

@约翰2: 地图有点笼统,你不觉得吗?对于 Fibonacci 案例,所有键都是连续的,因此向量是合适的,再次有更快的方法来计算 fibo 序列,请参阅我的代码示例。

【讨论】:

+1 表示矩阵方程,这是我第一次看到精确公式的推导 谢谢! ( ^-^ ) 并且您可以将该方法用于任何递归序列。【参考方案9】:

如果您使用具有一流功能的语言(如 Scheme),则可以在不更改初始算法的情况下添加记忆:

(define (memoize fn)
  (letrec ((get (lambda (query) '(#f)))
           (set (lambda (query value)
                  (let ((old-get get))
                    (set! get (lambda (q)
                                (if (equal? q query)
                                    (cons #t value)
                                    (old-get q))))))))
    (lambda args
      (let ((val (get args)))
        (if (car val)
            (cdr val)
            (let ((ret (apply fn args)))
              (set args ret)
              ret))))))


(define fib (memoize (lambda (x)
                       (if (< x 2) x
                           (+ (fib (- x 1)) (fib (- x 2)))))))

第一个块提供记忆功能,第二个块是使用该功能的斐波那契数列。这现在有一个 O(n) 运行时间(与没有记忆的算法 O(2^n) 相对)。

注意:提供的记忆功能使用闭包链来查找以前的调用。在最坏的情况下,这可能是 O(n)。然而,在这种情况下,所需的值始终位于链的顶部,确保 O(1) 查找。

【讨论】:

【参考方案10】:

如果您使用的是 C#,并且可以使用 PostSharp,这里有一个简单的代码记忆方面:

[Serializable]
public class MemoizeAttribute : PostSharp.Laos.OnMethodBoundaryAspect, IEqualityComparer<Object[]>

    private Dictionary<Object[], Object> _Cache;

    public MemoizeAttribute()
    
        _Cache = new Dictionary<object[], object>(this);
    

    public override void OnEntry(PostSharp.Laos.MethodExecutionEventArgs eventArgs)
    
        Object[] arguments = eventArgs.GetReadOnlyArgumentArray();
        if (_Cache.ContainsKey(arguments))
        
            eventArgs.ReturnValue = _Cache[arguments];
            eventArgs.FlowBehavior = FlowBehavior.Return;
        
    

    public override void OnExit(MethodExecutionEventArgs eventArgs)
    
        if (eventArgs.Exception != null)
            return;

        _Cache[eventArgs.GetReadOnlyArgumentArray()] = eventArgs.ReturnValue;
    

    #region IEqualityComparer<object[]> Members

    public bool Equals(object[] x, object[] y)
    
        if (Object.ReferenceEquals(x, y))
            return true;

        if (x == null || y == null)
            return false;

        if (x.Length != y.Length)
            return false;

        for (Int32 index = 0, len = x.Length; index < len; index++)
            if (Comparer.Default.Compare(x[index], y[index]) != 0)
                return false;

        return true;
    

    public int GetHashCode(object[] obj)
    
        Int32 hash = 23;

        foreach (Object o in obj)
        
            hash *= 37;
            if (o != null)
                hash += o.GetHashCode();
        

        return hash;
    

    #endregion

这是一个使用它的斐波那契实现示例:

[Memoize]
private Int32 Fibonacci(Int32 n)

    if (n <= 1)
        return 1;
    else
        return Fibonacci(n - 2) + Fibonacci(n - 1);

【讨论】:

我对您的回答发表了评论,但由于太大而无法作为评论:***.com/questions/23962/…【参考方案11】:

@ESRogs:

std::map 查找是 O(log n) 这使得它在这里很慢。最好使用矢量。

vector<unsigned int> fib_cache;
fib_cache.push_back(1);
fib_cache.push_back(1);

unsigned int fib(unsigned int n) 
    if (fib_cache.size() <= n)
        fib_cache.push_back(fib(n - 1) + fib(n - 2));

    return fib_cache[n];

【讨论】:

【参考方案12】:

根据wikipediaFib(0) 应该是0 但没关系。

这是带有 for 循环的简单 C# 解决方案:

ulong Fib(int n)

  ulong fib = 1;  // value of fib(i)
  ulong fib1 = 1; // value of fib(i-1)
  ulong fib2 = 0; // value of fib(i-2)

  for (int i = 0; i < n; i++)
  
    fib = fib1 + fib2;
    fib2 = fib1;
    fib1 = fib;
  

  return fib;

将递归转换为tail recursion 然后循环是很常见的技巧。有关更多详细信息,请参见 lecture (ppt)。

【讨论】:

【参考方案13】:

此代码的问题在于它会为任何大于 15 的数字生成堆栈溢出错误(在大多数计算机中)。

真的吗?你用的是什么电脑?在 44 处花了很长时间,但堆栈没有溢出。事实上,在堆栈溢出(Fibbonaci(46))之前,您将获得一个大于整数可以容纳的值(约 40 亿无符号,约 20 亿有符号)。

这将适用于您想要做的事情(运行速度很快)

class Program

    public static readonly Dictionary<int,int> Items = new Dictionary<int,int>();
    static void Main(string[] args)
    
        Console.WriteLine(Fibbonacci(46).ToString());
        Console.ReadLine();
    

    public static int Fibbonacci(int number)
    
        if (number == 1 || number == 0)
        
            return 1;
        

        var minus2 = number - 2;
        var minus1 = number - 1;

        if (!Items.ContainsKey(minus2))
        
            Items.Add(minus2, Fibbonacci(minus2));
        

        if (!Items.ContainsKey(minus1))
        
            Items.Add(minus1, Fibbonacci(minus1));
        

        return (Items[minus2] + Items[minus1]);
    

【讨论】:

【参考方案14】:

尝试使用映射,n 是键,其对应的斐波那契数是值。

@保罗

感谢您的信息。我不知道。来自您提到的Wikipedia link:

这种保存值的技术 已经计算过的称为 记忆

是的,我已经看过代码 (+1)。 :)

【讨论】:

【参考方案15】:

这是一个故意选择的例子吗? (例如,您要测试的极端情况)

由于目前是 O(1.6^n),我只是想确保您只是在寻找有关处理此问题的一般情况(缓存值等)的答案,而不仅仅是意外编写糟糕的代码:D

看看这个具体的案例,你可能会有以下几点:

var cache = [];
function fib(n) 
    if (n < 2) return 1;
    if (cache.length > n) return cache[n];
    var result = fib(n - 2) + fib(n - 1);
    cache[n] = result;
    return result;

在最坏的情况下退化为 O(n) :D

[编辑:* 不等于 + :D]

[另一个编辑:Haskell 版本(因为我是受虐狂之类的)

fibs = 1:1:(zipWith (+) fibs (tail fibs))
fib n = fibs !! n

]

【讨论】:

【参考方案16】:

这被称为记忆化,这几天发布了一篇关于记忆化的非常好的文章Matthew Podwysocki。它使用斐波那契来举例说明。并显示 C# 中的代码。阅读here。

【讨论】:

【参考方案17】:

缓存通常是这种事情的好主意。由于斐波那契数是恒定的,因此您可以在计算后缓存结果。一个快速的 c/伪代码示例

class fibstorage 


    bool has-result(int n)  return fibresults.contains(n); 
    int get-result(int n)  return fibresult.find(n).value; 
    void add-result(int n, int v)  fibresults.add(n,v); 

    map<int, int>   fibresults;




fib(int n ) 
    if(n==0 || n==1)
            return 1;

    if (fibstorage.has-result(n)) 
        return fibstorage.get-result(n-1);
    

    return ( (fibstorage.has-result(n-1) ? fibstorage.get-result(n-1) : fib(n-1) ) +
             (fibstorage.has-result(n-2) ? fibstorage.get-result(n-2) : fib(n-2) )
           );



calcfib(n) 
    v = fib(n);
    fibstorage.add-result(n,v);

这会很慢,因为每次递归都会导致 3 次查找,但是这应该能说明总体思路

【讨论】:

【参考方案18】:

这是什么语言?它不会溢出 c 中的任何内容... 此外,您可以尝试在堆上创建查找表,或使用映射

【讨论】:

以上是关于有没有办法通过记住子节点来加速递归?的主要内容,如果未能解决你的问题,请参考以下文章

oracle 递归 通过子节点查询根节点

oracle递归查询子节点

java 找到一节点的所有子节点 是否得递归实现?

oracle递归查询子节点

SQL递归查询所有子节点

200分求助!SQL递归查询所有子节点