【问题标题】:How to avoid stack overflow in this F# program (recursive tree search)?如何避免此 F# 程序中的堆栈溢出(递归树搜索)?
【发布时间】:2017-02-10 15:10:33
【问题描述】:

我有一个像这样的可区分联合树:

type rbtree =
    | LeafB of int
    | LeafR of int
    | Node of int*rbtree*rbtree

而我要做的是搜索树中存在的每个 LeafB,所以我提供了一个递归函数:

let rec searchB (tree:rbtree) : rbtree list = 
    match tree with
    | LeafB(n) -> LeafB(n)::searchB tree
    | LeafR(n) -> []
    | Node(n,left,right) -> List.append (searchB left) (searchB right)

但是当我尝试测试它时,我得到了堆栈溢出异常,我不知道如何修改它以使其正常工作。

【问题讨论】:

    标签: f#


    【解决方案1】:

    正如@kvb 所说,您的更新版本并不是真正的tail-rec,也可能导致stackoverflow。

    您可以做的是使用延续,本质上是使用堆空间而不是堆栈空间。

    let searchB_ tree =
      let rec tail results continuation tree =
        match tree with
        | LeafB v           -> continuation (v::results)
        | LeafR _           -> continuation results
        | Node  (_, lt, rt) -> tail results (fun leftResults -> tail leftResults continuation rt) lt
      tail [] id tree |> List.rev
    

    如果我们查看ILSpy 中生成的代码,它看起来基本上是这样的:

    internal static a tail@13<a>(FSharpList<int> results, FSharpFunc<FSharpList<int>, a> continuation, Program.rbtree tree)
    {
      while (true)
      {
        Program.rbtree rbtree = tree;
        if (rbtree is Program.rbtree.LeafR)
        {
          goto IL_34;
        }
        if (!(rbtree is Program.rbtree.Node))
        {
          break;
        }
        Program.rbtree.Node node = (Program.rbtree.Node)tree;
        Program.rbtree rt = node.item3;
        FSharpList<int> arg_5E_0 = results;
        FSharpFunc<FSharpList<int>, a> arg_5C_0 = new Program<a>.tail@17-1(continuation, rt);
        tree = node.item2;
        continuation = arg_5C_0;
        results = arg_5E_0;
      }
      Program.rbtree.LeafB leafB = (Program.rbtree.LeafB)tree;
      int v = leafB.item;
      return continuation.Invoke(FSharpList<int>.Cons(v, results));
      IL_34:
      return continuation.Invoke(results);
    }
    

    因此,正如 F# 中的尾递归函数所期望的那样,它被转换为 while 循环。如果我们看一下非尾递归函数:

    // Program
    public static FSharpList<int> searchB(Program.rbtree tree)
    {
      if (tree is Program.rbtree.LeafR)
      {
        return FSharpList<int>.Empty;
      }
      if (!(tree is Program.rbtree.Node))
      {
        Program.rbtree.LeafB leafB = (Program.rbtree.LeafB)tree;
        return FSharpList<int>.Cons(leafB.item, FSharpList<int>.Empty);
      }
      Program.rbtree.Node node = (Program.rbtree.Node)tree;
      Program.rbtree right = node.item3;
      Program.rbtree left = node.item2;
      return Operators.op_Append<int>(Program.searchB(left), Program.searchB(right));
    }
    

    我们在函数Operators.op_Append&lt;int&gt;(Program.searchB(left), Program.searchB(right));的末尾看到递归调用

    所以尾递归函数分配延续函数而不是创建一个新的堆栈帧。我们仍然可以用完堆,但堆比堆栈多得多。

    演示 stackoverflow 的完整示例:

    type rbtree =
      | LeafB of int
      | LeafR of int
      | Node  of int*rbtree*rbtree
    
    let rec searchB tree = 
      match tree with
      | LeafB(n) -> n::[]
      | LeafR(n) -> []
      | Node(n,left,right) -> List.append (searchB left) (searchB right)
    
    let searchB_ tree =
      let rec tail results continuation tree =
        match tree with
        | LeafB v           -> continuation (v::results)
        | LeafR _           -> continuation results
        | Node  (_, lt, rt) -> tail results (fun leftResults -> tail leftResults continuation rt) lt
      tail [] id tree |> List.rev
    
    let rec genTree n =
      let rec loop i t =
        if i > 0 then
          loop (i - 1) (Node (i, t, LeafB i))
        else
          t
      loop n (LeafB n)
    
    [<EntryPoint>]
    let main argv =
      printfn "generate left leaning tree..."
      let tree  = genTree 100000
      printfn "tail rec"
      let s     = searchB_  tree
      printfn "rec"
      let f     = searchB   tree
    
      printfn "Is equal? %A" (f = s)
    
      0
    

    【讨论】:

      【解决方案2】:

      哦,我可能想出了一个解决方案:

      let rec searchB (tree:rbtree) : rbtree list = 
      match tree with
      | LeafB(n) -> LeafB(n)::[]
      | LeafR(n) -> []
      | Node(n,left,right) -> List.append (searchB left) (searchB right)
      

      现在,当我尝试它时,它看起来工作正常。

      【讨论】:

      • 只要你的树不是太深,这将起作用;但是,请注意,对searchB 的递归调用将导致堆栈增长,因此对于非常深的树,仍然可能导致堆栈溢出。
      猜你喜欢
      • 2011-08-15
      • 2015-11-22
      • 1970-01-01
      • 2013-06-28
      • 2010-10-28
      • 1970-01-01
      • 2010-11-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多