web-dev-qa-db-ja.com

Nodeバイナリツリーオーバーフロースタックでの検索

次の方法を使用して、300000レベルの二分木をトラバース*します。

Node* find(int v){
   if(value==v)
      return this;
   else if(right && value<v)
      return right->find(v); 
   else if(left && value>v)
      return left->find(v);
}

ただし、スタックオーバーフローが原因でセグメンテーション違反が発生します。再帰的な関数呼び出しのオーバーヘッドなしで深いツリーをトラバースする方法についてのアイデアはありますか?

*「トラバース」とは、完全なツリートラバーサルではなく、「指定された値を持つノードを検索する」ことを意味します。

18
PerelMan

はい! 300 000レベルのツリーの場合再帰を回避。ツリーをトラバースし、ループを使用して値反復的にを見つけます。

二分探索木表現

           25             // Level 1
        20    36          // Level 2
      10 22  30 40        // Level 3
  .. .. .. .. .. .. .. 
.. .. .. .. .. .. .. ..   // Level n

問題をさらに明確にするためだけに。あなたの木の深さはn = 300.000レベルです。したがって、最悪のシナリオでは、二分探索木(BST)はツリーのノードの[〜#〜] all [〜#〜]にアクセスする必要があります。 最悪の場合にはアルゴリズムがありますO(n)時間計算量なので、これは悪いニュースです。そのような木は持つことができます:

2ˆ300.000ノード= 9.9701e + 90308ノード(約)


9.9701e + 90308ノード指数関数的に大規模アクセスするノードの数です。これらの番号を使用すると、コールスタックがオーバーフローする理由が非常に明確になります。


解決策(反復的な方法):

あなたのNode class/struct宣言は古典的な標準整数であると仮定しています[〜#〜] bst [〜#〜] 1.次に、それを適応させることができ、それは機能します:

struct Node {
    int data;
    Node* right;
    Node* left;
};

Node* find(int v) {
    Node* temp = root;  // temp Node* value copy to not mess up tree structure by changing the root
    while (temp != nullptr) {
        if (temp->data == v) {
            return temp;
        }
        if (v > temp->data) {
            temp = temp->right;
        }
        else {
            temp = temp->left;
        }
    }
    return nullptr;
}

これを採用する反復アプローチは再帰を回避するため、プログラム呼び出しスタックで非常に大きなツリー内の値を再帰的に見つける手間を省くことができます。

27
Santiago Varela

次のノードに設定したNode *タイプの変数がある単純なループで、もう一度ループします...
探している値が存在しない場合を忘れないでください!

9
Rene

呼び出しスタックではなく、ユーザー定義スタックなどを使用して再帰を実装できます。これは、既存の stack テンプレートを介して実行できます。アプローチは、スタックが空になるまで繰り返すwhileループを持つことです。既存の実装では深さ優先探索が使用されているため、再帰呼び出しの排除が見つかります ここ

7
Codor

あなたが持っているツリーが二分探索木であり、あなたがしたいのは特定の値を持つノードを検索することだけである場合、物事は単純です:再帰なし必要な場合は、他の人が指摘しているように、単純なループを使用してそれを行うことができます。

必ずしもバイナリ検索ツリーではないツリーがあり、フルトラバーサルを実行したいというより一般的なケースではそれは、最も簡単な方法は再帰を使用することですが、すでに理解しているように、ツリーが非常に深い場合、再帰は機能しません。

したがって、再帰を回避するには、C++ヒープにスタックを実装する必要があります。元の再帰関数が持っていたローカル変数ごとに1つのメンバーと、元の再帰関数が受け入れたパラメーターごとに1つのメンバーを含む新しいStackElementクラスを宣言する必要があります。 (より少ないメンバー変数で回避できる可能性があります。コードを機能させた後は、それについて心配することができます。)

StackElementのインスタンスをスタックコレクションに格納することも、各インスタンスにその親へのポインタを含めるだけで、自分でスタックを完全に実装することもできます。

したがって、関数がそれ自体を再帰的に呼び出す代わりに、関数は単にループで構成されます。関数は、currentStackElementがツリーのルートノードに関する情報で初期化された状態で、ループに入ります。その親ポインタはnullになります。これは、スタックが空になることを示す別の言い方です。

関数の再帰バージョンがそれ自体を呼び出していたすべての場所で、新しい関数はStackElementの新しいインスタンスを割り当て、それを初期化し、この新しいインスタンスをとして使用してループを繰り返します。 )current要素。

関数の再帰バージョンが戻ってきたすべての場所で、新しい関数はcurrentStackElementを解放し、上に座っていたものをポップしますスタック、それを新しいcurrent要素にし、ループを繰り返します。

探していたノードが見つかったら、ループから抜け出すだけです。

または、既存のツリーのノードがa)「親」ノードへのリンクとb)ユーザーデータ(「訪問済み」フラグを保存できる)をサポートしている場合は、独自のスタックを実装する必要はありません。ツリーをインプレースでトラバースするだけです。ループの各反復で、最初に現在のノードが探していたノードであるかどうかを確認します。そうでない場合は、まだ訪問されていない子供が見つかるまで子供を列挙し、次にそれを訪問します。リーフ、またはすべての子が訪問されたノードに到達したら、親へのリンクをたどってバックトラックします。また、ツリーをトラバースするときにツリーを破棄する自由がある場合は、「ユーザーデータ」の概念も必要ありません。子ノードを使い終わったら、ツリーを解放してnullにします。

6
Mike Nakis

まあ、それは単一の追加のローカル変数といくつかの比較を犠牲にして末尾再帰にすることができます:

Node* find(int v){
  if(value==v)
    return this;
  else if(!right && value<v)
    return NULL;
  else if(!left && value>v)
    return NULL;
  else {
    Node *tmp = NULL;
    if(value<v)
      tmp = right;
    else if(value>v)
      tmp = left;
    return tmp->find(v);
  }
}
3
user2793784

二分木を歩くことは再帰的なプロセスであり、現在いるノードがどこにもポイントしていないことがわかるまで歩き続けます。

それはあなたが適切な基本条件を必要とするということです。次のようなもの:

if (treeNode == NULL)
   return NULL;

一般に、ツリーのトラバースは次のように実行されます(Cの場合)。

void traverse(treeNode *pTree){
  if (pTree==0)
    return;
  printf("%d\n",pTree->nodeData);
  traverse(pTree->leftChild);
  traverse(pTree->rightChild);
}
0
user4063679