再帰、スタック、またはキューなしでツリーをたどることができますか?


15

半年前、私はデータ構造クラスに座っていました。そこでは、再帰、スタック、キューなど(または他の同様のデータ構造)といくつかのポインターを使用せずに誰もがツリーを横断できる場合、教授は追加のクレジットを提供しました。私はその質問に対する明白な答えだと思ったものを思いついたが、それは教授によって最終的に受け入れられた。私は同じ学部の別の教授と一緒に離散数学クラスに座っていました。彼は、再帰、スタック、キューなどなしでツリーを横断することは不可能であり、私の解決策は無効であると主張しました。

それで、それは可能ですか、不可能ですか?なぜですか?

編集:明確化を追加するために、3つの要素(各ノードに保存されたデータと2つの子へのポインター)を持つバイナリツリーにこれを実装しました。私のソリューションは、わずかな変更を加えるだけでn項ツリーに拡張できます。

私のデータ構造の教師は、ツリーの変更に対して何の制約も課していませんでした。実際、彼自身の解決策は、子ポインターを使用してツリーを下に向けることであることがわかりました。私の離散数学教授は、ツリーの突然変異は、ツリーの数学的な定義によるとツリーではなくなったことを意味し、彼の定義は親へのポインタも排除することを言った-これは上記で解決した場合と一致します。


3
制約を指定する必要があります。ツリーを変異させることはできますか?ツリーはどのように表されますか?(たとえば、各ノードにはその親への親ポインターがありますか?)答えは特定の制約に依存します。これらの制約を指定しないと、これは適切な問題ではありません。
DW

2
教授が本当に表明したかった制約は、「O1追加スペースあり」だったと思います。しかし、とにかくあなたの解決策は何でしたか?
ラファエル

回答:


17

この領域での多くの研究はドームであり、ガベージコレクションのコンテキストでツリーと一般的なリスト構造を「安く」トラバースする方法に動機付けられています。

スレッド化されたバイナリツリーは、ツリー内の後続ノードへのリンクにいくつかのnilポインタが使用されるバイナリツリーの適応表現です。この追加情報を使用して、スタックなしでツリーを走査できます。ただし、スレッドを子ポインターと区別するには、ノードごとに余分なビットが必要です。ウィキペディア:Tree_traversal

私の知る限り、通常の方法(ノードごとの左右のポインター)でポインターを使用して実装されたバイナリツリーは、Morrisに帰属するメソッドでスレッドのメソッドを使用して走査できます。NILポインターは一時的に再利用され、パスをルートに戻します。賢い部分は、トラバーサル中に、元のエッジと一時的なスレッドリンクを区別できることです(ツリー内でサイクルを形成する方法を使用)。

良い部分:追加のデータ構造はありません。悪い部分:わずかに不正行為。スタックは巧妙な方法でツリー内にあります。非常に賢い。

P. MatetiおよびR. Manghirmalani:Morris's Tree Traversal Algorithm Reconsidered DOI:10.1016 / 0167-6423(88)90063-9に、隠されたスタックの証明が示されています。

JM Morris:二分木を簡単かつ安価にたどります。IPL 9(1979)197-200 DOI:10.1016 / 0020-0190(79)90068-1

次に、Lindstromスキャンもあります。このメソッドは、各ノードに関連する3つのポインター(親と2つの子)を「回転」させます。適切な事前注文または事後注文アルゴリズムを実行する場合、ノードごとに追加のビットが必要です。すべてのノードにアクセスしたい場合(3回ですが、どの訪問を実行したかわからない場合)、ビットなしで実行できます。

G. Lindstrom:スタックやタグビットのないリスト構造をスキャンします。IPL 2(1973)47-51。DOI:10.1016 / 0020-0190(73)90012-4

おそらく最も簡単な方法は、ロブソンによる方法です。ここでは、古典的なアルゴリズムに必要なスタックがリーフに通されています。

JM Robson:補助スタックIPL 1(1973)149-152なしで二分木を横断するための改善されたアルゴリズム。10.1016 / 0020-0190(73)90018-5

IPL =情報処理レター


私もこのソリューションが好きですが、コンピューターサイエンスの授業の最初の1年で思いついたことは何もありませんでした。ええ、おそらく私の教授のルールに従って不正行為をしています。
NL -モニカに謝罪

2
戦略のリンク/参照を提供できますか?
ラファエル

1
この方法の本当の悪い点は、一度に複数のトラバーサルを実行できないことです。
ジル 'SO-悪であるのをやめる'

6

各ノードには、その親(ルートでない限り)と最初の子(ある場合)へのポインターがあり、その子にはその次の兄弟(ある場合)へのポインターがあると思います。これで、お気に入りの走査順序をシミュレートできます。次のノードを選択するルールを考える必要があります。たとえば、ポストオーダーをシミュレートするとします。最初のノードは「一番左の子孫」です。これは、ルートから開始し、最初の子に繰り返し移動することで取得できます。ここで、ノードいると仮定します。次の兄弟がいる場合、その兄弟の左端の子孫を出力します。次の兄弟がいない場合は、親を出力します。親がいない場合は完了です。v


これは、問題を提案したデータ構造教授が解決に使用したソリューションに似ています。離散数学の教授は、親へのポインターがある場合、「これはツリーではなくグラフになった」と反対した。
NL -モニカに謝罪

@NathanLiddle:それは使用したツリー定義に依存します(定義していません)。「現実の世界」では、グラフ理論が彼が定義するものがもちろん木ではないと言う場合でも、Yuvalのツリー表現は合理的です。
ラファエル

@Raphaelはい、元の教授の要件を満たしているので、受け入れられる答えです。
NL -モニカに謝罪

0

私の解決策は、ネストされたforループを使用してツリーをブルートフォースする、ブリストスファーストのトラバースです。これは決して効率的ではなく、実際、ツリーのような再帰的なデータ構造は再帰的なトラバースを求めていますが、問題はツリーを効率的にトラバースできるかどうかではなく、それが可能かどうかでした。

Pseudocode:
root = pointer root 
depth = integer 0
finished = bool false
//If we n-ary tree also track how many children have been found 
//on the node with the most children for the purposes of this psuedocode 
//we'll assume a binary tree and insert a magic number of 2 so that we 
//can use bitwise operators instead of integer division 
while(!finished)
    ++depth
    treePosition = pointer root
    finished = true;
    for i := 0..2**depth
        for j := 0..depth
            if (i & j) //bitwise operator explained below
                // if right child doesn't exist break the loop
                treePosition = treePosition.rightChild
            else
                // if left child doesn't exist break the loop
                treePosition = treePosition.leftChild
        if j has any children
            finished = false
            do anything else you want when visiting the node

ご覧のように、最初のいくつかのレベルでは次のようになります。擬似コードのビット演算子は、単純にバイナリツリーの左または右のターンを決定します。

2**1       0               1
2**2   00      01      10      11
2**3 000 001 010 011 100 101 110 111

n-aryの場合、i%(maxChildren ** j)/ jを使用して、0〜maxChildrenの間でどのパスを取るかを決定します。

n-aryの各ノードで、子の数がmaxChildrenより大きいかどうかを確認し、適切に更新する必要もあります。


バイナリ以上を使用したい場合は、マジックナンバー2を、それが見た最大の子に一致するようにインクリメントされる変数に置き換える必要があり、ビットごとの演算子の代わりに、同じ変数で除算する必要がありますあなたがいた木の深さの力。
NL-モニカに謝罪

O1OlgnO1OnO1たとえばdepth、幅を超える場合はどうしますintか?
DW

DW、問題を提起した教授は問題にその制約を課しませんでした、そして、離散数学教授との私の議論について私を悩ませたのは、再帰、スタック、または、コストに関係なくキュー。私の解決策を示すことが唯一のものは、それはあなたがなど、スタック、キュー、のためのオプションを削除しても、再帰的に行うことができ、反復可能なDOのものであるということです
NL -モニカに謝罪

O(1)の追加スペースがないと解決できないと言うこともありますが、再帰、スタック、またはキューなしで問題を解決できないと宣言することはまったく別です。実際、私のコードを見た後、離散数学教授は、最初のforループで「i」がキューの代わりになっていると言ったので、まだポイントを認めていません。頑固な人はどうですか?
NL -モニカに謝罪

1
idepthdepthΘnΘniO1
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.