Según tengo entendido, este árbol de búsqueda binaria tiene su estructura de nodo de la siguiente manera:
árbol de estructura
{
item_type x;
struct Tree * left;
struct Tree * right;
estructura Tree * parent;
}
Por lo tanto, un método de inserción asignará dinámicamente memoria para el nuevo nodo y almacenará los 4 elementos de datos (cualesquiera que sean: valores o dirección) en ese nodo. Es decir:
- ¿Cuáles son los mejores métodos para el análisis competitivo?
- ¿Cómo funciona el algoritmo iPod shuffle?
- ¿Qué algoritmos de visión por computadora se utilizan en Protracer para el vuelo de una pelota de golf?
- Noruega: ¿Cómo funciona BankID?
- Cómo resolver el problema de 'La lista negra' en un CodeSprint reciente de HackerRank
1. Almacene el valor del artículo en x.
2. Almacene la dirección del nodo secundario izquierdo: será NULL cuando inserte un nuevo nodo.
3. Almacene la dirección del nodo secundario derecho: será NULL cuando inserte un nuevo nodo.
4. Almacene la dirección del nodo primario del nuevo nodo.
Se insertará un nuevo nodo en el lugar correcto del árbol, lo que implica que el algoritmo debe incorporar la propiedad BST mientras se itera sobre el árbol. El método anterior adopta un enfoque basado en la recursividad para hacer esto.
Déjame contarte acerca de los dobles punteros antes de discutir este método:
Un puntero contiene una dirección de memoria de una variable (mismo tipo que el puntero)
1. int a = 10;
2. int * ptr = & a;
La segunda declaración anterior almacenó la dirección del entero ‘a’ en la variable de puntero ‘ptr’.
Para acceder a ‘a’ a través del puntero ‘ptr’, necesitamos desreferenciar el último. Dereferencia: vaya a la dirección a la que apunta el puntero y obtenga el valor.
Puede hacer * ptr para obtener el valor en la dirección a la que apunta el puntero.
Un puntero doble, como su nombre indica, será un “puntero a puntero”. En otras palabras, almacenará la dirección del puntero.
3. int ** ptr1 = & ptr;
La declaración anterior almacena la dirección de ‘ptr’ en ‘ptr1’.
Digamos que la dirección de la variable a es 1000
Digamos que la dirección de la variable de puntero es 2000
Entonces, el contenido de los punteros sería:
ptr tendrá 1000: almacena la dirección de la variable a.
ptr1 tendrá 2000: almacena la dirección de la variable de puntero ptr
Reglas similares de desreferenciación (usando el operador *) son aplicables ahora también.
* ptr significa ir y obtener el valor en la dirección 1000. El resultado sería 10.
* ptr1 significa ir y obtener el valor en la dirección 2000. El resultado sería 1000.
** ptr1 significa anidar las 2 operaciones anteriores en orden inverso: vaya y obtenga el valor en la dirección 2000. Esto nos da 1000. Ahora obtenga el valor en esta ubicación de memoria. Esto le dará el valor de la variable ‘a’ (10).
Volviendo al programa anterior:
Argumentos:
I: un puntero doble a la raíz de BST . Esto será NULL cuando el árbol aún no se haya inicializado y el primer nodo aún no se haya creado.
En algún lugar del programa, debe haber una variable ‘ Tree * root ‘ y esta es la raíz del BST. Es un puntero y la dirección de este puntero se pasa a la variable I (puntero doble) en el método de inserción.
Siguiendo los principios básicos de doble puntero anteriores: podemos decir que:
La variable ‘raíz’ almacena la dirección del nodo raíz. Será NULL si el árbol aún no se ha creado.
La variable ‘I’ almacena la dirección de la variable de puntero ‘root’. Por ejemplo:
Árbol * raíz = NULL;
insert (& root, x, NULL);
X : nuevo elemento de datos que se insertará en el nuevo nodo.
Parent : dirección del padre del nuevo nodo (que se creará). Tenga en cuenta que el padre de la raíz será NULL. Esto siempre se pasará como NULL a la función. Más tarde, durante las llamadas recursivas, su valor cambiará. Más tarde diré por qué.
Digamos que el árbol aún no se ha creado. Implica que el contenido de ‘root’ será NULL. No apunta a nada.
Digamos que la dirección de la variable del puntero ‘raíz’ es 1000.
1. La dirección del puntero raíz (1000) se pasará a I (puntero a raíz).
2. X tendrá algún elemento.
3. Los padres serán NULOS.
Verificamos si root es NULL o no. ¿Cómo verificamos esto?
Al desreferenciar el puntero doble I como * I, significa ir a la ubicación de memoria 1000 y obtener el valor. Si es NULL, implica que la raíz es NULL
La condición si (* I == NULL) verifica esto.
Ahora ingresa el bloque if, y crea un nuevo nodo asignando memoria del montón. La variable temporal ‘p’ almacena la dirección devuelta por malloc (digamos 2000) para el fragmento de memoria recientemente asignado. Luego almacena los datos requeridos en esa ubicación de memoria de la siguiente manera.
p-> elemento = x; (almacenar el artículo)
p-> left = p-> right = NULL; (cada nuevo nodo se insertará como hoja)
p-> parent = parent (que es NULL)
Ahora que se ha reservado una memoria para un nodo de árbol y se han almacenado datos en él, necesitamos actualizar nuestra estructura de datos de árbol.
El nodo creado anteriormente se convertirá en el nodo raíz. Significa que necesitamos almacenar (2000, que es la dirección de este nuevo nodo) en la variable de puntero ‘root’.
Como hacemos eso ? Recuerde que I (puntero doble) es un puntero a la raíz.
Simplemente desreferencia I como * I y asígnele el valor 2000.
La declaración (* I = p) hace esto: vaya a la ubicación de memoria 1000 y almacene el valor 2000.
Ahora acabamos de regresar.
Después de la primera llamada, el puntero ‘root’ tiene almacenada la dirección 2000. Tenemos una raíz para nuestro BST.
Ahora, se insertará el segundo elemento. Lo mismo que arriba:
1. La dirección del puntero raíz (1000) se pasará a I (puntero a raíz).
2. X tendrá algún elemento.
3. Los padres serán NULOS. Ahora te diré por qué.
Al ingresar al método, verificamos si root es NULL. La verificación falla. Por qué ?
if (* I == NULL) significa ir a la dirección 1000 y ver si el valor es NULL. No, no es. Es 2000, que no es igual a nulo.
No ingresamos el bloque if, y luego hacemos algunas comparaciones basadas en la propiedad BST típica y hacemos llamadas recursivas adecuadas con los argumentos apropiados.
1. Si el nuevo elemento a insertar es menor que la raíz, atravesamos el subárbol izquierdo.
1. Si el nuevo elemento a insertar es mayor que la raíz, atravesamos el subárbol derecho.
¿Cómo nos comparamos? (* I) -> artículo
(* I): significa ir a la ubicación de memoria 1000 (dirección del puntero raíz) y obtener el valor. Este valor es 2000 (ubicación de memoria de los datos del nodo raíz)
(* I) -> elemento – significa ir a la ubicación de memoria 2000 y obtener el valor de ‘elemento’.
Entonces puedes comparar como
si ((* I) -> elemento> X)
{hacer una llamada recursiva para la inserción en el subárbol izquierdo}
más
{hacer una llamada recursiva para la inserción en el subárbol derecho}
¿Cómo hacemos llamadas recursivas?
La verificación de condición anterior determinará en qué subárbol se realizará la inserción. Para poder hacer eso, debemos entender que:
1. A medida que hacemos llamadas recursivas en cualquiera de los subárboles (según lo determinado por la condición anterior), el árbol virtual que será examinado por la llamada recursiva tiene que ser diferente.
2. La llamada recursiva ya no puede funcionar con la misma raíz de nivel superior del BST. El hijo izquierdo o derecho (dependiendo de qué subárbol necesita ser examinado) de la raíz del nivel superior será tratado virtualmente como la raíz del árbol que será examinada por la llamada recursiva. Este procedimiento continuará mientras se realicen las llamadas recursivas y se encuentre el punto de inserción correcto en el árbol.
3. El padre ya no puede ser NULL. Era NULL solo para la raíz de nivel superior. Ahora que las llamadas recursivas examinarán un árbol “virtualmente” enraizado en un nodo diferente, el puntero principal ya no puede ser NULL. Seguirá cambiando a medida que la pila de llamadas de recursividad crezca.
4. Necesitamos actualizar los punteros del nodo primario al nodo secundario (uno recién creado) dependiendo de si el nuevo nodo se crea como un elemento secundario izquierdo o derecho de su elemento primario. Esta es la parte más importante para entender en las llamadas recursivas.
Recurrente para el subárbol izquierdo
insert (dirección del puntero apuntando al hijo izquierdo de la raíz actual, X, dirección de la raíz actual);
que se convierte en:
insertar (& ((* I) -> izquierda), X, * I);
& ((* I) -> left): esta es la dirección [puntero que apunta al hijo izquierdo de la raíz actual].
X: elemento
* I: Desreferenciar el puntero doble a la raíz virtual actual para obtener la dirección del fragmento de memoria que contiene los datos de esta raíz virtual. Esta dirección se almacenará como el contenido del puntero principal del nuevo nodo.
Este proceso se repetirá y los parámetros pasados se cambiarán a medida que la pila de recursividad crezca.
Recurrente para el subárbol correcto
insert (dirección del puntero apuntando al hijo derecho de la raíz actual, X, dirección de la raíz actual);
que se convierte en:
insertar (& ((* I) -> derecha), X, * I);
& ((* I) -> derecha): esta es la dirección [puntero que apunta al hijo derecho de la raíz actual].
X: igual que el anterior
* I: igual que el anterior
Espero que esto ayude. Avísame si quieres una explicación esquemática. Proporcionaré eso.