Implementación de un iterador sobre un árbol de búsqueda binario


He estado codificando un montón de diferentes implementaciones de árbol de búsqueda binaria recientemente (AVL, splay, treap) y tengo curiosidad si hay una forma particularmente "buena" de escribir un iterador para atravesar estas estructuras. La solución que he utilizado ahora es tener cada nodo en el almacén BST punteros a los elementos siguiente y anterior en el árbol, lo que reduce la iteración a una iteración de lista vinculada estándar. Sin embargo, no estoy realmente satisfecho con esta respuesta. Aumenta el uso de espacio de cada uno nodo por dos punteros (siguiente y anterior), y en cierto sentido es solo hacer trampa.

Conozco una forma de construir un iterador de árbol de búsqueda binario que usa O(h) espacio de almacenamiento auxiliar (donde h es la altura del árbol) mediante el uso de una pila para realizar un seguimiento de los nodos de frontera para explorar más adelante, pero me he resistido a codificar esto debido al uso de memoria. Esperaba que hubiera alguna manera de construir un iterador que use solo espacio constante.

Mi pregunta es esta: ¿hay una manera de ¿diseñar un iterador sobre un árbol de búsqueda binario con las siguientes propiedades?

  1. Los elementos se visitan en orden ascendente (es decir, un recorrido por orden)
  2. next() y hasNext() las consultas se ejecutan en tiempo O(1).
  3. El uso de memoria es O (1)

Para hacerlo más fácil, está bien si asumes que la estructura del árbol no está cambiando de forma durante la iteración (es decir, sin inserciones, eliminaciones o rotaciones), pero sería realmente genial si hubiera una solución que pudiera manejar este.

Author: Lii, 2011-01-03

8 answers

El iterador más simple posible almacena la última clave vista, y luego en la siguiente iteración, busca en el árbol el límite superior mínimo para esa clave. La iteración es O (log n). Esto tiene la ventaja de ser muy simple. Si las claves son pequeñas, entonces los iteradores también son pequeños. por supuesto, tiene la desventaja de ser una forma relativamente lenta de iterar a través del árbol. Tampoco funcionará para secuencias no únicas.

Algunos árboles utilizan exactamente la implementación que ya utiliza, porque es importante para su uso específico que el escaneo sea muy rápido. Si el número de claves en cada nodo es grande, entonces la penalización de almacenar punteros hermanos no es demasiado onerosa. La mayoría de los árboles B usan este método.

Muchas implementaciones de árbol de búsqueda mantienen un puntero padre en cada nodo para simplificar otras operaciones. Si tiene eso, entonces puede usar un simple puntero al último nodo visto como el estado de su iterador. en cada iteración, se busca el siguiente hijo en el último nodo visto padre. si no hay más hermanos, entonces subes un nivel más.

Si ninguna de estas técnicas le conviene, puede utilizar una pila de nodos, almacenados en el iterador. Esto sirve a la misma función que la pila de llamadas a funciones cuando se itera a través del árbol de búsqueda como es normal, pero en lugar de hacer un bucle entre hermanos y repetir sobre hijos, se empujan hijos a la pila y se devuelve cada hermano sucesivo.

 30
Author: SingleNegationElimination,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-03 02:25:05

Como TokenMacGuy mencionó, puede usar una pila almacenada en el iterador. Aquí hay una implementación rápida probada de esto en Java:

/**
 * An iterator that iterates through a tree using in-order tree traversal
 * allowing a sorted sequence.
 *
 */
public class Iterator {

    private Stack<Node> stack = new Stack<>();
    private Node current;

    private Iterator(Node argRoot) {
        current = argRoot;
    }

    public Node next() {
        while (current != null) {
            stack.push(current);
            current = current.left;
        }

        current = stack.pop();
        Node node = current;
        current = current.right;

        return node;
    }

    public boolean hasNext() {
        return (!stack.isEmpty() || current != null);
    }

    public static Iterator iterator(Node root) {
        return new Iterator(root);
    }
}

Otra variación sería atravesar el árbol en el tiempo de construcción y guardar el recorrido en una lista. Puede usar el iterador de lista después.

 15
Author: user1712376,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-14 03:55:22

Ok, sé que esto es viejo, pero me preguntaron esto en una entrevista con Microsoft hace un tiempo y decidí trabajar en ello un poco. He probado esto y funciona bastante bien.

template <typename E>
class BSTIterator
{  
  BSTNode<E> * m_curNode;
  std::stack<BSTNode<E>*> m_recurseIter;

public:
    BSTIterator( BSTNode<E> * binTree )
    {       
        BSTNode<E>* root = binTree;

        while(root != NULL)
        {
            m_recurseIter.push(root);
            root = root->GetLeft();
        }

        if(m_recurseIter.size() > 0)
        {
            m_curNode = m_recurseIter.top();
            m_recurseIter.pop();
        }
        else
            m_curNode = NULL;
    }

    BSTNode<E> & operator*() { return *m_curNode; }

    bool operator==(const BSTIterator<E>& other)
    {
        return m_curNode == other.m_curNode;
    }

    bool operator!=(const BSTIterator<E>& other)
    {
        return !(*this == other);
    }

    BSTIterator<E> & operator++() 
    { 
        if(m_curNode->GetRight())
        {
            m_recurseIter.push(m_curNode->GetRight());

            if(m_curNode->GetRight()->GetLeft())
                m_recurseIter.push(m_curNode->GetRight()->GetLeft());
        }

        if( m_recurseIter.size() == 0)
        {
            m_curNode = NULL;
            return *this;
        }       

        m_curNode = m_recurseIter.top();
        m_recurseIter.pop();

        return *this;       
    }

    BSTIterator<E> operator++ ( int )
    {
        BSTIterator<E> cpy = *this;     

        if(m_curNode->GetRight())
        {
            m_recurseIter.push(m_curNode->GetRight());

            if(m_curNode->GetRight()->GetLeft())
                m_recurseIter.push(m_curNode->GetRight()->GetLeft());
        }

        if( m_recurseIter.size() == 0)
        {
            m_curNode = NULL;
            return *this;
        }       

        m_curNode = m_recurseIter.top();
        m_recurseIter.pop();

        return cpy;
    }

};
 3
Author: Jonathan Henson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-27 23:43:52

Tree traversal , de Wikipedia:

Todas las implementaciones de ejemplo requerirán un espacio de pila de llamadas proporcional a la altura del árbol. En un árbol mal equilibrado, esto puede ser bastante considerable.

Podemos eliminar el requisito de pila manteniendo los punteros padre en cada nodo, o enhebrando el árbol. En el caso de usar subprocesos, esto permitirá un recorrido de orden mucho mejor, aunque recuperando el nodo padre requerido para el preorden y postorder traversal será más lento que un simple algoritmo basado en pila.

En el artículo hay algún pseudocódigo para iteración con estado O(1), que se puede adaptar fácilmente a un iterador.

 1
Author: Giuseppe Ottaviano,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-03 02:09:47

Qué tal usar una técnica de búsqueda de profundidad primero. El objeto iterador solo debe tener una pila de los nodos ya visitados.

 0
Author: Diego Vélez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-21 21:02:06

Si usas stack, solo alcanzas "Uso Extra de memoria O(h), h es la altura del árbol". Sin embargo, si desea utilizar solo O(1) memoria adicional, debe grabar el Aquí están los análisis: - Si el nodo actual tiene el hijo correcto: buscar min del sub árbol correcto - El nodo actual no tiene un hijo correcto, debe buscarlo desde la raíz y seguir actualizando su ancestro más bajo, que es su siguiente nodo más bajo

public class Solution {
           //@param root: The root of binary tree.

           TreeNode current;
           TreeNode root;
           TreeNode rightMost;
           public Solution(TreeNode root) {

               if(root==null) return;
                this.root = root;
                current = findMin(root);
                rightMost = findMax(root);
           }

           //@return: True if there has next node, or false
           public boolean hasNext() {

           if(current!=null && rightMost!=null && current.val<=rightMost.val)    return true; 
        else return false;
           }
           //O(1) memory.
           public TreeNode next() {
                //1. if current has right child: find min of right sub tree
                TreeNode tep = current;
                current = updateNext();
                return tep;
            }
            public TreeNode updateNext(){
                if(!hasNext()) return null;
                 if(current.right!=null) return findMin(current.right);
                //2. current has no right child
                //if cur < root , go left; otherwise, go right

                    int curVal = current.val;
                    TreeNode post = null;
                    TreeNode tepRoot = root;
                    while(tepRoot!=null){
                      if(curVal<tepRoot.val){
                          post = tepRoot;
                          tepRoot = tepRoot.left;
                      }else if(curVal>tepRoot.val){
                          tepRoot = tepRoot.right;
                      }else {
                          current = post;
                          break;
                      }
                    }
                    return post;

            }

           public TreeNode findMin(TreeNode node){
               while(node.left!=null){
                   node = node.left;
               }
               return node;
           }

            public TreeNode findMax(TreeNode node){
               while(node.right!=null){
                   node = node.right;
               }
               return node;
           }
       }
 0
Author: flora liu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-24 23:41:48

Use espacio O(1), lo que significa que no usaremos pila O(h).

Para comenzar:

  1. HasNext ()? actual.val

  2. Buscar min vía left-most: Siempre podemos buscar left-most para encontrar el siguiente valor mínimo.

  3. Una vez más a la izquierda se marca min (nombre current). El próximo minuto serán 2 casos: Si es actual.¡Vale!= null, podemos seguir buscando current.la derecha es el niño más izquierdo, como el próximo minuto. O, necesitamos mira hacia atrás para los padres. Utilice el árbol de búsqueda binario para encontrar el nodo padre de current.

Nota : al hacer la búsqueda binaria para padre, asegúrese de que satisface padre.izquierda = corriente.

Porque:Si padre.derecha = = actual, ese padre debe haber sido visitado antes. En el árbol de búsqueda binario, conocemos a ese padre.val

public class BSTIterator {
    public TreeNode root;
    public TreeNode current;
    public TreeNode endNode;
    //@param root: The root of binary tree.
    public BSTIterator(TreeNode root) {
        if (root == null) {
            return;
        }
        this.root = root;
        this.current = root;
        this.endNode = root;

        while (endNode != null && endNode.right != null) {
            endNode = endNode.right;
        }
        while (current != null && current.left != null) {
            current = current.left;
        }
    }

    //@return: True if there has next node, or false
    public boolean hasNext() {
        return current != null && current.val <= endNode.val;
    }

    //@return: return next node
    public TreeNode next() {
        TreeNode rst = current;
        //current node has right child
        if (current.right != null) {
            current = current.right;
            while (current.left != null) {
                current = current.left;
            }
        } else {//Current node does not have right child.
            current = findParent();
        }
        return rst;
    }

    //Find current's parent, where parent.left == current.
    public TreeNode findParent(){
        TreeNode node = root;
        TreeNode parent = null;
        int val = current.val;
        if (val == endNode.val) {
            return null;
        }
        while (node != null) {
            if (val < node.val) {
                parent = node;
                node = node.left;
            } else if (val > node.val) {
                node = node.right;
            } else {//node.val == current.val
                break;
            }
        }
        return parent;
    }
}
 0
Author: Shawn_Fan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-27 16:42:39

Por definición, no es posible que next() y hasNext() se ejecuten en tiempo O(1). Cuando está mirando un nodo en particular en un BST, no tiene idea de la altura y la estructura de los otros nodos, por lo tanto, no puede simplemente "saltar" al siguiente nodo correcto.

Sin embargo, la complejidad del espacio se puede reducir a O(1) (excepto para la memoria de la propia BST). Esta es la forma en que lo haría en C:

struct node{
    int value;
    struct node *left, *right, *parent;
    int visited;
};

struct node* iter_next(struct node* node){
    struct node* rightResult = NULL;

    if(node==NULL)
        return NULL;

    while(node->left && !(node->left->visited))
        node = node->left;

    if(!(node->visited))
        return node;

    //move right
    rightResult = iter_next(node->right);

    if(rightResult)
        return rightResult;

    while(node && node->visited)
        node = node->parent;

    return node;
}

El truco es tener un enlace padre y una bandera visitada para cada uno nodo. En mi opinión, podemos argumentar que esto no es un uso de espacio adicional, es simplemente parte de la estructura del nodo. Y obviamente, iter_next () debe ser llamada sin que cambie el estado de la estructura del árbol (por supuesto), pero también que las banderas "visitadas" no cambien los valores.

Aquí está la función tester que llama a iter_next() e imprime el valor cada vez para este árbol:

                  27
               /      \
              20      62
             /  \    /  \
            15  25  40  71
             \  /
             16 21

int main(){

    //right root subtree
    struct node node40 = {40, NULL, NULL, NULL, 0};
    struct node node71 = {71, NULL, NULL, NULL, 0};
    struct node node62 = {62, &node40, &node71, NULL, 0};

    //left root subtree
    struct node node16 = {16, NULL, NULL, NULL, 0};
    struct node node21 = {21, NULL, NULL, NULL, 0};
    struct node node15 = {15, NULL, &node16, NULL, 0};
    struct node node25 = {25, &node21, NULL, NULL, 0};
    struct node node20 = {20, &node15, &node25, NULL, 0};

    //root
    struct node node27 = {27, &node20, &node62, NULL, 0};

    //set parents
    node16.parent = &node15;
    node21.parent = &node25;
    node15.parent = &node20;
    node25.parent = &node20;
    node20.parent = &node27;
    node40.parent = &node62;
    node71.parent = &node62;
    node62.parent = &node27;

    struct node *iter_node = &node27;

    while((iter_node = iter_next(iter_node)) != NULL){
        printf("%d ", iter_node->value);
        iter_node->visited = 1;
    }
    printf("\n");
    return 1;
}

Que imprimirá los valores en orden:

15 16 20 21 25 27 40 62 71 
 0
Author: KZcoding,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-13 06:56:51