web-dev-qa-db-ja.com

二分木をリンクリストに変換します。幅優先、一定のストレージ/破壊的

これは宿題ではないので、答える必要はありませんが、今は夢中になっています:)

問題は:

  • 幅優先で、バイナリツリーをリンクリストに破壊的に平坦化するアルゴリズムを設計します。さて、簡単です。キューを作成して、必要なことを実行するだけです。
  • それがウォームアップでした。ここで、定数ストレージを使用して実装します(再帰は、それを使用して答えを理解できる場合、定数ではなく対数ストレージです)。

約1年前にインターネットでこの問題の解決策を見つけましたが、今は忘れてしまいました。知りたいです:)

私が覚えている限り、その秘訣は、アルゴリズムの破壊的な性質を利用して、ツリーを使用してキューを実装することでした。リストをリンクすると、アイテムもキューにプッシュされます。

これを解決しようとするたびに(次のノードをリンクしたり、キューに追加したりするたびに)ノードが失われたり、追加のストレージが必要になったり、複雑な方法を理解できなくなったりします。必要なポインタを持つノード。

その元の記事/投稿へのリンクでさえ私に役立つでしょう:)グーグルは私に喜びを与えていません。

編集:

Jérémieは、親ポインタがあれば、かなり単純な(そしてよく知られている答え)があると指摘しました。私は今、彼が親ポインターを含む元の解決策について正しいと思いますが、私は本当にそれなしで問題を解決したかったです:)

洗練された要件では、ノードに次の定義を使用します。

struct tree_node
{
  int value;
  tree_node* left;
  tree_node* right;
};
18

アイデア:

ツリーの左側の子に沿ってリンクリストを作成できます。同時に、そのリストの適切な子は、末尾に挿入する次のサブツリーのキューを保持するために使用されます。


擬似コードのアルゴリズム:

編集:わかりやすくするために書き直しました

  • ノードには、値、左の子への参照、および右の子への参照の3つのコンポーネントがあります。参照はnullである可能性があります。
  • そのようなノードの二分木を単一のリンクリストに変換する関数
    • ルートノードへの参照をパラメータrootとして受け取り、
    • ループ、tailroot:[.____の左の子から始まります。]
      • tailの左の子をrootの右の子と交換します。
      • ループ(O(n)キュー挿入)。bubble-torootから始まりbubble-fromは常にbubble-to、[の左の子です。 ____。]
        • bubble-toの右の子を「bubble-from」の右の子と交換します。
        • bubble-tobubble-fromを左の子供に進めます。
        • bubble-fromtailに達するまで、
      • tailを左の子に進め、
      • tailはnullではありません。
    • 最後に、headを返します。単一のリンクリストが左側の子に沿って実行されるようになりました。

開始ツリー(完全なツリーである必要があると思います。ノードが欠落している場合は、最後から実行する必要があります。他の場合に意味を与えることもできますが、それにはいくつかの異なる可能性があり、それにはいじくり回す):

              1
          /       \
      2               3
    /   \           /   \
  4       5       6       7
 / \     / \     / \     / \
8   9   A   B   C   D   E   F

これらは必ずしもノードの値ではなく、表示のために番号が付けられているだけであることに注意してください。

1回の反復後のツリー(1から3に形成されるリストと、4および5をルートとするサブツリーのキューに注意してください):

                head
                  v
                  1
               /    \
    tail    2         4
      v  /    \      / \
      3         5   8   9
    /   \      / \
  6       7   A   B
 / \     / \
C   D   E   F

3回の反復後(リストは1から5になり、キューは6から9をルートとするサブツリーを保持します):

                   head
                     v
                     1
                  /     \
               2          6
             /   \       / \
           3       7    C   D
          / \     / \
         4   8   E   F
        / \
tail > 5   9
      / \
     A   B

そして8回の反復後(ほぼ完了):

                       head
                         v
                         1
                        / \
                       2   B
                      / \
                     3   C
                    / \
                   4   D
                  / \
                 5   E
                / \
               6   F
              /
             7
            /
           8
          /
         9
        /
tail > A

リアルコード(LISP)

これはノードクラスです:

(defclass node ()
  ((left :accessor left)
   (right :accessor right)
   (value :accessor value)))

便利なヘルパー:

(defmacro swap (a b)
  `(psetf ,a ,b
          ,b ,a))

変換関数(編集:わかりやすくするために書き直されました):

(defun flatten-complete-tree (root)
  (loop for tail = (and root (left root)) then (left tail)
        while tail
        do (swap (left tail) (right root))
           (loop for bubble-to = root then (left bubble-to)
                 for bubble-from = (left bubble-to)
                 until (eq bubble-from tail)
                 do (swap (right bubble-to) (right bubble-from))))
  root)

ギザギザの木の不可逆操作:

上記を書き直して、任意のギザギザの木を許可しました。ただし、これから元のツリーを再構築することはできません。

(defun flatten-tree (root)

;;ここでの内側のループは、headをまだ平坦化されていないサブツリーのルートに保持します。
;;つまり、最初のブランチのノード、
;;同時に、ルートの分岐していないレベルから左側にすべてアイロンをかけます。
;;ツリーが完全に平坦化されると、最終的にnilになります。

  (loop for head = (loop for x = (or head root) then (left x)
                         do (when (and x (null (left x)))
                              (swap (left x) (right x)))
                         until (or (null x) (right x))
                         finally (return x))
        for tail = (and head (left head)) then (left tail)
        while head
        do (swap (left tail) (right head))

;;この内側のループはO(n)キューの挿入です

           (loop for bubble-to = head then (left bubble-to)
                 for bubble-from = (left bubble-to)
                 until (eq bubble-from tail)
                 do (swap (right bubble-to) (right bubble-from))))

;;最後に、元のルートを返します。

  root)
22
Svante

念のために言っておきますが、再帰バージョンは美しいです(これはC#です):

[編集:st0leが指摘しているように、私の最初のバージョンには 'new'sが含まれています!許してください、私は過去20年間、宣言型言語でコーディングしてきました。これが修正されたバージョンです。]

[編集:ダブルラット。これは幅優先ではありません。]

public static Tree<T> Flatten(Tree<T> t, Tree<T> u = null)
{
    if (t == null) return u;
    t.R = Flatten(t.L, Flatten(t.R, u));
    t.L = null;
    return t;
}
5
Rafe

まず、ノードに親を指す「親」フィールドがあると仮定します。それか、ツリー内で上に戻るにはスタックが必要です(したがって、そのO(1)補助メモリ要件)を達成することはできません)。

反復とO(1)スペースの両方である、よく知られている順序どおりの反復があります。たとえば、アイテムを順番に印刷したいとします。基本的に、二分木では、任意の時点で、任意のノードで、上(親)、左(左の子)、または右のいずれに移動するかを決定する必要があります。このアルゴリズムのアイデアは、この決定に基づいて行うことです。あなたの出身地:

  1. 親から降りてきた場合は、明らかに初めてノードにアクセスしているので、左に移動します。
  2. 左の子から出てきた場合は、現在のノードよりも小さいすべてのノードにアクセスしたので、現在のノードを印刷して(ここではノードを順番に印刷することを忘れないでください)、右に移動します。
  3. 最後に、適切な子から出てきた場合は、この特定のノードをルートとするサブツリー全体にアクセスしたことを意味するため、親にバックアップできます。

OK:これがベースに使用するアルゴリズムです。さて、明らかに、これをリンクリストに破壊的に変更する場合は、ノードにアクセスしなくなったときにのみノードを変更する必要があります。それはあなたが右から来ているときです。その時点で、そのノードの後継が何になるかを決定する必要があります...?

さて、あなたは常に2つのポインタを追跡する必要があります:1つはあなたが訪問した最小のノードへ、もう1つはあなたが現在ルート化されたサブツリーで訪問した最大のノードへ。正しい子から来たときに最後にアクセスしたノードの後継として最小のものを使用し、別の場所から来た最後にアクセスしたノードの後継として最大のものを使用します。これは、ノードに正しい子がないためです。

編集1:二分木の「親」フィールドがリンクリストの「次の」フィールドになると暗黙のうちに考えると言うのを忘れました- -それが最後のステップで変更したものです。

編集3:私の答えの次の部分は、元の質問に完全には答えていないことが判明しました(ただし、前の部分はまだ適切です)。


編集2:左回転をいくつかのコードに使用するという私の提案を明示するというSvanteの理解できる願いに従って:

#include <stdlib.h>
#include <stdio.h>

typedef struct node *node;

struct node
{
  int value;
  node left;
  node right;
};

node new_node(int value, node left, node right)
{
    node n = (node) malloc(sizeof(struct node));
    n->value = value; n->left = left; n->right = right;
    return n;
}

int rotate_right(node tree)
{
    if(tree != NULL && tree->left != NULL)
    {
        node
            a = tree->left->left,
            b = tree->left->right,
            c = tree->right;
        int tmp = tree->value;
        tree->value = tree->left->value;
        tree->left->value = tmp;

        tree->left->left = b;
        tree->left->right = c;
        tree->right = tree->left;

        tree->left = a;
        return 1;
    }
    return 0;
}

回転機能はエレガントではありませんが、混乱しやすいので、 ウィキペディアの回転に関する記事 で使用されているのとまったく同じ名前を付けようとしました。ノードA、B、Cは、私のコードではそのように名前が付けられています。ノードPとQはそうではなく、ポインターのポインターを使用しないことを選択したので、代わりにPとQの値を切り替えるトリックに頼りました--- inlieuof場所を切り替える。 「rotation_right」を自由に使用できるため、変換アルゴリズムは非常に単純です。

void convert_to_list(node tree)
{
    if(tree != NULL) {
        while(rotate_right(tree) != 0);
        convert_to_list(tree->right);
    }
}

結果のツリーはソートされたリンクリストです。リストのnextポインターは、以前はrightポインターでした。木。最後に、テストプログラムは次のとおりです。

int main()
{
    node t =
        new_node(4,
              new_node(2, NULL, new_node(3, NULL, NULL)),
              new_node(8, new_node(5, NULL, NULL), NULL));
    convert_to_list(t);
    for(; t != NULL; t = t->right)
        printf("%d, ", t->value);
    return 0;
}
2
Jérémie

さて、私は今これがこの状況でどのように役立つかを完全に理解することはできませんが、それはあなたにリードを与えるかもしれません。ポインタを格納するためにスタック/キューを使用せずにツリーを繰り返しトラバースするために使用される「ポインタ反転」と呼ばれる手法があります。これは主に、メモリのオーバーヘッドが少ないガベージコレクタに使用されていました。この背後にある考え方は、ノードの子にトラバースするときに、その子へのポインタを親にリンクして、そのノードで終了したときにどこに戻るかがわかるようにすることです。これにより、通常スタック/キューに保持するトレースバック情報がツリー自体に埋め込まれるようになります。

私は次のことを見つけました slideshow 例を挙げます(残念ながら、グーグルにはこれ以上良いものはありません)。ここでの例は、追加のストレージなしでバイナリツリーをトラバースする方法を示しています。

1
smichak

最初に説明したメソッドを使用した単純なJava実装があります。

http://www.dsalgo.com/BinaryTreeToLinkedList.php

このソリューションについてのワット

temp=root;
struct node*getleftmost(struct node* somenode)
{
   while(somenode->left)
   somenode=somenode->left;
   return somenode;
}

 while(temp)
 {
 if(temp->right){
 (getletfmost(temp))->left=temp->right;
 temp->right=NULL;}
 temp=temp->left;
 }
0
Sree Ram

親ポインタは必要ないと思います。レベル0からk-1に加えてセンチネルノードが、レベルkのノードを指す右の子を持つ左の子ポインターの単一リンクリストに帰納的に変換されたとします。リストを繰り返し処理し、各「右の子」(レベルkノード)を順番に取得してリストの最後に挿入し、間もなく上書きされる左の子が付属していた右のポインターを上書きします。リストの最初の最後に到達すると、帰納的仮説をk +1に拡張しました。

編集:コード

#include <cstdio>

struct TreeNode {
  int value;
  TreeNode *left;
  TreeNode *right;
};

// for simplicity, complete binary trees with 2^k - 1 nodes only
void Flatten(TreeNode *root) {
  TreeNode sentinel;
  sentinel.right = root;
  TreeNode *tail = &sentinel;
  while (true) {
    TreeNode *p = &sentinel;
    TreeNode *old_tail = tail;
    while (true) {
      if ((tail->left = p->right) == NULL) {
        return;
      }
      tail = p->right;
      p->right = p->right->left;
      if (p == old_tail) {
        break;
      }
      p = p->left;
    }
  }
}

int main() {
  const int n = 31;
  static TreeNode node[1 + n];
  for (int i = 1; i <= n; ++i) {
    node[i].value = i;
    if (i % 2 == 0) {
      node[i / 2].left = &node[i];
    } else {
      node[i / 2].right = &node[i];
    }
  }
  Flatten(&node[1]);
  for (TreeNode *p = &node[1]; p != NULL; p = p->left) {
    printf("%3d", p->value);
  }
  printf("\n");
}
0
user382751

これが問題に対する私のアプローチです。

struct TreeNode
{
    TreeNode(int in) : data(in)
    {
        left = nullptr;
        right = nullptr;
    }
    int data;
    TreeNode* left;
    TreeNode* right;
};


//Converts left pointer to prev , right pointer to next
// A tree which is like              5 
//                                 11  12

//will be converted to double linked list like 5 -> 12 -> 11 
void finalize(TreeNode* current, TreeNode* parent)
{
    if (parent == nullptr)
    {
        current->left = nullptr;
        return;
    }

    if (parent->left == current)
    {
        if (parent->right == nullptr)
        {
            parent->right = current;
            current->left = parent;
        }
        current->left = parent->right;
    }
    else
    {
        current->left = parent;
        parent->right = current;
        current->right = parent->left;
    }
}


void traverser(TreeNode* current, TreeNode* parent)
{
    if (current->left != nullptr)
        traverser(current->left, current);
    if (current->right != nullptr)
        traverser(current->right, current);

    finalize(current, parent);
}

void start(TreeNode* head)
{
    if (head == nullptr || (head->left == nullptr && head->right == nullptr))
        return;

    traverser(head, nullptr);
}


int main()
{
    TreeNode* n1 = new TreeNode(5);
    TreeNode* n2 = new TreeNode(11);
    TreeNode* n3 = new TreeNode(12);



    n1->left = n2;
    n1->right = n3;

    start(n1);
}
0

これが私の答えです。右側にツリーを構築していますが、これはSvanteのソリューション(!)と同じアプローチであることがわかりました。

記録のために、ここにC#コードがあります:

// Flatten a tree into place in BFS order using O(1) space and O(n) time.
// Here is an example of the transformation (the top-row indicates the
// flattened parts of the tree.
//  
//  a
//  |---.
//  b   c
//  |-. |-.
//  d e f g
//  
//  becomes
//  
//  a-b-c
//  | | |-.
//  d e f g
//  
//  becomes
//  
//  a-b-c-d-e-f-g
//  
public static void FlattenBFS(Tree<T> t)
{
    var a = t; // We "append" newly flattened vertices after 'a'.
    var done = (t == null);
    while (!done)
    {
        done = true;
        var z = a; // This is the last vertex in the flattened part of the tree.
        var i = t;
        while (true)
        {
            if (i.L != null)
            {
                var iL = i.L;
                var iLL = iL.L;
                var iLR = iL.R;
                var aR = a.R;
                i.L = iLL;
                a.R = iL;
                iL.L = iLR;
                iL.R = aR;
                a = iL;
                done &= (iLL == null) & (iLR == null);
            }
            if (i == z)
            {
                break; // We've flattened this level of the tree.
            }
            i = i.R;
        }
        a = (a.R ?? a); // The a.R item should also be considered flattened.
    }
}
0
Rafe