Algoritmos y Lenguaje de Programación, Sección 1 Porqué usar listas? • En una palabra: orden Listas )Fácil ordenar datos )Fácil reordenar datos Ciudad Mario Medina C. [email protected] Superficie urbana (km2) 4360 12147100 12147100 Buenos Aires 3680 11655100 12923800 Ciudad de México 4980 8589600 19013000 Nueva York 8680 8103700 18498000 13500 8027500 31036900 Acceso a los datos Struct ciudad { char *nombre; int superficie; int pobCiudad; int pobUrbana; } ciudades[5] = { {“Bombay”, 4360, 12147100, 12147100}, {“Buenos Aires”, 3680, 11655100, 12923800}, {“Ciudad de México”, 4980, 8589600, 19013000}, {“Nueva York”, 8680, 8103700, 18498000}, {“Tokio”, 13500, 8027500, 31036900} }; Listas vs. Vectores )Generar listas de punteros a los datos )Reordenar las listas )Orden más complejo: 1, 3, 2, 4, 5 • Acceso por superficie creciente )Orden más complejo: 2, 1, 3, 4, 5 • Qué pasa si elimino o agrego una ciudad? )Reordenar la tabla! )Acceso aleatorio a datos eficiente Lista 4360 12147100 12147100 Í 1 1 Î Buenos Aires 3680 11655100 12923800 Í 3 3 Î Ciudad de México 4980 8589600 19013000 Í 2 8680 8103700 18498000 Í 4 13500 8027500 31036900 Í 5 )Los datos no se mueven! ©Mario Medina C. • Acceso por población urbana creciente • Vectores 2 Î Bombay 5 Î Tokio )Fácil: orden usado en almacenamiento de datos Listas vs. Vectores • Idea: dejar los datos donde están 4 Î Nueva York • Acceso alfabético por nombre ` Movimiento de datos es costoso! • Ciudades almacenadas en orden alfabético Lista Población área urbana Bombay Tokio Vector de estructuras Población ciudad ` Acceso via índice es O(1) )Requiere conocer el tamaño a priori ` Memoria estática )Búsquedas ` Si índice es conocido, O(1) ` Si no, O(n) • Listas )Tamaño definido en tiempo de ejecución ` Memoria dinámica )Inserciones y eliminaciones eficientes ` O(1) )Búsquedas son más lentas ` O(n) ` Una vez encontrado el nodo, muchas operaciones son O(1) )Difícil cambiar el tamaño! 1 Algoritmos y Lenguaje de Programación, Sección 1 Listas Lista encadenada • Colección de estructuras de datos independientes (nodos) que contienen datos y están unidas por punteros • Cada nodo contiene un puntero al nodo siguiente • Último nodo apunta a NULL • Raíz apunta a primer nodo Nodos Raíz )Raíz no contiene datos NULL Nodos Raíz 5 6 8 9 NULL Datos Estructura para lista encadenada typedef struct miNodo { struct miNodo *proximo; int dato; } Nodo; • Fácil recorrer lista de raíz a final • No permite recorrer en orden inverso • En este ejemplo, nodos están ordenados por valor en forma ascendente Buscar en lista encadenada (1) Nodo* buscar(Nodo* actual, int valor) { Nodo *n = actual; while (n->dato != valor) { n = n->proximo; } return n; } ©Mario Medina C. Función buscar() • Buscar valor 8 en la lista )Comenzar en la raíz ` Comparar dato con 8 ` Si no es igual, continuar )Apuntar a próximo nodo ` Comparar con 8 ` Si no es igual, repetir )Detenerse al encontrar un nodo NULL Función buscar() • Qué pasa al llegar al final de la lista? )Último nodo tiene proximo = NULL )Ciclo intenta obtener miembro dato de un puntero a NULL ` Error! )Verificar que n no sea NULL antes de acceder a n->dato 2 Algoritmos y Lenguaje de Programación, Sección 1 Buscar en lista encadenada (2) Nodo* buscar(Nodo* actual, int valor) { Nodo *n = actual; while (n != NULL && n->dato != valor) { n = n->proximo; } return n; } • Condición aprovecha la propiedad de cortocircuito Función insertar() • Insertar valor 7 en mitad de la lista )Crea un nuevo nodo ` Solicita memoria via malloc() )Asigna a dato de éste el valor 7 )Modifica puntero proximo de nodo anterior ` Apunta al nodo nuevo )Puntero próximo de nodo nuevo apunta a nodo siguiente )Si la primera condición se cumple, la segunda nunca se evalúa Insertar en lista encadenada int insertar(Nodo* actual, int valor) { Nodo *nuevo, *anterior; while (actual->dato < valor) { anterior = actual; actual = actual->proximo; } nuevo = (Nodo *) malloc(sizeof(Nodo)); if (nuevo == NULL) return FALSE; nuevo->dato = valor; nuevo->proximo = actual; anterior->proximo = nuevo; return TRUE; } Insertar en lista encadenada (2) int insertar(Nodo* actual, int valor) { Nodo *nuevo, *anterior; while (actual != NULL && actual->dato < valor) { anterior = actual; actual = actual->proximo; } nuevo = (Nodo *) malloc(sizeof(Nodo)); if (nuevo == NULL) return FALSE; nuevo->dato = valor; nuevo->proximo = actual; anterior->proximo = nuevo; return TRUE; } ©Mario Medina C. Función insertar() • Cómo insertar al final de la lista? )Código trata de leer dato de nodo NULL ` Error! )Verificar que actual no sea NULL antes de acceder a actual->dato Función insertar() • Cómo insertar al comienzo de la lista? )Modificar raiz )Pasar raiz como argumento a la función ` Raíz es un puntero a un Nodo ` Argumento es puntero a puntero a Nodo )Modifica puntero de nodo anterior )Apunta a nodo siguiente 3 Algoritmos y Lenguaje de Programación, Sección 1 Insertar en lista encadenada (3) int insertar(Nodo **raiz, int valor) { Nodo *nuevo, *actual, *anterior; actual = *raiz; anterior = NULL; while (actual != NULL && actual->dato < valor) { anterior = actual; actual = actual->proximo; } nuevo = (Nodo *) malloc(sizeof(Nodo)); if (nuevo == NULL) return FALSE; nuevo->dato = valor; nuevo->proximo = actual; if (anterior == NULL) *raiz = nuevo; else anterior->proximo = nuevo; return TRUE; } Insertar en lista encadenada (4) int insertar(Nodo **p, int valor) { Nodo *nuevo, *actual; while ((actual = *p) != NULL && actual->dato < valor) { n = &actual->proximo; } nuevo = (Nodo *) malloc(sizeof(Nodo)); if (nuevo == NULL) return FALSE; nuevo->dato = valor; nuevo->proximo = actual; *p = nuevo; return TRUE; } Lista doblemente encadenada • Cada nodo contiene un puntero al nodo siguiente y al nodo anterior )Puede recorrerse en ambas direcciones Raíz NULL 5 Función insertar() • Cómo insertar al comienzo de la lista? )raiz es un puntero a un nodo )proximo también es un puntero a un nodo • Modificar código para considerar este hecho )Puntero p apunta a la raíz o al próximo nodo )Puntero anterior ya no es necesario Otras funciones • Contar los nodos en una lista • Insertar un dato al comienzo de una lista • Insertar un dato al final de una lista • Retornar el n-ésimo dato de una lista • Eliminar un dato de una lista • Eliminar todas las ocurrencias de un dato en una lista • Eliminar la lista Estructura para lista doble typedef struct miNodo { struct miNodo *proximo; struct miNodo *anterior; int dato; } Nodo; • Raíz necesita ahora dos punteros 6 8 9 NULL )Puntero al primer nodo de la lista )Puntero al último nodo de la lista Nodos ©Mario Medina C. 4 Algoritmos y Lenguaje de Programación, Sección 1 Técnica del nodo fantasma Función insertar() • Declarar raíz como un nodo de la lista • Cuatro casos: )proximo es puntero al primer nodo de la lista )anterior es puntero al último nodo de la lista )dato puede ser usado para almacenar el número de nodos, por ejemplo )Insertar valor en mitad de la lista )Insertar valor al comienzo de la lista )Insertar valor al final de la lista )Insertar valor en lista vacía • En cada caso, necesario modificar 4 punteros: )2 en nodo a insertar )1 en nodo anterior )1 en nodo siguiente Insertar en lista doble Insertar en lista doble int insertarDoble(Nodo* raiz, int valor) { Nodo *nuevo, *actual, *siguiente; for (actual = raiz; (siguiente = actual->proximo) != NULL; actual = siguiente) { if (siguiente->dato == valor) return 0; if (siguiente->dato > valor) break; } nuevo = (Nodo *) malloc(sizeof(Nodo)); if (nuevo == NULL) return -1; if (actual != raiz) nuevo->anterior = actual; else nuevo->anterior = NULL; if (siguiente != NULL) siguiente->anterior = nuevo; else raiz->anterior = nuevo; return 1; } • Función retorna 0 si el valor ya está en la lista, 1 si la inserción fue exitosa y –1 si hubo un error nuevo->dato = valor; nuevo->proximo = siguiente; actual->proximo = nuevo; Lista circular • Cada nodo contiene un puntero al nodo siguiente • Último nodo apunta al primer nodo )Puntero raíz al primer nodo )Lista puede ser simple o doblemente encadenada Nodos Raíz 5 ©Mario Medina C. 6 8 9 5