コンピュータサイエンスにおいて、ツリー走査(ツリーサーチ、ツリーウォークとも呼ばれる)はグラフ走査の一種であり、ツリーデータ構造内の各ノードを(取得、更新、削除など)正確に一度だけ訪問するプロセスを指します。このような走査は、ノードを訪問する順序によって分類されます。以下のアルゴリズムは二分木を対象としていますが、他のツリーにも一般化できます。
0
走査方法: 1連結リスト、一次元配列、その他の線形データ構造は、通常は線形順序で走査されますが、ツリーは複数の方法で走査できます。深さ優先または幅優先で走査できます。深さ優先で走査する一般的な方法は、順序通り、前順、後順の 3 つです。[ 1 ]これらの基本的な走査方法以外にも、反復深化深さ優先探索のような深さ制限探索など、さまざまな複雑な方法やハイブリッドな方法が可能です。後者は、幅優先探索と同様に、無限ツリーの走査にも使用できます(下記参照)。
ツリーを走査するには、何らかの方法で全てのノードを反復処理する必要があります。あるノードから次のノードは複数存在する可能性があるため(ツリーは線形データ構造ではないため)、逐次計算(並列処理ではない)を前提とすると、一部のノードは後回しにされ、後でアクセスできるように何らかの方法で格納される必要があります。これは通常、スタック(LIFO)またはキュー(FIFO)によって行われます。ツリーは自己参照(再帰的に定義される)データ構造であるため、走査は再帰、あるいはより微妙な共再帰によって、自然かつ明確な方法で定義できます。これらの場合、後回しにされたノードは暗黙的にコールスタックに格納されます。
深さ優先探索はスタックを用いて容易に実装でき、再帰的にも(コールスタックを介して)実装できる一方、幅優先探索はキューを用いて容易に実装でき、共再帰的にも実装できる。[ 2 ]: 45-61
深さ優先探索(DFS)では、次の兄弟ノードに進む前に、探索ツリーを可能な限り深く掘り下げます。
深さ優先探索で二分木を走査するには、各ノードで次の操作が実行されます。[ 3 ] [ 4 ]
トラバーサルのトレースは、ツリーのシーケンシャル化と呼ばれます。トラバーサルのトレースは、訪問した各ノードのリストです。前順、中順、後順のいずれかのシーケンシャル化では、基となるツリーを一意に記述することはできません。異なる要素を持つツリーが与えられた場合、前順または後順と中順の組み合わせのいずれかで、ツリーを一意に記述するのに十分です。ただし、前順と後順の組み合わせでは、ツリー構造にいくらかの曖昧さが残ります。[ 6 ]
ノードに対する走査の位置(図では赤、緑、青)によって、ノードの訪問が行われる方法は3つあります。1つの色だけを選択すると、以下に説明するように、ノードの訪問は1回だけになります。3つの色すべてで訪問すると、同じノードが3回訪問され、「全順序」のシーケンス化が実現します。

先行順走査はトポロジカルソートされた走査である。なぜなら、親ノードは子ノードの処理が完了する前に処理されるからである。
二分探索木では、各ノードのキーがその左部分木内のすべてのキーよりも大きく、右部分木内のすべてのキーよりも小さいように順序付けられているため、順序通りの走査では、キーが昇順で取得されます。[ 7 ]
二分探索木において、各ノードのキーがその左部分木内のすべてのキーよりも大きく、右部分木内のすべてのキーよりも小さいように順序付けられている場合、逆順走査では、キーが降順で取得されます。
深さ優先探索を用いて任意の木構造(必ずしも二分木とは限らない)を走査するには、各ノードで以下の操作を実行します。
扱う問題によっては、前順、後順、特にサブツリーの数-1個のインオーダー操作のいずれかが省略可能な場合があります。また、実際には、前順、後順、インオーダー操作のうち複数が必要になる場合もあります。例えば、三分木に要素を挿入する場合、要素を比較することで前順操作が実行されます。その後、木のバランスを再調整するために後順操作が必要になる場合があります。

幅優先探索(BFS)またはレベル順探索では、次の深さに進む前に、探索木を可能な限り広くします。
深さ優先探索でも幅優先探索でもないツリー探索アルゴリズムも存在する。その一つがモンテカルロツリー探索であり、これは最も有望な手を分析することに重点を置いており、探索空間のランダムサンプリングに基づいて探索ツリーを拡張する。

Pre-order traversal can be used to make a prefix expression (Polish notation) from expression trees: traverse the expression tree pre-orderly. For example, traversing the depicted arithmetic expression in pre-order yields "+ * A − BC + DE". In prefix notation, there is no need for any parentheses as long as each operator has a fixed number of operands. Pre-order traversal is also used to create a copy of the tree.
Post-order traversal can generate a postfix representation (Reverse Polish notation) of a binary tree. Traversing the depicted arithmetic expression in post-order yields "ABC − * DE + +"; the latter can easily be transformed into machine code to evaluate the expression by a stack machine. Post-order traversal is also used to delete the tree. Each node is freed after freeing its children.
In-order traversal is very commonly used on binary search trees because it returns values from the underlying set in order, according to the comparator that set up the binary search tree.
Below are examples of stack-based implementation for pre-order, post-order and in-order traversal in recursive approach (left) as well as iterative approach (right).
Implementations in iterative approach are able to avoid the drawbacks of recursion, particularly limitations of stack space and performance issues.
Several alternative implementations are also mentioned.
ツリーが配列で表現されている場合(最初のインデックスは 0)、次の要素のインデックスを計算することができます。[ 8 ]
手続きbubbleUp(配列、i、葉) k ← 1 i ← (i - 1)/2 while (leaf + 1) % (k * 2) ≠ k i ← (i - 1)/2 k ← 2 * k iを返す手続きpreorder(配列) i ← 0 i ≠ array.size の間 訪問(配列[i]) i = size - 1 の場合 i ← サイズ それ以外の場合、i < size/2の場合 i ← i * 2 + 1 それ以外 葉 ← i - サイズ/2 親 ← bubble_up(配列、i、葉) i ← 親 * 2 + 2
開始する要素は、標準的な検索関数によってnode二分探索木内で見つけることができ、ここでは親ポインタを使用しない実装で示しています。つまり、祖先ポインタを保持するために を使用します。bststack
手続きsearch(bst, key) // (ノード、スタック)を返します ノード ← bst.root stack ←空のスタックwhile node ≠ null stack.push(node) key が node.key と等しい 場合は(node, stack) を返す。 key が node.key より小さい場合は、(node, stack) を返す。 ノード ← ノード.左 それ以外 ノード ← ノード右 return ( null、空のスタック)
関数inorderNext [ 2 ] : 60 はnode、の順序付き隣接ノード、つまり順序付き後続ノード( の場合dir=1) または順序付き前続ノード( の場合dir=0) と更新された を返しますstack。これにより、二分探索木を順次順序付きで走査し、指定された方向にdirさらに探索することができます。
手続きinorderNext(node, dir, stack) newnode ← node.child[dir] newnode ≠ null の場合、 ノード ← 新しいノード stack.push(node) newnode ← node.child[1-dir] newnode がnull になるまで、 (node, stack)を返します。 // ノードにディレクトリの子要素がありません: スタックが 空の場合は、空のスタックを返す。 古いノード ← ノード node ← stack.pop() // oldnode の親 oldnode ≠ node.child[dir]になるまで // 現在 oldnode = node.child[1-dir]、 // つまり、ノードは元のノードの祖先(および前任者/後任者)である。 return (node, stack)
この関数はキーを使用しないため、シーケンス構造は二分探索木のエッジによって完全に記録されることに注意してください。方向を変えない走査の場合、(償却)平均複雑度は完全なトラバーサルにはサイズ BST の手順エッジアップで1ステップ、エッジダウンで1ステップ。最悪の場合の複雑さはと木の高さと同じくらい。
上記の実装はすべて、ツリーの高さに比例したスタック領域を必要とします。再帰的な実装ではコールスタック、反復的な実装では親(祖先)スタックが必要です。バランスの悪いツリーでは、このスタック領域は相当な量になる可能性があります。反復的な実装では、各ノードに親ポインタを保持するか、ツリーをスレッド化することで(次のセクションで説明)、スタックの要件をなくすことができます。
二分木は、すべての左子ポインタ(そうでなければnullになる)をノードの順序通りの先行ノード(存在する場合)に、すべての右子ポインタ(そうでなければnullになる)をノードの順序通りの続ノード(存在する場合)に向けることでスレッド化されます。
利点:
デメリット:
Morrisトラバーサルは、スレッドを使用するインオーダートラバーサルの実装です。[ 9 ]
また、以下に単純なキューベースのレベル順走査の擬似コードを示します。この走査には、指定された深さにおけるノードの最大数に比例したメモリ容量が必要となります。これは、ノード総数の半分程度になる場合もあります。この種の走査において、よりメモリ効率の良い方法は、反復深化型深さ優先探索を用いることで実現できます。
手続きレベル順序(ノード) キュー ←空のキュー queue.enqueue(node) キューが空でない間 ノード ← キュー.dequeue() 訪問(ノード) node.left ≠ nullの場合 queue.enqueue(node.left) node.right がnullでない場合 、queue.enqueue(node.right)を実行する
ツリーが配列(最初のインデックスは0)で表現されている場合、すべての要素を反復処理すれば十分です。
手続きlevelorder(array) for i from 0 to array.size 訪問(配列[i])
走査は通常、有限個のノード(したがって有限の深さと有限の分岐係数)を持つ木に対して行われますが、無限の木に対しても行うことができます。これは関数型プログラミング(特に遅延評価の場合)において特に重要です。無限のデータ構造は、厳密には評価されないものの、容易に定義および操作できることが多いためです。評価には無限の時間がかかるためです。チェスや囲碁のゲームツリーのように、有限の木でも大きすぎて明示的に表現できないものもあるため、それらを無限の木であるかのように解析することが有用です。
走査の基本的な要件は、最終的にすべてのノードを訪問することです。無限ツリーの場合、単純なアルゴリズムではこの要件を満たせないことがよくあります。たとえば、無限の深さを持つ二分木の場合、深さ優先探索はツリーの一方の側(慣例として左側)をたどるだけで、残りの側を訪問することはありません。また、中順走査や後順走査では、葉ノードに到達していないため(そして実際には到達しないため)、どのノードも訪問しません。これに対し、幅優先(レベル順)走査は、無限の深さを持つ二分木を問題なく走査でき、分岐係数が制限されている任意のツリーを走査できます。
一方、深さ2の木において、根ノードが無限に多くの子ノードを持ち、それぞれの子ノードがさらに2つの子ノードを持つ場合、深さ優先探索ではすべてのノードを探索します。これは、あるノードの子ノードの孫ノードをすべて探索し尽くすと、次のノードに進むためです(後順探索でない場合。後順探索では根ノードに到達することはありません)。これに対し、幅優先探索では、まず子ノードをすべて探索するため、孫ノードに到達することはありません。
実行時間のより洗練された分析は、無限順序数によって行うことができます。たとえば、上記の深さ 2 の木の幅優先探索では、ω ·2 ステップかかります。最初のレベルで ω、そして 2 番目のレベルで別の ω です。
したがって、単純な深さ優先探索や幅優先探索では、すべての無限木を網羅することはできず、非常に大きな木では効率的ではありません。しかし、ハイブリッド法では、対角線引数(垂直方向と水平方向の組み合わせである「対角線」は、深さと幅の組み合わせに対応します)を介して、任意の(可算)無限木を網羅することができます。
具体的には、無限に分岐する無限の深さの木が与えられた場合、根を ()、根の子を (1)、(2)、...、孫を (1, 1)、(1, 2)、...、(2, 1)、(2, 2)、...、といったようにラベル付けします。したがって、ノードは、有限個の(空の場合もある)正の数列と1 対 1で対応しており、これらの数は可算であり、まずエントリの合計によって、次に与えられた合計内で辞書式順序によって順序付けできます(与えられた値に合計される数列は有限個しかないため、すべてのエントリに到達できます。形式的には、与えられた自然数の構成は有限個あり、具体的にはn ≥ 1の2 n −1個の構成があります)。これにより、走査が得られます。具体的には、次のようになります。
等
これは、無限深度二分木をこの木にマッピングし、幅優先探索を適用すると解釈できます。親ノードからその2番目以降の子ノードに接続する「下」エッジを、最初の子ノードから2番目の子ノード、2番目の子ノードから3番目の子ノードなどへの「右」エッジに置き換えます。したがって、各ステップで、下に進む(末尾に(、1)を追加する)か、右に進む(最後の番号に1を追加する)かのいずれかになります(ルートノードは例外で、下に進むことしかできません)。これは、無限二分木と上記の番号付けとの対応関係を示しています。エントリの合計(マイナス1)はルートノードからの距離に対応し、これは無限二分木の深さn -1にある2n -1個のノードと一致します(2は二分木に対応します)。