Aprendizaje de la serie de estructura de datos (5) - Lista doblemente enlazada (Double_Linked_List)

Tabla de contenido

introducción:

estudiar:

Código:

Archivo de encabezado (Double_Linked_List):

Diseño de estructura:

Declaración de función:

La implementación específica de la función función en el archivo fuente (Double_Linked_List):

Función de inicialización (Init_dlist):

Borrar función (Borrar):

Función Destroy (eliminación de encabezado inalámbrico) (Destroy1):

Función de destrucción (nodo de liberación cooperativa de dos punteros) (Destroy2):

Función de impresión (Mostrar):

Función de búsqueda (Buscar):

Obtenga la función de longitud efectiva (Get_Length):

Función de juicio vacío (IsEmpty):

Función de inserción de cabeza (Insert_head):

Función de inserción de cola (Insert_tail):

Interpolar función por posición (Insert_pos):

Función de eliminación de cabeza (Delete_head):

Función de eliminación de cola (Delete_tail):

Eliminar función por posición (Delete_pos):

Eliminar función por valor (Delete_val):

prueba:

Función de inicialización de prueba, función de impresión:

Función de complemento de encabezado de prueba: 

Función de interpolación de cola de prueba: 

Pruebe la función de interpolación posicional:

Función de eliminación de encabezado de prueba:

 Función de eliminación de cola de prueba:

Pruebe la función de eliminación por posición:

Pruebe la función de eliminación por valor: 

Función de búsqueda de prueba: 

Función clara de prueba: 

Prueba destruir la función 1: 

Función de destrucción de prueba 2: 

Resumir:


introducción:

Directorio de aprendizaje de la estructura de datos:

Aprendizaje de la serie de estructuras de datos (1): una introducción a la estructura de datos

 Aprendizaje de series de estructura de datos (2) - tabla de secuencia (Contiguous_List)

Aprendizaje de la serie de estructura de datos (3) - Lista enlazada individualmente (Linked_List)

Aprendizaje de la serie de estructura de datos (4) - Lista enlazada circular

En el artículo anterior, aprendimos el conocimiento teórico de una lista enlazada circular de un solo elemento y la implementamos usando código.Hay otra forma de expresión en la estructura de almacenamiento encadenado: una lista enlazada bidireccional. En la lista enlazada simple o la lista enlazada circular unidireccional que aprendimos antes, cada nodo en la lista enlazada guarda la dirección de su nodo sucesor, pero la lista doblemente enlazada que presentaremos y aprenderemos hoy es diferente. list No solo puede guardar la dirección de su nodo sucesor, sino también guardar la dirección de su nodo predecesor. Por eso se le llama lista doblemente enlazada.

estudiar:

La lista de enlaces dobles es diferente de la lista de enlaces simples. Cada nodo de la lista de enlaces dobles no solo guarda la dirección del nodo sucesor, sino que también guarda la dirección del nodo predecesor.

¿Por qué hay una lista doblemente enlazada?

En la "Estructura de datos (edición en lenguaje C)" de Yan Weimin, se dice que solo hay un campo de puntero que indica el sucesor directo en la estructura de almacenamiento encadenado. Por lo tanto, a partir de un cierto nodo, solo puede buscar otros nodos hacia atrás a través de el puntero. . Para superar las deficiencias unidireccionales de la lista de enlace simple, se produce una lista de enlace doble.

El significado de la existencia de la lista doblemente enlazada: cada nodo puede encontrar tanto al sucesor directo como al predecesor directo. Y cada nodo puede retroceder o avanzar, lo que equivale a una mejora en la lista de enlaces únicos, como se muestra en la figura:

Código:

Las funciones que queremos implementar en la lista doblemente enlazada (15):

función de inicialización (Init_dlist);

Borrar función (Borrar);

Función de destrucción (eliminación de encabezado inalámbrico) (Destroy1);

Función de destrucción (nodo de liberación cooperativa de doble puntero) (Destroy2);

función de impresión (Mostrar);

Función de búsqueda (Buscar);

Obtener la función de longitud efectiva (Get_Length);

función de juicio vacío (IsEmpty);

Función de inserción de encabezado (Insert_head);

Función de inserción de cola (Insert_tail);

Interpolar función por posición (Insert_pos);

función de eliminación de cabeza (Delete_head);

Función de eliminación de cola (Delete_tail);

Eliminar función por posición (Delete_pos);

Eliminar función por valor (Delete_val);

Archivo de encabezado (Double_Linked_List):

Diseño de estructura:

Como su nombre lo indica, hay dos dominios punteros en la lista doblemente enlazada, que son el predecesor (dominio anterior) para almacenar la dirección del nodo anterior, el sucesor (dominio siguiente) para almacenar la dirección del siguiente nodo y el dominio de datos para almacenar datos, como se muestra en la figura:

Así que diseñamos la estructura de la lista doblemente enlazada de acuerdo con la estructura de la lista doblemente enlazada:

typedef int Elem_type;
typedef struct DNode
{
    Elem_type data;
    struct DNode* next;
    struct DNode* prior;
}DNode, *PDnode;

Declaración de función:

void Init_dlist(PDnode dlist);
void Clear(PDnode dlist);
void Destroy(PDnode dlist);
void Destroy1(PDnode dlist);
void Show(PDnode dlist);
struct DNode* Search(PDnode dlist,Elem_type val);
int Get_Length(PDnode dlist);
bool IsEmpty(PDnode dlist);
bool Insert_head(PDnode dlist,Elem_type val);
bool Insert_tail(PDnode dlist,Elem_type val);
bool Insert_pos(PDnode dlist,int pos,Elem_type val);
bool Delete_head(PDnode dlist);
bool Delete_tail(PDnode dlist);
bool Delete_pos(PDnode dlist,int pos);
bool Delete_val(PDnode dlist,Elem_type val);

La implementación específica de la función función en el archivo fuente (Double_Linked_List):

Función de inicialización (Init_dlist):

Debido a que los nodos de la lista doblemente enlazada se componen de tres partes, a saber: dominio de datos, dominio siguiente (sucesor), dominio anterior (predecesor), cuando inicializamos la lista doblemente enlazada, primero aclaramos que no hay nodos válidos en la etapa inicial, por lo que los datos iniciales El campo no almacena ningún dato, y luego se pueden asignar valores vacíos al siguiente campo y al campo anterior.

void Init_dlist(PDnode dlist)
//如果没有有效节点,则双向链表的头节点,应该:头节点的数据域浪费掉,不使用,头节点的next域
{
    assert(dlist != nullptr);
    dlist->next = nullptr;
    dlist->prior = nullptr;
}

Borrar función (Borrar):

Como se mencionó anteriormente, borrar y destruir la lista enlazada tienen el mismo significado, por lo que podemos llamar directamente a la función de destrucción en la función de limpieza.

void Clear(PDnode dlist)
{
    Destroy(dlist);
}

Función Destroy (eliminación de encabezado inalámbrico) (Destroy1):

La primera forma de destruir la lista enlazada: cuando la lista enlazada no está vacía, llamamos a la función de eliminación de cabecera infinitamente hasta que se eliminen todos los nodos de la lista enlazada.

void Destroy(PDnode dlist)//无线头删
{
    while(!IsEmpty(dlist)){
        Delete_head(dlist);
    }
}

Función de destrucción (nodo de liberación cooperativa de dos punteros) (Destroy2):

La segunda forma de destruir la lista enlazada: dos punteros cooperan para liberar el nodo. Primero, desconectamos el nodo principal (dejamos vacío el siguiente campo del nodo principal), definimos el puntero de tipo de estructura p para que apunte al primer nodo válido, definimos el puntero de tipo de estructura q y lo dejamos vacío. Defina un bucle para recorrer la lista enlazada utilizando el puntero p. Cada vez que p apunta a un nodo, el siguiente campo de q se asigna a q, y luego se suelta el puntero q, y luego q se copia en p.

void Destroy1(PDnode dlist)//两个指针辅助
{
    assert(dlist != nullptr);
    PDnode p = dlist->next;
    PDnode q = nullptr;
    dlist->next = nullptr;
    while(p != nullptr){
        q = q->next;
        free(p);
        p = q;
    }
}

Función de impresión (Mostrar):

Defina el puntero de tipo de estructura p para que apunte al primer nodo efectivo y defina el ciclo. Cada vez que p atraviesa un nodo, se imprime el valor en el campo de datos del nodo al que apunta p.

void Show(PDnode dlist)
{
    assert(dlist != nullptr);
    PDnode p = dlist->next;
    for(;p != nullptr;p = p->next){
        printf("%5d",p->data);
    }
    printf("\n");
}

Función de búsqueda (Buscar):

Defina el puntero de tipo de estructura p para que apunte al primer nodo válido después del nodo principal, defina un bucle, verifique si el valor en el campo de datos apuntado por el puntero p es igual al valor que se buscará en el proceso de atravesar p toda la lista enlazada, si es Igual devuelve la dirección del nodo, o una dirección nula si no se encuentra.

struct DNode* Search(PDnode dlist,Elem_type val)
{
    assert(dlist != nullptr);
    PDnode p = dlist->next;
    for(;p->next != nullptr;p = p->next){
        if(p->data == val){
            return p;
        }
    }
    return nullptr;
}

Obtenga la función de longitud efectiva (Get_Length):

Defina el valor entero de recuento para registrar el número de nodos válidos, defina el puntero de tipo de estructura p para que apunte al primer nodo válido después del nodo principal, defina un bucle y la condición del bucle es que p no es igual a vacío, y cada vez que se atraviesa un recuento de nodos válido Simplemente agregue uno, y la función devuelve el valor de recuento al final.

int Get_Length(PDnode dlist)
{
    assert(dlist != nullptr);
    int count = 0;
    PDnode p = dlist->next;
    for(;p != nullptr;p = p->next){
        count++;
    }
    return count;
}

Función de juicio vacío (IsEmpty):

Cuando el siguiente campo del nodo principal está vacío, significa que toda la lista vinculada no tiene nodos válidos, lo que significa que la lista vinculada está vacía.

bool IsEmpty(PDnode dlist)
{
    return dlist->next == nullptr;
}

Función de inserción de cabeza (Insert_head):

Antes de escribir la función de inserción de encabezado, debemos considerar cómo debemos cambiar la directividad de pnewnode y sus nodos frontal y posterior después de solicitar el espacio de memoria de pnewnode en el área del montón.

Como se muestra en la imagen:

En primer lugar, lo que debemos hacer es modificar el campo de datos de pnewnode y la ducha, poner el valor que se insertará en el campo de datos de pnewnode y asignar la dirección almacenada en el siguiente campo del nodo principal original ( es decir, la dirección del siguiente nodo válido) al siguiente dominio de pnewnode, y luego copiamos la dirección del nodo principal al dominio anterior de pnewnode. En este momento, surge la pregunta, ¿deberíamos modificar primero el siguiente dominio del nodo principal o el dominio anterior del segundo nodo válido? Este problema se encontró antes cuando estábamos en la lista de un solo enlace. Cuando agregamos un nodo, si primero modificamos el siguiente campo del nodo antes del nuevo nodo, la dirección del nodo posterior se perderá y la computadora no ser capaz de encontrar el nodo subsiguiente. Luego, por la misma razón, en la lista doblemente enlazada, si primero modificamos el siguiente campo del nodo principal, inevitablemente provocará la pérdida de la dirección del nodo posterior. Entonces, nuestro enfoque correcto debería ser modificar el campo anterior del primer nodo válido original y luego modificar el siguiente campo del nodo principal.

Nota: si solo hay un nodo principal en la lista vinculada en este momento, solo necesitamos modificar el siguiente campo del nodo principal y el campo anterior de pnewnode, y luego asignar el siguiente campo de pnewnode para que esté vacío.

Entonces, en general, los pasos para modificar el puntero deben ser: el campo siguiente y el campo anterior del nuevo nodo, el campo anterior del nodo posterior y el campo siguiente del nodo anterior. 

bool Insert_head(PDnode dlist,Elem_type val)
{
    assert(dlist != nullptr);
    PDnode pnewnode = (PDnode) malloc(1 * sizeof(DNode));
    //先修改pnewnode自身的两个域,再处理下一个节点的prior域,最后处理上一个节点的next域
    assert(pnewnode != nullptr);
    pnewnode->data = val;
    pnewnode->next = dlist->next;//1
    pnewnode->prior = dlist;
    if(dlist->next != nullptr) {
        dlist->next->prior = pnewnode;
    }
    dlist->next = pnewnode;
    return true;
}

Función de inserción de cola (Insert_tail):

Primero solicitamos la memoria del nuevo nodo (pnewnode) en el área del montón, guardamos los datos que se insertarán en el campo de datos del nuevo nodo solicitado, definimos el puntero de tipo de estructura p para que apunte al nodo principal y definimos un bucle , la condición del bucle es que p no es igual a vacío , copia el siguiente campo de p a p y luego p apunta al nodo final. Luego modificamos la orientación del puntero, asignamos el valor del siguiente campo (nullptr) del nodo final original al siguiente campo de pnewnode, asignamos la dirección del nodo final original al campo anterior de pnewnode, luego copiamos la dirección de pnewnode al nodo final original El siguiente dominio es suficiente, como se muestra en la figura:

bool Insert_tail(PDnode dlist,Elem_type val)//不存在特殊情况 每一种情况都是修改三个指针域
{
    assert(dlist != nullptr);
    PDnode pnewnode = (PDnode) malloc(1 * sizeof(DNode));
    assert(pnewnode != nullptr);
    pnewnode->data = val;
    PDnode p = dlist;
    for(;p->next != nullptr;p = p->next);
    pnewnode->next = p->next;
    pnewnode->prior = p;
    p->next = pnewnode;
    return true;
}

Interpolar función por posición (Insert_pos):

Primero solicitamos la memoria del nuevo nodo (pnewnode) en el área del montón y guardamos los datos que se insertarán en el campo de datos del nuevo nodo solicitado. Cuando pos es 0, llamamos directamente a la función de inserción de encabezado que escribimos antes. Cuando pos es igual a Cuando la longitud de toda la lista enlazada es -1, llame directamente a la función de inserción de cola que escribimos antes. Si ninguno de estos dos casos es el caso, se inserta en la posición media, se define el puntero de tipo de estructura p para que apunte al nodo principal, se define el puntero de posicionamiento del bucle p en la posición de inserción y luego modificamos el apuntamiento del y establecemos el siguiente campo de p (el nodo después de la dirección de la posición de inserción) en el siguiente campo de pnewnode, luego copiamos la dirección del puntero p (la dirección del nodo antes de la posición de inserción) al campo anterior de pnewnode, y luego asignamos la dirección de pnewnode al campo anterior del nodo después de la posición de inserción, y luego modificamos el siguiente campo del puntero p (el nodo antes de la posición de inserción), y copiamos la dirección de pnewnode a el siguiente campo de p, como se muestra en la figura:

bool Insert_pos(PDnode dlist,int pos,Elem_type val)//当pos==0的时候是头插 当pos等于length的时候是尾插,其他位置pos >0 && pos < length 的时候是中间插入
{
    assert(dlist != nullptr);
    assert(pos >= 0 && pos <= Get_Length(dlist));
    PDnode pnewnode = (PDnode) malloc(1 * sizeof(DNode));
    assert(pnewnode != nullptr);
    pnewnode->data = val;
    if(pos == 0){
        return Insert_head(dlist,val);
    }
    else if(pos == Get_Length(dlist) - 1){
        return Insert_tail(dlist,val);
    }
    PDnode p = dlist;
    for(int i = 0;i < pos;i++){//当pos等于几
        p = p->next;
    }
    pnewnode->next = p->next;//1
    pnewnode->prior = p;//2
    p->next->prior = pnewnode;//4
    p->next = pnewnode;//3
    return true;
}

Función de eliminación de cabeza (Delete_head):

Antes de escribir la función de eliminación de cabecera, primero debemos juzgar que la lista enlazada está vacía. En este momento, debemos considerar el primer caso. Si la lista enlazada tiene solo un nodo válido en este momento, entonces solo necesitamos vaciar el siguiente. campo del nodo principal. Si el número de nodos válidos es mayor que 1, defina el puntero de tipo de estructura p para que apunte al primer nodo válido de la lista enlazada, copie el siguiente campo de p (es decir, la dirección del segundo nodo válido) al siguiente campo del nodo principal y, a continuación, simplemente asigne la dirección del nodo principal al campo anterior del segundo nodo válido, como se muestra en la figura:

bool Delete_head(PDnode dlist)
{
    assert(dlist != nullptr);
    if(IsEmpty(dlist)){
        return false;
    }
    if(dlist->next->next == nullptr){
        dlist->next = nullptr;
    }
    PDnode p = dlist->next;
    dlist->next = p->next;
    p->next->prior = dlist;
    free(p);
    return true;
}

Función de eliminación de cola (Delete_tail):

Primero, vacíe la lista enlazada, defina el puntero de tipo de estructura p para que apunte al nodo principal, defina el ciclo para que p apunte al nodo final, defina el puntero de tipo de estructura q para que apunte al nodo principal, defina el ciclo, el La condición del bucle es que el siguiente campo de q no sea igual a p, y luego asigne el siguiente campo de q a p, y p apunte al penúltimo nodo. Ahora asignamos el siguiente campo de p (es decir, nullptr) al siguiente campo de q, como se muestra en la figura:

bool Delete_tail(PDnode dlist)
{
    //尾删不存在特殊情况,因为待删除节点就是尾节点,且待删除节点的后一个节点永远永远不存在
    assert(dlist != nullptr);
    if(IsEmpty(dlist)){
        return false;
    }
    PDnode p = dlist;
    for(;p->next != nullptr;p = p->next);
    PDnode q = dlist;
    for(;q->next != p;q = q->next);
    q->next = p->next;
    free(p);
    return true;
}

Eliminar función por posición (Delete_pos):

Primero, juzgue la lista enlazada como vacía. Si pos es 0, simplemente llame a la función de eliminación de cabecera que escribimos antes. Si pos es igual a la longitud efectiva de la lista enlazada menos 1, simplemente llame a la función de eliminación de cola que escribimos antes. Si Ni la eliminación de la cabeza es la eliminación de la cola, es decir, la eliminación de la posición media. Defina el puntero de tipo de estructura q para que apunte al nodo principal, defina un bucle, la condición del bucle es i < pos, después de completar el bucle, asigne el siguiente campo de q a q, q apunte al nodo anterior en la posición a ser eliminado, y el siguiente campo de q es Representa el siguiente nodo que se eliminará, defina el puntero de tipo de estructura p, y asigne el siguiente campo de q a p. En este momento, p apunta al nodo que se eliminará, y luego realizamos una operación de cruce de puntos, y pasaremos al siguiente nodo que se eliminará La dirección del nodo (es decir, el siguiente campo de p) se asigna al siguiente campo de un nodo en el nodo que se eliminará (es decir , el siguiente campo de q), y luego asignamos la dirección del nodo apuntado por q al campo anterior de p. Como se muestra en la imagen:

bool Delete_pos(PDnode dlist,int pos)
{
    assert(dlist != nullptr);
    assert(pos >= 0 && pos < Get_Length(dlist));
    if(IsEmpty(dlist)){
        return false;
    }
    //头删的情况
    if(pos == 0){
        return Delete_head(dlist);
    }
    //尾删的情况
    if(pos == Get_Length(dlist) - 1){
        return Delete_tail(dlist);
    }
    //既不是头删也不是尾删的情况——中间位置的删除,需要统一修改两个指针域
    PDnode q = dlist;
    for(int i = 0;i < pos;i++){
        q = q->next;
    }
    PDnode p = q->next;
    q->next = p->next;
    p->next->prior = q;
    free(p);
    return true;
}

Eliminar función por valor (Delete_val):

Primero, la lista enlazada se considera vacía y, si la lista enlazada está vacía, devuelve falso. Defina el puntero de tipo de estructura p para guardar la dirección devuelta por la función de búsqueda. Si la dirección guardada está vacía, devolverá falso. Si no está vacía, p apunta al nodo a eliminar. Defina el puntero de tipo de estructura q para apunte al nodo principal y defina el ciclo, la condición del ciclo es que el siguiente campo de q no esté vacío, luego asignamos el siguiente campo de q a q, luego q apunta a la dirección del nodo anterior al nodo p en este tiempo, aquí consideramos un caso especial, si solo uno en la lista enlazada es un nodo válido (es decir, el siguiente campo de p está vacío), entonces podemos asignar directamente el siguiente valor de q para que esté vacío. nodos válidos es mayor que 1, realizaremos una operación de cruce de puntos y asignaremos la dirección del nodo después del nodo que se eliminará Proporcione el siguiente dominio del nodo anterior y luego copie la dirección del nodo anterior del nodo a eliminarse al dominio anterior del siguiente nodo, como se muestra en la figura:

bool Delete_val(PDnode dlist,Elem_type val)
{
    assert(dlist != nullptr);
    if(IsEmpty(dlist)){
        return false;
    }
    PDnode p = Search(dlist,val);
    if(p == nullptr){
        return false;
    }
    PDnode q = dlist;
    for(;q->next != p;q = q->next);
    if(p->next == nullptr){
        q->next = nullptr;
    }
    else{
        q->next = p->next;
        p->next->prior = q;
    }
    free(p);
    return true;
}

prueba:

Función de inicialización de prueba, función de impresión:

#include<cstdio>
#include<cassert>
#include<cstdlib>
#include "Double_Linked_List.h"
int main()
{
    DNode head;
    Init_dlist(&head);
    for(int i = 0;i < 10;i++){
        Insert_pos(&head,i,i + 1);
    }
    printf("原始数据为:\n");
    Show(&head);
/*
    其他函数的测试代码在此添加...
*/

}

resultado de la operación:

Función de complemento de encabezado de prueba: 

Insertamos 100 al principio de la lista enlazada:

    printf("经过头插后的数据为:\n");
    Insert_head(&head,100);
    Show(&head);

resultado de la operación:

Función de interpolación de cola de prueba: 

Insertamos 100 al final de la lista enlazada:

    printf("经过尾插后的数据为:\n");
    Insert_tail(&head,100);
    Show(&head);

 resultado de la operación:

Pruebe la función de interpolación posicional:

Insertamos 100 después del segundo nodo válido en la lista enlazada:

    printf("经过按位置插后的数据为:\n");
    Insert_pos(&head,2,100);
    Show(&head);

resultado de la operación:

Función de eliminación de encabezado de prueba:

Eliminamos el elemento de cabecera de la lista enlazada:

    printf("经过头删后的数据为:\n");
    Delete_head(&head);
    Show(&head);

resultado de la operación;

 Función de eliminación de cola de prueba:

Eliminamos el elemento de cola de la lista enlazada:

    printf("经过尾删后的数据为:\n");
    Delete_tail(&head);
    Show(&head);

resultado de la operación:

Pruebe la función de eliminación por posición:

Eliminamos elementos después del cuarto nodo válido de la lista enlazada:

    printf("经过按位置删后的数据为:\n");
    Delete_pos(&head,4);
    Show(&head);

resultado de la operación:

Pruebe la función de eliminación por valor: 

Queremos eliminar el elemento 2 en la lista enlazada:

    printf("经过按值删后的数据为:\n");
    Delete_val(&head,2);
    Show(&head);

resultado de la operación;

Función de búsqueda de prueba: 

Encuentra si hay el elemento 4 en la lista enlazada:

    PDnode p = Search(&head,4);
    printf("地址为:%p",p);

resultado de la operación:

Encuentra si hay el elemento 100 en la lista enlazada:

    PDnode p = Search(&head,100);
    printf("地址为:%p",p);

resultado de la operación:

Función clara de prueba: 

    Clear(&head);
    Show(&head);

 resultado de la operación:

Prueba destruir la función 1: 

    Destroy(&head);
    Show(&head);

resultado de la operación:

Función de destrucción de prueba 2: 

    Destroy1(&head);
    Show(&head);

resultado de la operación:

Resumir:

La lista doblemente enlazada es una optimización de la lista enlazada única, que supera la desventaja de que la lista enlazada solo puede ir de adelante hacia atrás. En la lista doblemente enlazada, cada nodo tiene un campo anterior que puede guardar la dirección del anterior nodo y un siguiente campo que puede guardar el siguiente nodo. Por lo tanto, cada nodo puede ir hacia atrás o hacia adelante. El significado de la existencia de la lista doblemente enlazada es que se puede acceder a cualquier nodo en la lista enlazada desde cualquier nodo. La dificultad de la lista con doble enlace es mayor que la de la lista con un solo enlace, pero después de aprender y comprender el principio de la lista con doble enlace, es relativamente fácil escribir código, y la lista con doble enlace también es más importante. , se usa La lista doblemente enlazada se usa para guardar la dirección del proceso, a fin de lograr el acceso cruzado, por lo que es muy importante dominar el conocimiento de la lista doblemente enlazada y escribir el código de forma independiente.

Supongo que te gusta

Origin blog.csdn.net/weixin_45571585/article/details/127773893
Recomendado
Clasificación