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

时间:2008-08-23 04:12:29

标签: performance recursion

例如, 查看计算第n个Fibonacci数的代码:

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

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

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

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

18 个答案:

答案 0 :(得分:17)

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

对于fibo,您甚至不需要缓存所有内容:

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

**这是一个线性时间算法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)的精确数学公式。它包含那些特征值的幂,因此复杂性仍然是对数的。

Fibo经常被作为一个例子来说明动态规划,但正如你所看到的,它并不是真正相关的。

@约翰: 我不认为它与哈希有任何关系。

@ John2: 一张地图有点一般你不觉得吗?对于Fibonacci情况,所有键都是连续的,因此矢量是合适的,再次有更快的方法来计算fibo序列,请参阅那里的代码示例。

答案 1 :(得分:7)

这称为memoization,这篇文章中有一篇关于memoization Matthew Podwysocki的文章非常好。它使用斐波纳契来举例说明它。并在C#中显示代码。阅读here

答案 2 :(得分:5)

如果你正在使用C#,并且可以使用PostSharp,这里有一个代码的简单memoization方面:

[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
}

以下是使用它的Fibonacci实现示例:

[Memoize]
private Int32 Fibonacci(Int32 n)
{
    if (n <= 1)
        return 1;
    else
        return Fibonacci(n - 2) + Fibonacci(n - 1);
}

答案 3 :(得分:4)

C ++中快速而脏的memoization:

任何递归方法type1 foo(type2 bar) { ... }都可以使用map<type2, type1> 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 Rudolph
Konrad指出std :: map不是我们在这里可以使用的最快的数据结构。这是真的,vector<something>应该比map<int, something>更快(尽管如果函数的递归调用的输入不是像这种情况那样的连续整数,它可能需要更多的内存),但是映射一般使用方便。

答案 4 :(得分:2)

根据wikipedia Fib(0)应为0,但无关紧要。

这是一个简单的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)。

答案 5 :(得分:1)

这是什么语言?它没有溢出任何东西... 此外,您可以尝试在堆上创建查找表,或使用映射

答案 6 :(得分:1)

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

答案 7 :(得分:1)

Mathematica有一种特别灵活的记忆方式,依赖哈希和函数调用使用相同语法的事实:

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

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

答案 8 :(得分:1)

其他人已经很好准确地回答了你的问题 - 你正在寻找备忘录。

使用tail call optimization(主要是函数式语言)的编程语言可以在没有堆栈溢出的情况下执行某些递归的情况。它不直接适用于你对Fibonacci的定义,虽然有一些技巧..

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

答案 9 :(得分:1)

@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];
}

答案 10 :(得分:1)

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

因为它现在是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

答案 11 :(得分:1)

对于这种事情来说,缓存通常是一个好主意。由于斐波纳契数是常数,因此您可以在计算结果后对结果进行缓存。快速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次查找,但这应该说明一般的想法

答案 12 :(得分:1)

尝试使用地图,n是键,其对应的Fibonacci数是值。

@保罗

感谢您的信息。我不知道。从你提到的Wikipedia link

  

这种保存价值的技术   已被计算的人被称为   记忆化

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

答案 13 :(得分:0)

  

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

真的?你用的是什么电脑?这需要很长时间在44,但堆栈没有溢出。实际上,在堆栈过流之前,你将得到一个大于整数的值可以保持(约40亿无符号,约20亿符号)。(/ p> p / p>

这可以用于你想要做的事情(快速运行)

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 :(得分:0)

如果您使用的语言类似于Scheme这样的第一类函数,则可以在不更改初始算法的情况下添加memoization:

(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)运行时(相对于没有memoization的算法的O(2 ^ n))。

注意:提供的memoization工具使用一系列闭包来查找以前的调用。在最坏的情况下,这可以是O(n)。但是,在这种情况下,所需的值始终位于链的顶部,确保O(1)查找。

答案 15 :(得分:0)

正如其他海报所指出的,memoization是以速度换取内存的标准方法,这里有一些伪代码来实现任何函数的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]

答案 16 :(得分:0)

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 ;-)

答案 17 :(得分:0)

@lassevk:

这很棒,正是在阅读 Higher Order Perl 中的memoization之后,我一直在想的。我认为有两件事是有用的补充:

  1. 一个可选参数,用于指定用于生成缓存密钥的静态或成员方法。
  2. 更改缓存对象的可选方法,以便您可以使用磁盘或数据库支持的缓存。
  3. 不确定如何使用Attributes执行此类操作(或者如果使用这种实现它们甚至可能),但我打算尝试找出。

    (偏离主题:我试图将此作为评论发布,但我没有意识到评论的长度允许这么短,所以这并不适合作为'答案')