18

ウィキペディアを引用:

従来のバイナリ ツリー データ構造を使用してバイナリ ヒープを実装することはまったく問題ありません。アルゴリズムで解決 できる要素を追加するときに、バイナリ ヒープの最後のレベルで隣接する要素を見つけることに問題があります...

そのようなアルゴリズムがどのように機能するかについてのアイデアはありますか?

ほとんどのバイナリ ヒープは配列を使用して実装されているため、この問題に関する情報は見つかりませんでした。

どんな助けでも感謝します。


最近、OpenID アカウントを登録しましたが、最初の投稿やコメントの回答を編集できません。そのため、この回答で回答しています。申し訳ありません。


ミッチ・ウィートの引用:

@Yse:「バイナリヒープの最後の要素を見つけるにはどうすればよいですか」という質問はありますか?

はい、そうです。または、より正確に言うと、私の質問は、「非配列ベースのバイナリ ヒープの最後の要素を見つけるにはどうすればよいですか?」です。

Suppressingfire の引用:

この質問をしている文脈はありますか?(つまり、解決しようとしている具体的な問題はありますか?)

上記のとおり、ノードの挿入と削除に必要な「非配列ベースのバイナリヒープの最後の要素を見つける」ための良い方法を知りたいです。

ロイの引用:

通常のバイナリ ツリー構造 ([data, pLeftChild, pRightChild] として定義された pRoot と Node を使用) を使用し、2 つの追加ポインター (pInsertionNode と pLastNode) を追加するのが最も理解しやすいようです。pInsertionNode と pLastNode は両方とも、挿入サブルーチンと削除サブルーチン中に更新され、構造内のデータが変更されたときにそれらを最新の状態に保ちます。これにより、構造体の挿入ポイントと最後のノードの両方に O(1) アクセスできます。

はい、これでうまくいくはずです。私が間違っていなければ、削除/挿入のために場所が別のサブツリーに変更されたときに、挿入ノードと最後のノードを見つけるのが少し難しいかもしれません。しかし、私はこれを試してみます。

ザック・スクリヴェナの引用:

深さ優先検索を実行するのはどうですか...

はい、これは良いアプローチです。私もそれを試してみます。

それでも、最後のノードと挿入ポイントの位置を「計算」する方法があるかどうか疑問に思っています。N 個のノードを持つバイナリ ヒープの高さは、N よりも大きい最小の 2 のべき乗の (2 を底とする) 対数を取ることで計算できます。おそらく、最も深いレベルのノード数も計算できます。次に、挿入ポイントまたは削除用のノードに到達するためにヒープをどのようにトラバースする必要があるかを判断することができた可能性があります。

4

6 に答える 6

21

基本的に、引用されたステートメントは、ヒープへのデータ要素の挿入およびヒープからの削除の場所を解決する問題を指します。バイナリ ヒープの「形状プロパティ」を維持するには、ヒープの最下位レベルを常に左から右に埋めて、空のノードを残さないようにする必要があります。バイナリ ヒープの平均 O(1) 挿入および削除時間を維持するには、次の挿入の場所と、ルート ノードの削除に使用する最下位レベルの最後のノードの場所を特定できる必要があります。一定時間で。

配列に格納されたバイナリ ヒープ (ウィキペディアのエントリで説明されているように、暗黙的で圧縮されたデータ構造を持つ) の場合、これは簡単です。配列の最後に最新のデータ メンバーを挿入し、それを所定の位置に "バブル" します (ヒープ ルールに従います)。または、ルートを配列の最後の要素に置き換えて、削除のために「バブリングダウン」します。配列ストレージのヒープの場合、ヒープ内の要素の数は、次のデータ要素が挿入される場所と、削除に使用する最後の要素を見つける場所への暗黙的なポインターです。

ツリー構造に格納されたバイナリ ヒープの場合、この情報は明らかではありませんが、完全なバイナリ ツリーであるため、計算することができます。たとえば、4 つの要素を持つ完全なバイナリ ツリーでは、挿入ポイントは常にルート ノードの左の子の右の子になります。削除に使用するノードは、常にルート ノードの左の子の左の子になります。また、特定の任意のツリー サイズに対して、ツリーは常に明確に定義された挿入ポイントと削除ポイントを持つ特定の形状を持ちます。ツリーは任意のサイズの特定の構造を持つ「完全なバイナリ ツリー」であるため、O(1) 時間で挿入/削除の場所を計算することが非常に可能です。ただし、問題は、ノードが構造的にどこにあるかを知っていても、ノードがメモリ内のどこにあるかがわからないことです。そう、O(log n) プロセスである特定のノードに到達するには、ツリーをトラバースする必要があります。これは、すべての挿入と削除を最小の O(log n) にし、通常望ましい O(1) 動作を破ります。すべての検索 (「深さ優先」またはその他の検索) は、トラバーサルの問題が指摘されているため、少なくとも O(log n) になり、半ソートされたヒープのランダムな性質のために、通常は O(n) になります。

秘訣は、データ構造を拡張する (ウィキペディアの記事で言及されているように、ツリーを「スレッド化」する) か、追加のポインターを使用することによって、これらの挿入/削除ポイントを一定時間内に計算および参照できるようにすることです。

少ないメモリと追加のコーディング オーバーヘッドで、最も理解しやすいと思われる実装は、通常の単純なバイナリ ツリー構造を使用することです ([data, pParent, pLeftChild, pRightChild] として定義された pRoot と Node を使用)。 2 つのポインター (pInsert と pLastNode) を追加します。pInsert と pLastNode は両方とも、挿入サブルーチンと削除サブルーチン中に更新され、構造内のデータが変更されたときにそれらを最新の状態に保ちます。この実装は、挿入ポイントと構造の最後のノードの両方に O(1) アクセスを提供し、挿入と削除の両方で全体的な O(1) 動作を維持できるようにする必要があります。実装のコストは、2 つの追加ポインターと、挿入/削除サブルーチンでのマイナーな追加コードです (別名、最小)。

EDIT : O(1) insert() の疑似コードを追加

以下は、平均して O(1) である挿入サブルーチンの擬似コードです。

define Node = [T data, *pParent, *pLeft, *pRight]

void insert(T data)
{
    do_insertion( data );   // do insertion, update count of data items in tree

    # assume: pInsert points node location of the tree that where insertion just took place
    #   (aka, either shuffle only data during the insertion or keep pInsert updated during the bubble process)

    int N = this->CountOfDataItems + 1;     # note: CountOfDataItems will always be > 0 (and pRoot != null) after an insertion

    p = new Node( <null>, null, null, null);        // new empty node for the next insertion

    # update pInsert (three cases to handle)
    if ( int(log2(N)) == log2(N) )
        {# #1 - N is an exact power of two
        # O(log2(N))
        # tree is currently a full complete binary tree ("perfect")
        # ... must start a new lower level
        # traverse from pRoot down tree thru each pLeft until empty pLeft is found for insertion
        pInsert = pRoot;
        while (pInsert->pLeft != null) { pInsert = pInsert->pLeft; }    # log2(N) iterations
        p->pParent = pInsert;
        pInsert->pLeft = p;
        }
    else if ( isEven(N) )
        {# #2 - N is even (and NOT a power of 2)
        # O(1)
        p->pParent = pInsert->pParent;
        pInsert->pParent->pRight = p;
        }
    else 
        {# #3 - N is odd
        # O(1)
        p->pParent = pInsert->pParent->pParent->pRight;
        pInsert->pParent->pParent->pRight->pLeft = p;
        }
    pInsert = p;

    // update pLastNode
    // ... [similar process]
}

したがって、insert(T) は平均で O(1) です。O(log N) のときにツリーを 1 レベル増やす必要がある場合を除いて、すべての場合で正確に O(1) です。削除)。別のポインター (pLeftmostLeaf) を追加すると、insert() がすべてのケースで O(1) になり、完全なバイナリ ツリーで挿入と削除が交互に繰り返されるという病的なケースを回避できます。(pLeftmost の追加は演習として残します [かなり簡単です]。)

于 2009-02-01T07:58:26.823 に答える
9

初めてスタックオーバーフローに参加しました。

はい、Zach Scrivena による上記の回答 (他の人を適切に参照する方法がわかりません、申し訳ありません) は正しいです。私が追加したいのは、ノードの数が与えられている場合の単純化された方法です。

基本的な考え方は次のとおりです。

この完全なバイナリ ツリーのノード数 N を指定して、"N % 2" の計算を実行し、結果をスタックにプッシュします。N == 1 になるまで計算を続けます。その後、結果をポップアウトします。結果は 1 が右、0 が左を意味します。シーケンスは、ルートからターゲット位置までのルートです。

例:

ツリーには現在 10 個のノードがあります。位置 11 に別のノードを挿入したいのですが、どのようにルーティングしますか?

11 % 2 = 1  --> right    (the quotient is 5, and push right into stack)
 5 % 2 = 1  --> right    (the quotient is 2, and push right into stack)
 2 % 2 = 0  --> left     (the quotient is 1, and push left into stack. End)

次に、スタックをポップします: 左 -> 右 -> 右。これはルートからのパスです。

于 2016-02-04T04:13:50.013 に答える
7

バイナリ ヒープのサイズのバイナリ表現を使用して、O(log N) 内の最後のノードの場所を見つけることができます。サイズを保存してインクリメントすると、O(1) 時間かかります。この背後にある基本的な概念は、バイナリ ツリーの構造です。

ヒープ サイズが 7 だとします。7 のバイナリ表現は「111」です。ここで、常に最初のビットを省略することを忘れないでください。これで「11」が残りました。左から右に読んでください。ビットは「1」なので、ルート ノードの右の子に移動します。次に、左の文字列は「1」で、最初のビットは「1」です。そのため、現在のノードの右の子に再び移動します。処理するビットがなくなったので、これは最後のノードに到達したことを示しています。したがって、プロセスの生の作業は、ヒープのサイズをビットに変換することです。最初のビットを省略します。左端のビットに従って、'1' の場合は現在のノードの右の子に移動し、'0' の場合は現在のノードの左の子に移動します。

いつもバイナリ ツリーの最後まで行っているので、この操作には常に O(log N) の時間がかかります。これは、最後のノードを見つけるための簡単で正確な手順です。

初読では理解できないかもしれません。Binary Heap のさまざまな値について、紙の上でこの方法を試してみてください。その背後にある直感が得られると確信しています。この知識はあなたの問題を解決するのに十分であると確信しています.

私の答えがお役に立てば幸いです。☺

于 2015-02-08T11:31:42.340 に答える
1

深さ優先探索を実行して、右の子の前に左の子を訪問して、木の高さを決定するのはどうですか。その後、最初に遭遇する葉の深さが短くなるか、子が欠落している親は、「バブリング」する前に新しいノードを配置する場所を示します。


上記の深さ優先探索(DFS)アプローチは、ツリー内のノードの総数がわかっていることを前提とはしていません。この情報が利用できる場合は、完全な二分木のプロパティを利用して、目的の場所にすばやく「ズームイン」できます。

Nをツリー内のノードの総数、Hをツリーの高さとします。

( NH )のいくつかの値は、(1,0)、(2,1)、(3,1)、(4,2)、...、(7,2)、(8、3)です。この2つに関連する一般式は、H = ceil [log2(N +1)]-1です。ここで、Nのみが与えられた場合、最小のステップ数で、ルートから新しいノードの位置までトラバースします。 「バックトラック」なし。最初に、高さH = ceil [log2(N +1)] -1、つまりM = 2 ^(H +1)-1の完全な二分木でノードの総数Mを計算します。

N == Mの場合、ツリーは完全であり、新しいノードを新しいレベルに追加する必要があります。これは、最初のリーフに到達するまで、DFS(左から右)を実行できることを意味します。新しいノードがこのリーフの左の子になります。物語の終わり。

ただし、N < Mの場合、ツリーの最後のレベルにまだ空席があり、新しいノードを左端の空いている場所に追加する必要があります。ツリーの最後のレベルにすでにあるノードの数はちょうど(N -2 ^ H + 1)です。これは、新しいノードが最後のレベルで左からスポットX =(N -2 ^ H + 2)を取ることを意味します。

ここで、ルートからそこに到達するには、各レベルで正しいターン(L対R)を行って、最後のレベルのスポットXに到達するようにする必要があります。実際には、各レベルで少し計算してターンを決定します。ただし、次の表は、算術演算に惑わされることなく、全体像と関連するパターンを示していると思います(これは、一様分布の算術符号化の形式として認識できます)。

0 0 0 0 0 X 0 0 <--- represents the last level in our tree, X marks the spot!
          ^
L L L L R R R R <--- at level 0, proceed to the R child
L L R R L L R R <--- at level 1, proceed to the L child
L R L R L R L R <--- at level 2, proceed to the R child 
          ^                      (which is the position of the new node)
          this column tells us
          if we should proceed to the L or R child at each level

編集:ツリー内のノードの総数がわかっていると仮定して、最短のステップ数で新しいノードに到達する方法についての説明を追加しました。

于 2009-02-01T05:01:32.343 に答える
0

親への参照がない場合の解決策!!! 次のノードの適切な場所を見つけるには、3 つのケースを処理する必要があります

  • ケース (1) ツリー レベルが完了 Log2(N)
  • ケース (2) ツリー ノード数が偶数
  • case (3) ツリーのノード数が奇数

入れる:

void Insert(Node root,Node n)
{
Node parent = findRequiredParentToInsertNewNode (root);
if(parent.left == null)
parent.left = n;
else
parent.right = n;
}

挿入するためにノードの親を見つけます

void findRequiredParentToInsertNewNode(Node root){

Node last = findLastNode(root);

 //Case 1 
 if(2*Math.Pow(levelNumber) == NodeCount){
     while(root.left != null)
      root=root.left;
  return root;
 }
 //Case 2
 else if(Even(N)){
   Node n =findParentOfLastNode(root ,findParentOfLastNode(root ,last));
 return n.right;
 }
//Case 3
 else if(Odd(N)){
  Node n =findParentOfLastNode(root ,last);
 return n;
 }

}

最後のノードを見つけるには、BFS (幅優先検索) を実行し、キュー内の最後の要素を取得する必要があります

 Node findLastNode(Node root)
 {
     if (root.left == nil)
         return root

     Queue q = new Queue();

     q.enqueue(root);
     Node n = null;

     while(!q.isEmpty()){
      n = q.dequeue();
     if ( n.left != null )
         q.enqueue(n.left);
     if ( n.right != null )
         q.enqueue(n.right);
        }
   return n;
}

削除の場合にルートに置き換える場合にノードを null に設定するために、最後のノードの親を検索します

 Node findParentOfLastNode(Node root ,Node lastNode)
{
    if(root == null)
        return root;

    if( root.left == lastNode || root.right == lastNode )
        return root;

    Node n1= findParentOfLastNode(root.left,lastNode);
    Node n2= findParentOfLastNode(root.left,lastNode);

    return n1 != null ? n1 : n2;
}
于 2013-07-31T01:20:58.473 に答える