頼らずにこれを解決するに@
は (最初のリストの長さが直線的であるため非効率的です)、2 つのアキュムレータが必要になります。ノード)、およびこれまでに見つけたすべてのパスに 1 つ (見つけたパスを追加できるようにするため)。
let rec pathToListRec tree pathToParent pathsFound =
match tree with
| Leaf -> pathsFound
| Node (left, x, right) ->
let pathToHere = x :: pathToParent
// Add the paths to nodes in the right subtree
let pathsFound' = pathToListRec right pathToHere pathsFound
// Add the path to the current node
let pathsFound'' = pathToHere :: pathsFound'
// Add the paths to nodes in the left subtree, and return
pathToListRec left pathToHere pathsFound''
let pathToList1 tree = pathToListRec tree [] []
末尾再帰に関する限り、上記の関数の 2 つの再帰呼び出しの 1 つが末尾位置にあることがわかります。ただし、テール位置ではないコールがまだあります。
ツリー処理関数の経験則は次のとおりです。完全に末尾再帰にするのは簡単ではありません。理由は簡単です: 単純に行うと、2 つの再帰 (左のサブツリーまたは右のサブツリーへのダウン) の少なくとも 1 つが必然的に末尾以外の位置になります。これを行う唯一の方法は、リストを使用してコール スタックをシミュレートすることです。これは、システム提供の呼び出しスタックの代わりにリストを使用していることを除けば、非末尾再帰バージョンと同じランタイムの複雑さを持つことを意味するため、おそらく遅くなるでしょう。
とにかく、それがどのように見えるかは次のとおりです。
let rec pathToListRec stack acc =
match stack with
| [] -> acc
| (pathToParent, tree) :: restStack ->
match tree with
| Leaf -> pathToListRec restStack acc
| Node (left, x, right) ->
let pathToHere = x :: pathToParent
// Push both subtrees to the stack
let newStack = (pathToHere, left) :: (pathToHere, right) :: restStack
// Add the current path to the result, and keep processing the stack
pathToListRec newStack (pathToHere :: acc)
// The initial stack just contains the initial tree
let pathToList2 tree = pathToListRec [[], tree] []
コードはそれほど悪くはありませんが、スタックの仕事をするためにリストを使用しているため、非末尾再帰コードの 2 倍以上の時間がかかり、より多くの割り当てを行います!
> #time;;
--> Timing now on
> for i = 0 to 100000000 do ignore (pathToList1 t);;
Real: 00:00:09.002, CPU: 00:00:09.016, GC gen0: 3815, gen1: 1, gen2: 0
val it : unit = ()
> for i = 0 to 100000000 do ignore (pathToList2 t);;
Real: 00:00:21.882, CPU: 00:00:21.871, GC gen0: 12208, gen1: 3, gen2: 1
val it : unit = ()
結論:「末尾再帰にすれば速くなる!」というルール。いくつかの再帰呼び出しを行う必要がある場合は、コードをさらに遅くするようにコードを変更する必要があるため、極端には従わないでください。