¿Por qué esta solución ancestro común tiene un mejor rendimiento peor de los casos?

AfterWorkGuinness:

Estoy mirando dos soluciones para encontrar el primer antepasado común de los dos nodos en un árbol binario (no necesariamente un árbol de búsqueda binaria). Me han dicho que la segunda solución proporciona un mejor peor de los casos el tiempo de ejecución, pero no puedo entender por qué. ¿Puede alguien me ilumine?

Solución 1:

  • Encuentra la profundidad de cada uno de los dos nodos: p, q
  • Calcular el delta de sus profundidades
  • Establecer un puntero al nodo más superficial un puntero de un nodo más profundo
  • Mover el puntero del nodo más profundo por el delta para que podamos empezar de desplazamiento desde la misma altura
  • Recurrentemente visitar los nodos de parte de ambos punteros hasta llegar al mismo nodo que es el primer antepasado común
import com.foo.graphstrees.BinaryTreeNodeWithParent;

/*
   Find the first common ancestor to 2 nodes in a binary tree.
*/
public class FirstCommonAncestorFinder {

    public BinaryTreeNodeWithParent find(BinaryTreeNodeWithParent p, BinaryTreeNodeWithParent q) {

        int delta = depth(p) - depth(q);
        BinaryTreeNodeWithParent first = delta > 0 ? q: p; // use shallower node
        BinaryTreeNodeWithParent second = delta > 0 ? p: q; //use deeper

        second = goUp(second, delta); // move up so they are level, if 1 node is deeper in the tree than the other, their common ancestor obviously cannot be below the shallower node, so we start them off at the same height in the tree


        //keep going up the tree, once first == second, stop
        while(!first.equals(second) && first !=null && second !=null) {
            first = first.getParent();
            second = second.getParent();
        }

        return first == null || second == null ? null : first;

    }

    private int depth(BinaryTreeNodeWithParent n) {
        int depth = 0;
        while (n != null) {
            n = n.getParent();
            depth++;
        }
        return depth;
    }

    private BinaryTreeNodeWithParent goUp(BinaryTreeNodeWithParent node, int delta) {

        while (delta > 0 && node != null) {
            node = node.getParent();
            delta--;
        }
        return node;
    }
}

Solución 2:

  • Verificar ambos nodos (p, q) existe en el árbol que empiezan en el nodo raíz
  • Verificar que q no es un hijo de p y p no es un niño de q por la que atraviesa sus subárboles
  • Recursiva examinar subárboles de los sucesivos nodos padre de p hasta que se encuentre q
import com.foo.graphstrees.BinaryTreeNodeWithParent;

public class FirstCommonAncestorImproved {

    public BinaryTreeNodeWithParent find(BinaryTreeNodeWithParent root,
                                         BinaryTreeNodeWithParent a,
                                         BinaryTreeNodeWithParent b) {

        if (!covers(root, a) || !covers(root, b)) {
            return null;
        } else if (covers(a, b)) {
            return a;
        } else if (covers(b, a)) {
            return b;
        }

        var sibling = getSibling(a);
        var parent = a.getParent();

        while (!covers(sibling, b)) {
            sibling = getSibling(parent);
            parent = parent.getParent();
        }
        return parent;
    }

    private BinaryTreeNodeWithParent getSibling(BinaryTreeNodeWithParent node) {
        if (node == null || node.getParent() == null) return null;
        var parent = node.getParent();
        return node.equals(parent.getLeft()) ? node.getRight() : node.getLeft();
    }

    private boolean covers(BinaryTreeNodeWithParent root,
                           BinaryTreeNodeWithParent node) {

        if (root == null) return false;
        if (root.equals(node)) return true;
        return covers(root.getLeft(), node) || covers(root.getRight(), node);

    }
}

gen:

Depende de la estructura del problema.

Si los nodos de partida son el fondo de un gran árbol, y el antepasado está cerca, entonces todavía tendrá el primer algoritmo para recorrer todo el camino a la raíz para encontrar las profundidades. La segunda tendrá éxito mediante la inspección sólo una pequeña sub-árbol.

Por otro lado, si los nodos son profundas y el antepasado común es muy cerca de la raíz, entonces la primera travesía será sólo de dos rutas de acceso a la raíz, mientras que el segundo puede explorar todo el árbol.

Tenga en cuenta que como suele ser el caso, se puede obtener un algoritmo asintóticamente más rápido por el comercio de espacio para la velocidad. Mantener un conjunto de nodos. Traverse hacia arriba en la alternancia de pasos de ambos nodos de partida, añadiendo al conjunto hasta que encuentre uno que ya existe. Ese es el antepasado común. Teniendo en cuenta las operaciones de conjuntos son O (1), este algoritmo es O (k) donde k es la longitud de la trayectoria del ancestro común al nodo de inicio más distante. No se puede hacer mejor.

Set<Node> visited = new HashSet<>();
while (a != null && b != null) {
  if (visited.contains(a)) return a;
  if (visited.contains(b)) return b;
  visited.add(a);
  visited.add(b);
  a = a.parent();
  b = b.parent();
}
while (a != null) {
  if (visited.contains(a)) return a;
  a = a.parent();
}
while (b != null) {
  if (visited.contains(b)) return b;
  b = b.parent();
}
return null;

Supongo que te gusta

Origin http://43.154.161.224:23101/article/api/json?id=228887&siteId=1
Recomendado
Clasificación