La Lista es una estructura de datos muy importante en los lenguajes de programación donde:
En general las listas proveen las siguientes operaciones:
Acá vemos un ejemplo en seudo código:
tipo abstracto Lista<T>
operacion crearLista() -> Lista
operacion tamanio(Lista lista) -> int
operacion vacia(Lista lista) -> boolean
operacion cabeza(Lista<T> lista) -> T
operacion agregar(Lista<T> lista, T elemento)
operacion elementoEn(Lista<T> lista, int indice) -> T
operacion removerElementEn(Lista lista, int indice)
Una implementación muy común de lista es la llamada lista enlazada, en donde se representa internamente como Nodos que referencian al siguiente.
Así la lista
En la lista enlazada, agregar un elemento es fácil, o poco costoso en términos de alocación de memoria. Símplemente debemos alocar espacio para un nuevo nodo, y actualizar el ultimo nodo, para que apunte al nuevo, como "siguiente".
Por el contrario en una lista basada en array (que vamos a ver más adelante), agregar un elemento que involucre redimensionar el array podría ser costoso a nivel de memoria, si no imposible.
Como para agregar un elemento es necesario primero ubicar el último nodo, es necesario recorrer todos los nodos hasta llegar al final. Esto hace que sea una operación con complejidad lineal O(n), ya que cuantos más elementos tengamos en la lista, más tiempo va a demorar.
Por esto es que en general se implementa una variante, donde la lista además de conocer al primer elemento, conoce al último. Esto cambia la complejidad a orden constante O(1).
Esta operación requiere la misma cantidad de memoria, es decir un nuevo nodo. Por otro lado es también muy simple a nivel de operaciones. Lo que debemos hacer es posicionarnos en el nodo anterior y "meter" el nuevo nodo, actualizando las referencias del anterior al nuevo, y del nuevo al que previamente era el siguiente.
Al igual que el agregar, el remover un item es también simple en términos operativos. Se libera el nodo, pero primero se "atan" entre sí los nodos anterior y siguiente.
No se requiere re-ordenar o re-alocar memoria
La lista enlazada tiene las siguientes desventajas:
Para resolver el tercer problema mencionado anteriormente, el del recorrido inverso/reverso, existe una variante a la lista enlazada simple, llamada lista doblemente enlazada. La idea es que cada nodo, además de conocer al siguiente, conozca al anterior.
Es un poco más costoso a nivel de complejidad de código, porque ahora vamos a tener que mantener un puntero más, y en cada operación como agregar, eliminar, etc.. deberemos actualizarlo. Y por otro lado, también requerirá más memoria (un puntero más). Pero hará más rápido el recorrido inverso (pasamos de complejidad sum(0..N) a O(n))
Otra variante de lista enlazada es aquella en la que el último elemento conoce al primero (y viceversa si es además, doblemente enlazada). De ahí su nombre "circular", ya que se puede representar como un círculo de nodos. Obviamente esta lista no tiene fin.
Esta implementación sí que es bastante diferente a la enlazada (a diferencia de la doblemente enlazada y la circular). Junto a la enlazada, son las dos implementaciones más comunes en los lenguajes de programación.
La idea es bien simple:
Adicionar un elemento a la lista de tipo array tiene varios problemas:
Como ya vimos, a veces el sistema operativo no nos puede dar la posición siguiente de memoria, por lo que tiene que buscar otro sector de memoria que tenga la nueva capacidad, y luego copiar todos los elementos. Por lo cual, en estos casos necesitaremos memoria = N + (N + 1) que es igual a 2N + 1
Esta operación también es costosa. Ya que no podemos simplemente "nullear" la posición donde está el elemento a eliminar, porque tendríamos los índices corridos, y porque tendríamos fragmentación. Necesitamos re-ordenar todos los elementos, corriéndolos a la izquierda.
Por ejemplo, si en para la lista anterior nos puden borrar la "E". Acá podemos ver los pasos:
Por otro lado, por el contrario a las listas enlazadas, el acceder a un elemento arbitrario, es decir en la posición "i", es mucho más simple, ya que justamente tenemos todos los elementos contiguos (localidad), con lo cual podemos "indexar", a través de aritmética de punteros (o bien la notación de arrays).
Esta operación tiene orden de complejidad constante O(1), y es la principal ventaja de la lista basada en array.
Comparemos algunas operaciones y sus costos tanto en la lista enlazada como en la implementación basada en array.
En conclusión podemos decir que uno tiende a utilizar: