Apuntes DAM
Volver al inicio

Lista enlazada

AlgoritmosEstructuras de datosNivel intermedioTambién: linked list, lista simplemente enlazada, LinkedList

Una secuencia de nodos en la que cada uno guarda un valor y una referencia al siguiente. Insertar o borrar en un punto ya localizado es O(1), sin mover nada; llegar a una posición cuesta O(n).

Visualízalo paso a paso

Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.

Lista enlazada

Escribe las operaciones (principio x, final x o borrar x) y mira cómo se crean nodos y se cambian las flechas, sin mover ningún dato.

principio x, final x o borrar x (enteros), separadas por comas

Paso 1

Una lista vacía: cabeza no apunta a ningún nodo (null). Cada nodo guarda un valor y una referencia al siguiente.

1class Lista {
2    private Nodo cabeza;
3
4    void alPrincipio(int x) {
5        Nodo n = new Nodo(x);
6        n.siguiente = cabeza;
7        cabeza = n;
8    }
9
10    void alFinal(int x) {
11        Nodo n = new Nodo(x);
12        if (cabeza == null) { cabeza = n; return; }
13        Nodo p = cabeza;
14        while (p.siguiente != null) p = p.siguiente;
15        p.siguiente = n;
16    }
17
18    boolean borrar(int x) {
19        if (cabeza == null) return false;
20        if (cabeza.valor == x) { cabeza = cabeza.siguiente; return true; }
21        Nodo p = cabeza;
22        while (p.siguiente != null && p.siguiente.valor != x) p = p.siguiente;
23        if (p.siguiente == null) return false;
24        p.siguiente = p.siguiente.siguiente;
25        return true;
26    }
27}

Variables

lista
vacía

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

En un array los elementos están seguidos en memoria: por eso se llega a cualquiera en O(1) con su índice, pero insertar o borrar en medio obliga a desplazar todos los que van detrás.

Una lista enlazada hace lo contrario. Cada elemento vive en su propio objeto, un nodo, que guarda el valor y una referencia (una flecha) al siguiente nodo. La lista solo necesita saber dónde está el primero, la cabeza; el último apunta a null. Los nodos pueden estar en cualquier parte de la memoria.

Insertar un nodo nuevo o quitar uno es cambiar un par de flechas, O(1), sin mover ningún dato... siempre que ya se tenga el nodo anterior. El precio es que no hay acceso directo: para llegar al elemento 1.000 hay que recorrer 999 flechas desde la cabeza, O(n). Además cada nodo ocupa bastante más memoria que una casilla de un array.

Hay variantes: la doblemente enlazada guarda también una flecha al anterior (permite recorrer hacia atrás y borrar un nodo conociendo solo ese nodo), y la circular hace que el último apunte al primero. LinkedList de Java es doblemente enlazada.

Cuándo usarlo

  • Muchas inserciones y borrados al principio o en un punto que ya se tiene localizado (por ejemplo, mientras se recorre).
  • Como base de otras estructuras: pilas, colas, las listas de las cubetas de una tabla hash, las listas de adyacencia de un grafo.
  • Para entender las referencias de Java: es el ejercicio clásico para dominar null, la asignación de referencias y la recursividad sobre estructuras.

Cuándo no

  • Si se accede por posición (get(i)): en un array es O(1) y en una lista enlazada O(n). Por eso ArrayList es la opción por defecto en Java.
  • Si importa la memoria o la velocidad de recorrido: los nodos dispersos en memoria aprovechan muy mal la caché.

Paso a paso

  1. El nodo. Una clase con dos campos: valor y siguiente, una referencia a otro nodo (o null si es el último).
  2. Insertar al principio. Nodo nuevo cuyo siguiente es la cabeza actual; después, la cabeza pasa a ser el nodo nuevo. Dos asignaciones, O(1).
  3. Recorrer. for (Nodo p = cabeza; p != null; p = p.siguiente): se avanza de flecha en flecha hasta null.
  4. Borrar. Se busca el nodo anterior al que se borra y se hace que se lo salte: anterior.siguiente = anterior.siguiente.siguiente. Si es la cabeza, la cabeza pasa al segundo.

El código

Una lista enlazada con referencia al último

Guardar también el último nodo hace que añadir al final sea O(1); hay que acordarse de actualizarlo al borrar.

Java
1public class Main {
2    static class Nodo {
3        int valor;
4        Nodo siguiente;
5        Nodo(int valor) { this.valor = valor; }
6    }
7
8    /** Lista simplemente enlazada con referencia al primero y al último. */
9    static class Lista {
10        private Nodo cabeza, ultimo;
11        private int tam = 0;
12
13        void alPrincipio(int x) {                     // O(1)
14            Nodo n = new Nodo(x);
15            n.siguiente = cabeza;
16            cabeza = n;
17            if (ultimo == null) ultimo = n;
18            tam++;
19        }
20
21        void alFinal(int x) {                         // O(1) gracias a la referencia al último
22            Nodo n = new Nodo(x);
23            if (ultimo == null) cabeza = n;
24            else ultimo.siguiente = n;
25            ultimo = n;
26            tam++;
27        }
28
29        boolean borrar(int x) {                       // O(n): hay que encontrar al anterior
30            Nodo anterior = null, p = cabeza;
31            while (p != null && p.valor != x) { anterior = p; p = p.siguiente; }
32            if (p == null) return false;
33            if (anterior == null) cabeza = p.siguiente;   // era el primero
34            else anterior.siguiente = p.siguiente;        // el anterior se salta a p
35            if (p == ultimo) ultimo = anterior;
36            tam--;
37            return true;
38        }
39
40        @Override public String toString() {
41            StringBuilder sb = new StringBuilder("[");
42            for (Nodo p = cabeza; p != null; p = p.siguiente) sb.append(p.valor).append(p.siguiente != null ? " → " : "");
43            return sb.append("] (").append(tam).append(tam == 1 ? " nodo)" : " nodos)").toString();
44        }
45    }
46
47    public static void main(String[] args) {
48        Lista l = new Lista();
49        l.alFinal(3);
50        l.alFinal(7);
51        l.alPrincipio(1);
52        l.alFinal(9);
53        System.out.println(l);
54        l.borrar(7);
55        System.out.println("Tras borrar el 7: " + l);
56        l.borrar(1);
57        l.borrar(9);
58        System.out.println("Tras borrar el 1 y el 9: " + l);
59    }
60}

Salida al ejecutarlo (la misma en los 5 lenguajes)

[1 → 3 → 7 → 9] (4 nodos)
Tras borrar el 7: [1 → 3 → 9] (3 nodos)
Tras borrar el 1 y el 9: [3] (1 nodo)

Invertir una lista

La pregunta de entrevista más clásica sobre listas: darle la vuelta cambiando solo las flechas, con tres referencias.

Java
1/** Invierte la lista sin crear nodos nuevos: se le da la vuelta a cada flecha. */
2static Nodo invertir(Nodo cabeza) {
3    Nodo anterior = null, actual = cabeza;
4    while (actual != null) {
5        Nodo siguiente = actual.siguiente;   // se guarda antes de perderlo
6        actual.siguiente = anterior;         // la flecha apunta hacia atrás
7        anterior = actual;
8        actual = siguiente;
9    }
10    return anterior;                         // el que era el último es la nueva cabeza
11}

Traza: operaciones sobre una lista vacía

OperaciónListaQué cambia
alFinal(3)3 → nullrecorre la lista hasta el último (O(n)), o usa la referencia al último (O(1))
alFinal(7)3 → 7 → nullrecorre la lista hasta el último (O(n)), o usa la referencia al último (O(1))
alPrincipio(1)1 → 3 → 7 → nullnodo nuevo → antigua cabeza; cabeza → nodo nuevo (O(1))
alFinal(9)1 → 3 → 7 → 9 → nullrecorre la lista hasta el último (O(n)), o usa la referencia al último (O(1))
borrar(7)1 → 3 → 9 → nullel anterior pasa a apuntar al siguiente del borrado
alPrincipio(4)4 → 1 → 3 → 9 → nullnodo nuevo → antigua cabeza; cabeza → nodo nuevo (O(1))

Ningún valor se ha movido nunca de su nodo: solo cambian las referencias.

Complejidad

OperaciónLista enlazadaArray (ArrayList)
Acceder a la posición iO(n)O(1)
Insertar o borrar al principioO(1)O(n)
Insertar o borrar en medio (ya localizado)O(1)O(n)
Insertar al finalO(1) con referencia al últimoO(1) amortizado
Buscar un valorO(n)O(n)

Memoria: O(n), pero cada nodo lleva la sobrecarga de un objeto y de las referencias (en Java, unos 24-40 bytes por nodo frente a 4 de un int en un array).

0102030405015101520tamaño de la entrada (n)operacionesO(n!)O(2ⁿ)O(n²)O(n log n)O(1)O(log n)O(n)
  • Mejor caso: O(1)
  • Caso medio: O(n)
  • Peor caso: O(n)

Insertar o borrar al principio es O(1); buscar, llegar a una posición o insertar al final sin referencia al último es O(n). Las curvas grises son las demás clases, para comparar.

En la práctica

  • LinkedList de Java (doblemente enlazada) implementa List y Deque; aun así, ArrayList y ArrayDeque son más rápidas en casi todos los casos.
  • Las cubetas de HashMap son listas enlazadas (que pasan a árboles si se alargan demasiado).
  • Los sistemas de ficheros FAT encadenan los bloques de un fichero como una lista enlazada.
  • La lista de procesos del núcleo de Linux y las cachés LRU (lista doblemente enlazada + tabla hash) se apoyan en listas enlazadas.

Errores típicos

  • Perder la referencia a la cabeza al recorrer con ella misma (cabeza = cabeza.siguiente): la lista se pierde. Se recorre con otra variable.
  • No comprobar null antes de p.siguiente: NullPointerException con la lista vacía o al llegar al final.
  • Al borrar, no tratar aparte el caso de la cabeza (no tiene anterior).
  • Con referencia al último, olvidar actualizarla al borrar el último nodo.
  • Al invertir, cambiar actual.siguiente sin haber guardado antes el siguiente: se corta la lista.

Ejercicios

Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.

1. Lista ordenada con órdenes

Mantén una lista enlazada siempre ordenada de menor a mayor. Las órdenes llegan una por línea: insertar x mete x en su sitio, borrar x quita la primera aparición de x y mostrar escribe la lista. La lista está hecha con una clase Nodo (nada de ArrayList): completa insertarOrdenado y borrar.

  • Órdenes: insertar 5, borrar 5, mostrar (enteros de hasta 6 cifras, también negativos).
  • mostrar: 1 → 3 → 5 o (vacía). Borrar algo que no está: 5 no está.
  • Otra orden: Orden no válida: «…».
☕JavaLista ordenada con órdenesMedio

Ejemplo

Entrada (lo que se escribe por teclado)
insertar 5
insertar 1
insertar 9
insertar 3
mostrar
Salida esperada
1 → 3 → 5 → 9
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    static class Nodo {
5        int valor;
6        Nodo siguiente;
7        Nodo(int valor, Nodo siguiente) { this.valor = valor; this.siguiente = siguiente; }
8    }
9
10    static Nodo cabeza = null;
11
12    /** Inserta x dejando la lista ordenada de menor a mayor (los repetidos, detrás de sus iguales). */
13    static void insertarOrdenado(int x) {
14        if (cabeza == null || x < cabeza.valor) {
15            cabeza = new Nodo(x, cabeza);
16            return;
17        }
18        Nodo p = cabeza;
19        while (p.siguiente != null && p.siguiente.valor <= x) p = p.siguiente;
20        p.siguiente = new Nodo(x, p.siguiente);
21    }
22
23    /** Borra la primera aparición de x; devuelve si estaba. */
24    static boolean borrar(int x) {
25        if (cabeza == null) return false;
26        if (cabeza.valor == x) {
27            cabeza = cabeza.siguiente;
28            return true;
29        }
30        Nodo p = cabeza;
31        while (p.siguiente != null && p.siguiente.valor != x) p = p.siguiente;
32        if (p.siguiente == null) return false;
33        p.siguiente = p.siguiente.siguiente;
34        return true;
35    }
36
37    static String texto() {
38        if (cabeza == null) return "(vacía)";
39        StringJoiner sj = new StringJoiner(" → ");
40        for (Nodo p = cabeza; p != null; p = p.siguiente) sj.add(String.valueOf(p.valor));
41        return sj.toString();
42    }
43
44    public static void main(String[] args) {
45        Scanner sc = new Scanner(System.in);
46        while (sc.hasNextLine()) {
47            String linea = sc.nextLine().trim();
48            if (linea.isEmpty()) continue;
49            if (linea.matches("insertar -?\\d{1,6}")) insertarOrdenado(Integer.parseInt(linea.substring(9)));
50            else if (linea.matches("borrar -?\\d{1,6}")) {
51                int x = Integer.parseInt(linea.substring(7));
52                if (!borrar(x)) System.out.println(x + " no está");
53            } else if (linea.equals("mostrar")) System.out.println(texto());
54            else System.out.println("Orden no válida: «" + linea + "»");
55        }
56    }
57}

La inserción ordenada es un solo paso de la ordenación por inserción: encontrar el sitio es O(n), pero meter el nodo es O(1), sin desplazar nada.

Avanzar con <= deja los repetidos detrás de sus iguales: la lista conserva el orden de llegada de los iguales.

2. El medio y el k-ésimo desde el final, en una pasada

La primera línea son los valores de la lista, en orden. Cada línea siguiente es una consulta: medio o final k. Respóndelas recorriendo la lista una sola vez y sin contar antes cuántos nodos tiene: para el medio, una «liebre» que avanza de dos en dos y una «tortuga» de uno en uno; para el k-ésimo desde el final, un puntero que sale con k nodos de ventaja. Completa medio y desdeElFinal.

  • Entrada: 1 2 3 4 5 y luego consultas como medio o final 2.
  • Salida: Medio: 3 (con un número par de nodos, el segundo de los dos del medio) y 2º desde el final: 4, o La lista no tiene 9 nodos.
  • Errores: Número no válido: «x», La lista está vacía y Consulta no válida: «…».
☕JavaEl medio y el k-ésimo desde el final, en una pasadaDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
1 2 3 4 5
medio
final 1
final 2
final 5
Salida esperada
Medio: 3
1º desde el final: 5
2º desde el final: 4
5º desde el final: 1
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
⏳
Test oculto #6
0/6 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    static class Nodo {
5        final int valor;
6        Nodo siguiente;
7        Nodo(int valor) { this.valor = valor; }
8    }
9
10    /** El nodo del medio en una sola pasada: la liebre avanza de dos en dos y la tortuga de uno en uno.
11        Con un número par de nodos, el segundo de los dos del medio. */
12    static Nodo medio(Nodo cabeza) {
13        Nodo tortuga = cabeza, liebre = cabeza;
14        while (liebre != null && liebre.siguiente != null) {
15            tortuga = tortuga.siguiente;
16            liebre = liebre.siguiente.siguiente;
17        }
18        return tortuga;
19    }
20
21    /** El k-ésimo contando desde el final (k = 1 es el último), en una sola pasada: un puntero sale con
22        k nodos de ventaja. null si la lista tiene menos de k nodos. */
23    static Nodo desdeElFinal(Nodo cabeza, int k) {
24        Nodo delante = cabeza;
25        for (int i = 0; i < k; i++) {
26            if (delante == null) return null;
27            delante = delante.siguiente;
28        }
29        Nodo detras = cabeza;
30        while (delante != null) {
31            delante = delante.siguiente;
32            detras = detras.siguiente;
33        }
34        return detras;
35    }
36
37    public static void main(String[] args) {
38        Scanner sc = new Scanner(System.in);
39        String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
40        Nodo cabeza = null, ultimo = null;
41        if (!primera.isEmpty()) {
42            for (String t : primera.split("\\s+")) {
43                if (!t.matches("-?\\d{1,6}")) {
44                    System.out.println("Número no válido: «" + t + "»");
45                    return;
46                }
47                Nodo n = new Nodo(Integer.parseInt(t));
48                if (cabeza == null) cabeza = n;
49                else ultimo.siguiente = n;
50                ultimo = n;
51            }
52        }
53        if (cabeza == null) {
54            System.out.println("La lista está vacía");
55            return;
56        }
57        while (sc.hasNextLine()) {
58            String linea = sc.nextLine().trim();
59            if (linea.isEmpty()) continue;
60            if (linea.equals("medio")) System.out.println("Medio: " + medio(cabeza).valor);
61            else if (linea.matches("final [1-9]\\d{0,5}")) {
62                int k = Integer.parseInt(linea.substring(6));
63                Nodo n = desdeElFinal(cabeza, k);
64                System.out.println(n == null ? "La lista no tiene " + k + " nodos" : k + "º desde el final: " + n.valor);
65            } else System.out.println("Consulta no válida: «" + linea + "»");
66        }
67    }
68}

Las dos son la técnica de los dos punteros aplicada a listas: dos referencias que avanzan a distinta velocidad o con ventaja resuelven en una pasada lo que, si no, pediría contar primero los nodos.

La liebre y la tortuga (algoritmo de Floyd) sirven también para detectar si una lista tiene un ciclo: si lo tiene, la liebre acaba alcanzando a la tortuga.

Test

Test: Lista enlazada

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿Cuánto cuesta acceder al elemento i de una lista enlazada?

  2. 2.¿Qué operación es O(1) en una lista enlazada y O(n) en un ArrayList?

  3. 3.Para borrar un nodo de una lista simplemente enlazada, ¿qué hace falta?

  4. 4.¿Qué pasa si se recorre la lista con cabeza = cabeza.siguiente?

  5. 5.¿Qué tipo de lista es java.util.LinkedList?

Relacionado