再帰を使用せずにツリーを横断するにはどうすればよいですか?


19

メモリノードツリーが非常に大きいため、ツリーを走査する必要があります。各子ノードの戻り値を親ノードに渡します。これは、すべてのノードのデータがルートノードまでバブルするまで実行する必要があります。

トラバーサルはこのように機能します。

private Data Execute(Node pNode)
{
    Data[] values = new Data[pNode.Children.Count];
    for(int i=0; i < pNode.Children.Count; i++)
    {
        values[i] = Execute(pNode.Children[i]);  // recursive
    }
    return pNode.Process(values);
}

public void Start(Node pRoot)
{
    Data result = Execute(pRoot);
}

これは正常に機能しますが、コールスタックがノードツリーのサイズを制限するのではないかと心配しています。

への再帰呼び出しExecuteが行われないように、どのようにコードを書き直すことができますか?


8
ノードを追跡するために独自のスタックを維持するか、ツリーの形状を変更する必要があります。参照stackoverflow.com/q/5496464stackoverflow.com/q/4581576
ロバート・ハーヴェイ

1
このGoogle検索、特にMorris Traversalでも多くの助けを見つけました。
ロバートハーヴェイ14年

@RobertHarveyはRobに感謝します。これがどの条件に当てはまるかわかりませんでした。
Reactgular 14年

2
計算を行うと、メモリ要件に驚くかもしれません。たとえば、完全にバランスの取れたテラノードバイナリツリーには、40エントリのスタックしか必要ありません。
カールビーレフェルト14年

@KarlBielefeldtただし、ツリーは完全にバランスが取れていると仮定しています。バランスの取れていないツリーをモデリングする必要がある場合があり、その場合、スタックを爆破するのは非常に簡単です。
セルビー14年

回答:


27

再帰を使用しない汎用ツリートラバーサルの実装を次に示します。

public static IEnumerable<T> Traverse<T>(T item, Func<T, IEnumerable<T>> childSelector)
{
    var stack = new Stack<T>();
    stack.Push(item);
    while (stack.Any())
    {
        var next = stack.Pop();
        yield return next;
        foreach (var child in childSelector(next))
            stack.Push(child);
    }
}

あなたの場合、次のように呼び出すことができます:

IEnumerable<Node> allNodes = Traverse(pRoot, node => node.Children);

使用するQueue代わりにStack検索し、最初の呼吸のための最初の、というよりも深さを。PriorityQueue最高の最初の検索に使用します。


これはツリーをコレクションにフラット化するだけだと思う​​のは正しいですか?
Reactgular 14年

1
@MathewFoscariniはい、それが目的です。もちろん、必ずしも実際のコレクションに具体化する必要はありません。それは単なるシーケンスです。データセット全体をメモリにプルする必要なく、それを反復処理してデータをストリーミングできます。
セルビー14年

私はそれが問題を解決するとは思わない。
Reactgular 14年

4
彼は、検索のような独立した操作を実行するグ​​ラフを横断するだけでなく、子ノードからデータを集約しています。ツリーをフラット化すると、集計を実行するために必要な構造情報が破壊されます。
カールビーレフェルト14年

1
参考までに、これはこの質問をグーグルで探しているほとんどの人が探している正しい答えだと思います。+1
アンデルスアルピ

4

事前にツリーの深さの推定値がある場合は、スタックサイズを調整するだけで十分でしょうか?バージョン2.0以降のC#では、新しいスレッドを開始するたびにこれが可能です。こちらを参照してください。

http://www.atalasoft.com/cs/blogs/rickm/archive/2008/04/22/increasing-the-size-of-your-stack-net-memory-management-part-3.aspx

これにより、より複雑なものを実装することなく、再帰的なコードを保持できます。もちろん、独自のスタックを使用して非再帰的なソリューションを作成すると、時間とメモリの効率が向上する場合がありますが、コードは今ほど単純ではないはずです。


簡単なテストを行いました。私のマシンでは、stackoverflowに達する前に14000の再帰呼び出しを行うことができました。ツリーのバランスが取れている場合、40億のノードを保存するために必要な呼び出しは32だけです。各ノードは、(それが習慣)1バイトである場合は、高さ32のバランスのとれたツリーを格納するために4 GBのRAMを取る
エスベンSkovのPedersenの

スタック内の14000コールすべてを使用することにしました。各ノードは1バイト(それは文句を言わない)であれば、ツリーは2.6×10 ^ 4214バイトまでかかるだろう
エスベンSkovがペダーセン

-3

再帰を使用せずにツリー構造のデータ構造を横断することはできません。言語で提供されるスタックフレームと関数呼び出しを使用しない場合、基本的に独自のスタックと関数呼び出しをプログラムする必要があります。コンパイラの作成者がプログラムを実行するマシンで行ったよりも効率的な方法で言語内でそれを行うことはできません。

したがって、リソースの制限に達する恐れがあるため、再帰を回避することは通常誤った方向に導かれます。確かに、時期尚早なリソース最適化は常に誤った方向に導かれますが、この場合、メモリ使用量がボトルネックであることを測定して確認したとしても、レベルを落とさなければ改善できないでしょう。コンパイラライター。


2
これは単純に偽です。再帰を使用せずにツリーをトラバースすることは、ほぼ確実に可能です。それも難しいことではありません。特定のトラバーサルに必要なだけの情報を明示的なスタックに含めることができるため、再帰を使用すると、多くの場合実際に必要な情報よりも多くの情報を格納するため、非常に効率的に、非常に簡単に行うことができますケース。
セルビー14年

2
この論争は時々ここで起こります。自分のスタックを再帰しないと考えるポスターもあれば、ランタイムが暗黙的に行うのと同じことを明示的に行っているだけだと指摘するポスターもあります。このような定義について議論する意味はありません。
キリアンフォス14年

では、再帰をどのように定義しますか?独自の定義内でそれ自体を呼び出す関数として定義します。私の答えで示したように、これを実行することなくツリーをたどることができます。
セルビー14年

2
このような高い担当者のスコアを持っている人に下票をクリックする行為を楽しんでいるのは悪ですか?このウェブサイトでは、これは非常にまれな喜びです。
Reactgular

2
@Matに来てください、それは子供のものです。深すぎる木を爆撃することを恐れている場合、それは合理的な懸念です。あなたはそう言うことができます。
マイクダンラベイ14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.