Binary Tree mit std::unique_ptr



  • Ich habe eine Frage bezüglich dem Design eines Binary Trees, in dem an jedem Knoten ein Datensatz vom Typ T und ein Key vom Typ K gespeichert wird. Wie man den mit rohen Zeigern implementiert ist mir klar. Wenn ich nun statt rohen Zeigern std::unique_ptr verwende bin ich mir nicht ganz sicher ob mein Design gut ist.

    Mich stört es z.B., dass ich in Zeile 24 die get() Funktion brauche und ich trotzdem wieder mit rohen Pointern arbeite. Auf der anderen Seite sind das aber keine "owning" Pointer, soweit ich weiss ist es in Ordnung dies zu machen?

    Ist es eurer Meinung nach hier überhaupt sinnvoll std::unique_ptr zu verwenden? Ich habe Implementierung, die std::shared_ptr verwenden gefunden. Ich finde std::shared_ptr hier allerdings nicht sinnvoll.

    Ich wäre froh, wenn mir jemand ein Feedback zum Design geben könnte (die Implementierung von insert habe ich nur kurz hingeschrieben um zu zeigen, wie ich mit den Pointern umgehe, die ist weder fertig noch genauer durchdacht, es geht mir nur konzeptuell darum std::unique_ptr richtig zu verwenden 😉 ) Ansonsten nicht mit Kritik sparen 🙂

    Hier der Code:

    template <typename T, typename K>
    struct Node
    {
        Node(T d, K k) : data(d), key(k) { }
    
        T data;
        K key;
        std::unique_ptr<Node> left;
        std::unique_ptr<Node> right;
    };
    
    template <typename T, typename K>
    class BinTree
    {
    public:
        void insert(T t, K key);
    private:
        std::unique_ptr<Node<T,K>> root;
    };
    
    template <typename T, typename K>
    void BinTree<T,K>::insert(T t, K key)
    {
    	Node<T,K> *current = root.get();
    	if ( !current )
    	{
    		root.reset(new Node<T,K>(t,key));
    		return;
    	}
    
    	while ( true )
    	{
    		if ( key < current->key && !current->left )
    		{
    			current->left.reset(new Node<T,K>(t,key));
    			return;
    		}
    		else if ( current->key < key && !current->right )
    		{
    			current->right.reset(new Node<T,K>(t,key));
    			return;
    		}
    
    		current = key < current->key ? current->left.get() : current->right.get();
    	}
    }
    


  • es sollte auch so funktionieren:

    if ( !root )
    {
        root.reset(new Node<T,K>(t,key));
        return;
    }
    


  • firefly schrieb:

    es sollte auch so funktionieren:

    if ( !root )
    {
        root.reset(new Node<T,K>(t,key));
        return;
    }
    

    Ja, aber was wenn root != nullptr ? Dann muss ich mit left oder right weitermachen und spätestens dann brauche ich einen rohen Zeiger (siehe while-Schleife).



  • An nicht besitzenden rohen Zeigern ist nichts falsch. Wobei ich hier einen Zeiger auf einen unique_ptr genommen hätte.

    template <typename T, typename K>
    void BinTree<T,K>::insert(T t, K key)
    {
        auto *current = &root;
        while ( current )
        {
            if ( key < (*current)->key )
                current = &(*current)->left;
            else if ( (*current)->key < key )
                current = &(*current)->right;
        }
        *current = make_unique<Node<T,K> >(t,key);
    }
    

    Der Algorithmus schreit aber danach, rekursiv implementiert zu werden.



  • rohzeiger schrieb:

    Wobei ich hier einen Zeiger auf einen unique_ptr genommen hätte.

    👍 Da hätte ich selber drauf kommen sollen 🙄

    rohzeiger schrieb:

    Der Algorithmus schreit aber danach, rekursiv implementiert zu werden.

    Ja, aber rekursiv ist halt meistens langsamer als iterativ. Auf der anderen Seite nimmt man aber auch keinen Binären Baum (ohne Balancierung etc.), wenn man eine schnelle Datenstruktur will 🙂

    Danke für den Tipp!



  • Man kann bestimmt mit swap was machen.

    void BinTree<T,K>::insert(T t, K key){
        auto newNode = make_unique<Node<T,K>>(t, key);
        Node<T,K> *current = root.get();
        if (!current){
            swap(root, newNode);
            return;
        }
    
        while (true){
            if (key < current->key && !current->left){
                swap(current->left, newNode);
                return;
            }
            else if (current->key < key && !current->right){
                swap(current->right, newNode);
                return;
            }
            current = key < current->key ? current->left.get() : current->right.get();
        }
    }
    

    Zumindest das Balancing und Löschen wird damit bestimmt weniger umständlich, bei insert kommt der Vorteil nicht so richtig raus.



  • rohzeiger schrieb:

    Der Algorithmus schreit aber danach, rekursiv implementiert zu werden.

    Nein. Er hat sogar ziemlich viel Angst davor.



  • volkard schrieb:

    rohzeiger schrieb:

    Der Algorithmus schreit aber danach, rekursiv implementiert zu werden.

    Nein. Er hat sogar ziemlich viel Angst davor.

    Ohne Begründung ist die Aussage nix wert...

    Wieso sollte eine rekursive Implementierung schlechter sein als die iterative?
    Weil in jedem Rekursionsschritt eine temporäre Variable erzeugt wird?
    Weil der Stack durch zu viele Rekursionsschritte schnell voll wird?



  • Skym0sh0 schrieb:

    volkard schrieb:

    rohzeiger schrieb:

    Der Algorithmus schreit aber danach, rekursiv implementiert zu werden.

    Nein. Er hat sogar ziemlich viel Angst davor.

    Ohne Begründung ist die Aussage nix wert...

    rohzeiger hatte auc nix begründet.

    Skym0sh0 schrieb:

    Wieso sollte eine rekursive Implementierung schlechter sein als die iterative?
    Weil in jedem Rekursionsschritt eine temporäre Variable erzeugt wird?
    Weil der Stack durch zu viele Rekursionsschritte schnell voll wird?

    Ah, Du kennst die Antwort ja schon. Noch war nämlich noch keine Rede vom ausbalancieren.



  • Naja, für Rekursion spricht die Einfachheit des Algorithmus'. Eine Baumtraversierung oder Suche ist rekursiv implementiert nicht mehr als 'n kompakter Dreizeiler.

    Iterativ sind das mehr als 5 😉 😃

    Ok, wenn man von einem unbalanciertem Baum ausgeht, der ordentlich voll wird, dann kann der Stack schonmal voll werden.

    Aber OP will den Umgang mit unique_ptr lernen und implementiert den Baum nur zur Übung. Da wäre Rekursion intuitiv durchaus nachvollziehbar.



  • volkard schrieb:

    Skym0sh0 schrieb:

    Wieso sollte eine rekursive Implementierung schlechter sein als die iterative?
    Weil in jedem Rekursionsschritt eine temporäre Variable erzeugt wird?
    Weil der Stack durch zu viele Rekursionsschritte schnell voll wird?

    Ah, Du kennst die Antwort ja schon. Noch war nämlich noch keine Rede vom ausbalancieren.

    Also mein Compiler erkennt, dass das tailrekursiv ist.

    template <typename T>
    struct bintree {
      struct node {
        node(T key) : key(key) {}
        T key;
        std::unique_ptr<node> left, right;
    
        void insert(T x) {
          if (x < key)
            if (left)
              left->insert(x);
            else
              left = make_unique<node>(x);
          else
            if (right)
              right->insert(x);
            else
              right = make_unique<node>(x);
        }
      };
      std::unique_ptr<node> root;
    
      void insert(T key) {
        if (root)
          root->insert(key);
        else
          root = make_unique<node>(key);
      }
    };
    

    Und ganz allgemein ist bei Bäumen ist rekursiv die verständlichste Art, daher sehe ich keinen Grund, das anders zu machen ausser man hat einen gutes Argument.



  • rohzeiger schrieb:

    Also mein Compiler erkennt, dass das tailrekursiv ist.

    Würde mich ungern davon abhängig machen. Also will nicht, daß es vom Optimieren abhängt, ob das Programm überhaupt läuft. Ich will auch nicht riskieren, daß nach jeder kleinen Codeänderung vielleicht der Optimierer die Endrekursion nicht mehr erkennt.

    rohzeiger schrieb:

    template <typename T>
        void insert(T x) {
          if (x < key)
            if (left)
              left->insert(x);
            else
              left = make_unique<node>(x);
          else
            if (right)
              right->insert(x);
            else
              right = make_unique<node>(x);
        }
      };
      void insert(T key) {
        if (root)
          root->insert(key);
        else
          root = make_unique<node>(key);
      }
    };
    

    rohzeiger schrieb:

    template <typename T, typename K>
    void BinTree<T,K>::insert(T t, K key)
    {
        auto *current = &root;
        while ( current ) //oder ähnlich
        {
            if ( key < (*current)->key )
                current = &(*current)->left;
            else if ( (*current)->key < key )
                current = &(*current)->right;
        }
        *current = make_unique<Node<T,K> >(t,key);
    }
    

    Aha. Das ist jetzt aber seltsam. Ist iterativ am Ende doch einfacher? Oh, das darf nicht sein. Jetzt müssen wir schnell eine tolle Begründung finden.
    Ach, übrigens, Du hast rekursiv die Funktion in zwei kleinere zerhackt. Sollte man faiererweise iterativ auch machen. Eine, die den Zeiger auf die Kante sicht, wo der neue Knoten drangemacht werden würde, und eine, die dann schlicht dranmacht. Keine Sonderbahenadling des ersten Knotens mehr und diese Suchfunktion kann auch für find() mitverwendet werden.

    rohzeiger schrieb:

    Und ganz allgemein ist bei Bäumen ist rekursiv die verständlichste Art,

    Das ist ja wohl mal Oberquatsch. Dir fallen offenbar beide Versionen recht leicht. Du postulierst also wohl, was Du meinst, was für einen hypothetischen Anfänger wohl am verständlichsten wäre. Und ich sage, daß die Anfänger gar nicht behindert sind.

    rohzeiger schrieb:

    daher sehe ich keinen Grund, das anders zu machen ausser man hat einen gutes Argument.

    Ich sehe generell keinen Grund, etwas rekursiv zu machen, außer man hat einen guten Grund. Ein guter Grund dafür ist, daß das Problem schon rekursiv gegeben ist und es keine triviale iterative Implementierung gibt (Parser). Nicht aber der Baum, obwohl man ihn, als rekursiv definiert auffassen könnte, wenn die iterative Version doch so nah ist. Ein guter Grund dagegen ist, wenn die Rekursionstiefe nicht wenigstens logarithmisch beschränkt ist (Anfänger-Lehrbuch-Quicksort).



  • Skym0sh0 schrieb:

    Naja, für Rekursion spricht die Einfachheit des Algorithmus'.

    Ja, WENN die Rekursive Version deutlich einfacher ist.

    Skym0sh0 schrieb:

    Eine Baumtraversierung oder Suche ist rekursiv implementiert nicht mehr als 'n kompakter Dreizeiler.

    Bei der Traversierung gebe ich Dir recht, daß die iterativ nicht hübsch ist.


Log in to reply