17

Ist es möglich, einen binären Baum in O (1) Hilfsraum (w/o mit einem Stapel, Schlange, etc.) iterieren, oder hat sich diese bewährt unmöglich? Wenn es möglich ist, wie kann es gemacht werden?Iterieren über einen Binary Tree mit O (1) Hilfsraum

Edit: Die Antworten, die ich über diese Möglichkeit erhalten habe, wenn es Zeiger auf Elternknoten gibt, sind interessant und ich wusste nicht, dass dies getan werden könnte, aber abhängig davon, wie Sie es betrachten, kann O sein (n) Hilfsraum Außerdem gibt es in meinem praktischen Anwendungsfall keine Zeiger auf Elternknoten. Bitte gehen Sie von nun an davon aus, wenn Sie antworten.

+2

Ich betrachte Link zu Eltern als O (n) Hilfsraum. Sie sollten Ihre Frage bearbeiten, um explizit zu erwähnen, dass Ihr Binärbaum keine solche Eigenschaft besitzt. –

+1

Wie können Sie keine Zeiger auf Ihre übergeordneten Knoten haben? Wie kannst du ohne sie überhaupt über deinen Baum iterieren? ist das, wo der Stapel hereinkommt? Sie werden Elternzeiger sowieso brauchen, um auszubalancieren. –

Antwort

2

Um den Baum zu erhalten und verwendet nur O (1) Raum ist es möglich, wenn ...

  • jeder Knoten eine feste Größe ist.
  • Der ganze Baum ist in einem zusammenhängenden Teil des Speichers (dh ein Array)
  • Sie nicht über den Baum durchlaufen, durchlaufen Sie einfach über das Array

Oder wenn Sie den Baum zerstören als Sie verarbeiten es ...:

  • @Svante kam mit dieser Idee, aber ich wollte auf wie ein wenig erweitern, einen destruktiven Ansatz.
  • Wie? Sie können die am weitesten links stehenden Blatt Knoten in einem Baum (für (;;) node = node-> links etc ... nehmen fortsetzen, dann verarbeiten, dann löschen. Wenn der linke Knoten im Baum nicht a Blattknoten, dann bearbeiten Sie und löschen Sie die meisten Blattknoten links der rechten Knoten. Wenn ein rechter Knoten keine Kinder hat, dann ist es Sie bearbeiten und löschen.

Ways, dass es nicht funktionieren würde ...

Wenn Sie die Rekursion verwenden, werden Sie einen Stapel implizit verwendet werden. für einige Algorithmen (nicht für dieses Problem) Endrekursion Sie erlauben würde, Rekursion zu verwenden und O (1) Raum, aber da jeder bestimmte Knoten mehrere Kinder hat, und daher gibt es Nach dem rekursiven Aufruf ist O (1) Space Tail Recursion nicht möglich.

Sie könnten versuchen, das Problem 1-Ebene zu einer Zeit, in Angriff zu nehmen, aber es gibt keine Möglichkeit, eine beliebige Ebene der Knoten ohne Hilfs (implizit oder explizit) Raum zuzugreifen. Zum Beispiel könnten Sie rekrutieren, um den gewünschten Knoten zu finden, aber das erfordert impliziten Stack-Platz. Oder Sie könnten alle Ihre Knoten in einer anderen Datenstruktur pro Ebene speichern, was jedoch zusätzlichen Platz benötigt.

+2

Sie liegen falsch, Knuth TAOCP I.2.3.1 Übung 21 bezieht sich auf mindestens drei Algorithmen für das Traversieren von Bäumen in O (1) -Raum, ohne den ursprünglichen Baum zu zerstören (obwohl sie ihn vorübergehend vor Ort modifizieren). –

+0

@nobody_: Ich habe keine Fälle berücksichtigt, in denen Sie den Baum ändern dürfen. Aber ich habe 2 Beispiele gegeben, also sicherlich hat es einige :) Wie auch immer, ich habe meine Antwort geändert, um die ungültigen Teile herauszunehmen. –

+0

Für diejenigen ohne TAOCP bezieht sich @AntonTykhyy auf die Morris Tree Traversal Algorithmen, die in O (n) Zeit und O (1) Hilfsraum laufen. Diese Seite könnte helfen: http://stackoverflow.com/questions/5502916/explain-morris-inorder-tree-traversal-without-using-stacks-or-recursion. Außerdem hat jemand den Morris-Algorithmus vorbestellt. –

6

Es ist möglich, wenn Sie in jedem Kind einen Link zu dem Elternteil haben. Wenn Sie auf ein Kind stoßen, besuchen Sie den linken Teilbaum. Wenn du zurückkommst, überprüfe, ob du das linke Kind deiner Eltern bist. Wenn ja, besuchen Sie den rechten Teilbaum. Andernfalls gehst du weiter, bis du das linke Kind bist oder bis du die Wurzel des Baumes triffst.

In diesem Beispiel wird die Größe des Stapels konstant bleibt, so gibt es keinen zusätzlichen Speicher verbraucht. Natürlich, wie Mehrdad darauf hingewiesen hat, können Links zu den Eltern als O (n) -Raum betrachtet werden, aber dies ist eher eine Eigenschaft des Baumes als eine Eigenschaft des Algorithmus.

Wenn Sie sich nicht um die Reihenfolge kümmern, in der Sie den Baum durchlaufen, können Sie den Knoten mit der Wurzel 1 eine integrale Zuordnung zuweisen, die Kinder der Wurzel sind 2 und 3, deren Kinder 4, 5, 6, 7, usw. Dann durchlaufen Sie jede Reihe des Baumes, indem Sie einen Zähler inkrementieren und auf diesen Knoten mit seinem numerischen Wert zugreifen. Sie können das höchstmögliche Kindelement verfolgen und die Schleife anhalten, wenn Ihr Zähler es passiert. Zeitlich ist dies ein extrem ineffizienter Algorithmus, aber ich denke, es braucht O (1) -Raum.

(lieh ich mir die Idee von Halden Nummerierung. Wenn Sie Knoten N haben, können Sie die Kinder bei 2N und 2N + 1 finden. Sie können von dieser Nummer arbeiten rückwärts, um die Eltern eines Kindes zu finden.)

Hier ist ein Beispiel dieses Algorithmus in Aktion in C. Beachten Sie, dass es keine malloc ist außer für die Schaffung des Baumes ist, und dass es keine rekursiven Funktionen, was bedeutet, dass der Stapel konstant Platz in Anspruch nimmt:

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

typedef struct tree 
{ 
    int value; 
    struct tree *left, *right; 
} tree; 

tree *maketree(int value, tree *left, tree *right) 
{ 
    tree *ret = malloc(sizeof(tree)); 
    ret->value = value; 
    ret->left = left; 
    ret->right = right; 
    return ret; 
} 

int nextstep(int current, int desired) 
{ 
    while (desired > 2*current+1) 
     desired /= 2; 

    return desired % 2; 
} 

tree *seek(tree *root, int desired) 
{ 
    int currentid; currentid = 1; 

    while (currentid != desired) 
    { 
     if (nextstep(currentid, desired)) 
    if (root->right) 
     { 
     currentid = 2*currentid+1; 
     root = root->right; 
     } 
    else 
     return NULL; 
     else 
    if (root->left) 
     { 
     currentid = 2*currentid; 
     root = root->left; 
     } 
    else 
     return NULL; 
    } 
    return root; 
} 


void traverse(tree *root) 
{ 
    int counter; counter = 1; /* main loop counter */ 

    /* next = maximum id of next child; if we pass this, we're done */ 
    int next; next = 1; 

    tree *current; 

    while (next >= counter) 
    { 
     current = seek(root, counter); 
     if (current) 
     { 
      if (current->left || current->right) 
       next = 2*counter+1; 

      /* printing to show we've been here */ 
      printf("%i\n", current->value); 
     } 
     counter++; 
    } 
} 

int main() 
{ 
    tree *root1 = 
    maketree(1, maketree(2, maketree(3, NULL, NULL), 
          maketree(4, NULL, NULL)), 
       maketree(5, maketree(6, NULL, NULL), 
          maketree(7, NULL, NULL))); 

    tree *root2 = 
     maketree(1, maketree(2, maketree(3, 
      maketree(4, NULL, NULL), NULL), NULL), NULL); 

    tree *root3 = 
     maketree(1, NULL, maketree(2, NULL, maketree(3, NULL, 
      maketree(4, NULL, NULL)))); 

    printf("doing root1:\n"); 
    traverse(root1); 

    printf("\ndoing root2:\n"); 
    traverse(root2); 

    printf("\ndoing root3:\n"); 
    traverse(root3); 
} 

Ich entschuldige mich für die Qualität des Codes - das ist weitgehend ein Beweis für das Konzept. Außerdem ist die Laufzeit dieses Algorithmus nicht ideal, da es viel Arbeit erfordert, um zu kompensieren, dass keine Zustandsinformationen aufrechterhalten werden können. Auf der positiven Seite passt dies jedoch zu der Rechnung eines O (1) -Raumalgorithmus für den Zugriff auf die Elemente des Baums in beliebiger Reihenfolge, ohne dass Kind-zu-Eltern-Verbindungen erforderlich sind oder die Struktur des Baums modifiziert wird.

+0

Brian, jedes Bit der Nummer zeigt dir, in welche Richtung du gehen würdest. –

+0

Aber wie würden Sie auf den Knoten # 7 beispielsweise ohne zusätzlichen Speicherplatz zugreifen? Um einen solchen Knoten zu rekursiv zu finden, würde man Platz nehmen, und wenn man alle Knoten in einer anderen Datenstruktur speichert, würde das auch zusätzlichen Speicherplatz bedeuten. –

+0

@Brian Sie können 1 subtrahieren und durch 2 dividieren, um zu sehen, dass 3 die Eltern von 7 ist. Ebenso können Sie 1 subtrahieren und erneut durch 2 dividieren, um zu sehen, dass 1 der Elternteil von 3 ist. Daher benötigen Sie lediglich eine Schleife, die den nächsten Schritt im Pfad zum gewünschten Knoten vom aktuellen Knoten berechnet. –

1

Sie können dies erreichen, wenn Knoten Zeiger auf ihre Eltern haben. Wenn Sie den Baum (mit den Elternzeigern) zurückgehen, übergeben Sie auch den Knoten, von dem Sie kommen. Wenn der Knoten, von dem Sie kommen, das linke Kind des Knotens ist, auf dem Sie sich gerade befinden, dann durchlaufen Sie das rechte Kind. Ansonsten gehst du zurück zu seinem Elternteil.

EDIT als Antwort auf die Bearbeitung in der Frage: Wenn Sie den gesamten Baum durchlaufen möchten, dann ist dies nicht möglich. Um wieder auf den Baum zu klettern, müssen Sie wissen, wohin Sie gehen. Wenn Sie jedoch nur einen einzelnen Pfad durch den Baum durchlaufen möchten, dann kann dies in O (1) zusätzlichen Speicherplatz erreicht werden. Iterieren Sie den Baum einfach mit einer while-Schleife und behalten Sie einen einzelnen Zeiger auf den aktuellen Knoten. Fahren Sie den Baum weiter, bis Sie entweder den gewünschten Knoten finden oder einen Blattknoten treffen.

EDIT: Hier Code für den ersten Algorithmus ist (die iterate_constant_space überprüfen() Funktion und im Vergleich zu den Ergebnissen der Standard iterate() Funktion): Diese

#include <cstdio> 
#include <string> 
using namespace std; 

/* Implementation of a binary search tree. Nodes are ordered by key, but also 
* store some data. 
*/ 
struct BinarySearchTree { 
    int key;  // they key by which nodes are ordered 
    string data; // the data stored in nodes 
    BinarySearchTree *parent, *left, *right; // parent, left and right subtrees 

    /* Initialise the root 
    */ 
    BinarySearchTree(int k, string d, BinarySearchTree *p = NULL) 
    : key(k), data(d), parent(p), left(NULL), right(NULL) {}; 
    /* Insert some data 
    */ 
    void insert(int k, string d); 
    /* Searches for a node with the given key. Returns the corresponding data 
    * if found, otherwise returns None.""" 
    */ 
    string search(int k); 
    void iterate(); 
    void iterate_constant_space(); 
    void visit(); 
}; 

void BinarySearchTree::insert(int k, string d) { 
    if (k <= key) { // Insert into left subtree 
    if (left == NULL) 
     // Left subtree doesn't exist yet, create it 
     left = new BinarySearchTree(k, d, this); 
    else 
     // Left subtree exists, insert into it 
     left->insert(k, d); 
    } else { // Insert into right subtree, similar to above 
    if (right == NULL) 
     right = new BinarySearchTree(k, d, this); 
    else 
     right->insert(k, d); 
    } 
} 

string BinarySearchTree::search(int k) { 
    if (k == key) // Key is in this node 
    return data; 
    else if (k < key && left) // Key would be in left subtree, which exists 
    return left->search(k); // Recursive search 
    else if (k > key && right) 
    return right->search(k); 
    return "NULL"; 
} 

void BinarySearchTree::visit() { 
    printf("Visiting node %d storing data %s\n", key, data.c_str()); 
} 

void BinarySearchTree::iterate() { 
    visit(); 
    if (left) left->iterate(); 
    if (right) right->iterate(); 
} 

void BinarySearchTree::iterate_constant_space() { 
    BinarySearchTree *current = this, *from = NULL; 
    current->visit(); 
    while (current != this || from == NULL) { 
    while (current->left) { 
     current = current->left; 
     current->visit(); 
    } 
    if (current->right) { 
     current = current->right; 
     current->visit(); 
     continue; 
    } 
    from = current; 
    current = current->parent; 
    if (from == current->left) { 
     current = current->right; 
     current->visit(); 
    } else { 
     while (from != current->left && current != this) { 
     from = current; 
     current = current->parent; 
     } 
     if (current == this && from == current->left && current->right) { 
     current = current->right; 
     current->visit(); 
     } 
    } 
    } 
} 

int main() { 
    BinarySearchTree tree(5, "five"); 
    tree.insert(7, "seven"); 
    tree.insert(9, "nine"); 
    tree.insert(1, "one"); 
    tree.insert(2, "two"); 
    printf("%s\n", tree.search(3).c_str()); 
    printf("%s\n", tree.search(1).c_str()); 
    printf("%s\n", tree.search(9).c_str()); 
    // Duplicate keys produce unexpected results 
    tree.insert(7, "second seven"); 
    printf("%s\n", tree.search(7).c_str()); 
    printf("Normal iteration:\n"); 
    tree.iterate(); 
    printf("Constant space iteration:\n"); 
    tree.iterate_constant_space(); 
} 
+0

"Wenn du den Baum hinauf gehst" ... wie kommst du zum Boden jedes Knotens im Baum? –

+0

current = Wurzel; während current-> left exists: current = current-> left – marcog

+0

@marcog: Ja, du kannst sicher zu einem einzigen Knoten kommen .... Aber ich sagte, wie kommst du auf den Boden jedes Knotens im Baum? –

-1

Ich denke, es gibt keine Möglichkeit, die Sie tun können, So wie du die Knoten irgendwo finden solltest, wo du in einem Pfad aufgehört hast und um zu erkennen, dass du immer O (Höhe) Platz brauchst.

4

Sie können es destruktiv tun, Verknüpfung jedes Blatt wie Sie gehen. Dies kann in bestimmten Situationen anwendbar sein, d. H. Wenn Sie den Baum danach nicht mehr benötigen.

Als Erweiterung können Sie einen anderen Binärbaum erstellen, wenn Sie den ersten zerstören. Sie würden etwas Speichermikromanagement benötigen, um sicherzustellen, dass der Spitzenspeicher niemals die Größe des ursprünglichen Baums plus vielleicht eine kleine Konstante überschreitet. Dies würde jedoch ziemlich viel Rechenaufwand verursachen.

EDIT: Es gibt einen Weg! Sie können die Knoten selbst verwenden, um den Weg zurück zum Baum zu beleuchten, indem Sie sie vorübergehend umkehren.Wenn Sie einen Knoten besuchen, zeigen Sie seinen Zeiger left-child auf seinen übergeordneten Zeiger, seinen right-child Zeiger auf die letzte Zeit, die Sie rechts auf Ihrem Pfad (der in Mauszeiger in diesem Moment gefunden wird) war, und speichern Sie es real Kinder entweder im right-child Zeiger des jetzt redundanten Elternteils oder in Ihrem Traversalzustand bzw. der nächste besuchte Kinder left-child Zeiger. Sie müssen einige Zeiger auf den aktuellen Knoten und seine Umgebung beibehalten, aber nicht "nicht lokal". Wenn Sie den Baum wieder erreichen, kehren Sie den Prozess um.

Ich hoffe, ich könnte das irgendwie klar machen; Das ist nur eine grobe Skizze. Du wirst es irgendwo nachschlagen müssen (ich bin mir sicher, dass das irgendwo in The Art of Computer Programming erwähnt wird).

+0

+1, ich kommentierte diese Methode mehr mit einem Hinweis auf Ihre Antwort in meiner Antwort. –

+0

Genial, aber entsetzlich. Habe einfach einen Hochzeiger in jedem Element. Es ist so viel einfacher, weniger schmerzhaft und ist gut verstanden und anerkannt. –

1

Zeiger von Knoten zu ihren Vorfahren können mit keinem (gut, zwei Bits pro Knoten) zusätzlichen Speicher mit einer Struktur namens Threaded Tree haben. In einem Threaded-Tree werden Null-Links durch ein Zustands-Bit und nicht durch einen Null-Zeiger dargestellt. Dann können Sie die Null-Links durch Zeiger auf andere Knoten ersetzen: Links verweisen auf den Nachfolger-Knoten in einem Inorder-Traversal und rechte Links auf den Vorgänger. Hier ist ein Unicode-Schwer Diagramm (X steht für einen Kopfknoten verwendet, um den Baum zu steuern):

 
             ╭─┬────────────────────────────────────────╮ 
    ╭─────────────────────────▶┏━━━┯━━━┯━━▼┓│          │ 
    │      ╭─╂─ │ X │ ─╂╯          │ 
    │      ▼ ┗━━━┷━━━┷━━━┛           │ 
    │     ┏━━━┯━━━┯━━━┓            │ 
    │    ╭────╂─ │ A │ ─╂──╮           │ 
    │    ▼ ┗━━━┷━━━┷━━━┛ │           │  
    │  ┏━━━┯━━━┯━━━┓ ▲   │  ┏━━━┯━━━┯━━━┓      │ 
    │  ╭─╂─ │ B │ ─╂────┤   ├────────╂─ │ C │ ─╂───────╮    │ 
    │  ▼ ┗━━━┷━━━┷━━━┛ │   ▼  ┗━━━┷━━━┷━━━┛  ▼    │ 
    │┏━━━┯━━━┯━━━┓ ▲   │ ┏━━━┯━━━┯━━━┓  ▲   ┏━━━┯━━━┯━━━┓  │ 
    ╰╂─ │ D │ ─╂─╯   ╰───╂ │ E │ ─╂╮  │  ╭╂─ │ F │ ─╂╮  │ 
    ┗━━━┷━━━┷━━━┛    ┗━━━┷━━━┷━━━┛▼  │  ▼┗━━━┷━━━┷━━━┛▼  │ 
            ▲ ┏━━━┯━━━┯━━━┓ │ ┏━━━┯━━━┯━━━┓ ▲ ┏━━━┯━━━┯━━━┓│ 
            ╰─╂─ │ G │ ╂──┴─╂─ │ H │ ─╂─┴─╂─ │ J │ ─╂╯ 
             ┗━━━┷━━━┷━━━┛ ┗━━━┷━━━┷━━━┛ ┗━━━┷━━━┷━━━┛ 

Nachdem Sie die Struktur haben, eine Inorder Traversal tun, ist sehr, sehr einfach:

 
Inorder-Successor(p) 
    p points to a node. This routine finds the successor of p in 
    an inorder traversal and returns a pointer to that node 

    qp.right 
    Ifp.rtag = 0 Then 
     Whileq.ltag = 0 Do 
      qq.left 
     End While 
    End If 

    Returnq 

Viele weitere Informationen über Threaded Threads finden Sie in Kunst der Computerprogrammierung Ch.2 §3.1 oder verstreut im Internet.

+2

Und es ist O (n) Aux Speicher;) –

23

Meine Güte, ich muss es tatsächlich von Knuth eingeben. Diese Lösung ist von Joseph M. Morris [Inf. Proc. Briefe (1979), 197-200]. Soweit ich das beurteilen kann, läuft es in O (NlogN) Zeit.

static void VisitInO1Memory (Node root, Action<Node> preorderVisitor) { 
    Node parent = root ; 
    Node right = null ; 
    Node curr ; 
    while (parent != null) { 
    curr = parent.left ; 
    if (curr != null) { 
     // search for thread 
     while (curr != right && curr.right != null) 
     curr = curr.right ; 

     if (curr != right) { 
     // insert thread 
     assert curr.right == null ; 
     curr.right = parent ; 
     preorderVisitor (parent) ; 
     parent = parent.left ; 
     continue ; 
     } else 
     // remove thread, left subtree of P already traversed 
     // this restores the node to original state 
     curr.right = null ; 
    } else 
     preorderVisitor (parent) ; 

    right = parent ; 
    parent = parent.right ; 
    } 
} 

class Node 
{ 
    public Node left ; 
    public Node right ; 
} 
+0

das zerstört den Baum, oder? –

+5

Nein, es fügt nur vorübergehend Rückreferenzen von bestimmten Blättern hinzu. – Svante

+0

ah, sehr nett :) –

-3

Ist es möglich, über einen Binärbaum in O (1) Hilfsraum zu iterieren.

struct node { node * father, * right, * left; int value; }; 

Diese Struktur ermöglicht es Ihnen, 1-Schritt in jede Richtung durch den Binärbaum zu bewegen.
Aber immer noch in Iteration müssen Sie den Weg halten!

1

„Datenstrukturen und ihre Algorithmen“ von Harry Lewis und Larry beschreibt Denenberg Link Inversion Traversal für konstanten Raum Traversal eines binären Baumes. Dazu benötigen Sie an jedem Knoten keinen Elternzeiger. Die Traversierung verwendet die vorhandenen Zeiger in der Baumstruktur, um den Pfad für die Rückverfolgung zu speichern. 2-3 zusätzliche Knotenreferenzen sind erforderlich. Plus ein bisschen auf jedem Knoten, um die Traversierrichtung (aufwärts oder abwärts) zu verfolgen, während wir uns nach unten bewegen. In meiner Implementierung dieses Algorithmus aus dem Buch zeigt das Profiling, dass diese Traversierung viel weniger Speicher-/Prozessorzeit hat. Eine Implementierung in Java ist here.

+0

Sie benötigen eine zusätzliche Datenstruktur für dieses Bit-Array, das ist kein O (1) -Raum. – shinzou