Listas enlazadas en Java: implementación paso a paso

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 20 min de lectura ✓ Nivel intermedio ★ ★ ★ ★ ★ (5/5)

🔗 ¿Qué es una lista enlazada?

Una lista enlazada (linked list) es una estructura de datos lineal formada por una secuencia de elementos —denominados nodos— en la que cada nodo contiene un dato y una referencia (o puntero) al siguiente nodo de la secuencia. A diferencia de los arrays, los elementos de una lista enlazada no se almacenan en posiciones contiguas de memoria, lo que proporciona una flexibilidad extraordinaria para insertar y eliminar elementos sin necesidad de desplazar los restantes.

Las listas enlazadas constituyen una de las estructuras de datos fundamentales en ciencias de la computación. Comprender su funcionamiento interno resulta imprescindible no solo para resolver problemas algorítmicos, sino también para entender cómo operan internamente colecciones de alto nivel como java.util.LinkedList o las implementaciones de pilas, colas y grafos.

💡 Concepto clave: Cada nodo de una lista enlazada conoce únicamente a su vecino inmediato. Para acceder al tercer elemento, es necesario pasar primero por el primero y el segundo. Esta es la diferencia fundamental con un array, donde se accede a cualquier posición directamente mediante su índice.

🔹 Representación visual

Una lista enlazada simple con tres elementos se puede representar gráficamente como una cadena de cajas conectadas por flechas:

Diagrama
  cabeza
    │
    ▼
┌───────┐    ┌───────┐    ┌───────┐
│ "Ana" │───▶│ "Luis"│───▶│"María"│───▶ null
└───────┘    └───────┘    └───────┘
  nodo 1       nodo 2       nodo 3

La referencia cabeza apunta al primer nodo. El último nodo tiene su referencia siguiente establecida a null, lo que indica el final de la lista. Si la cabeza es null, la lista está vacía.

📋 Tipos de listas enlazadas

Existen tres variantes principales de listas enlazadas, cada una con características y casos de uso específicos:

Tipo Dirección de enlaces Ventaja principal Uso típico
Simple Solo hacia adelante (siguiente) Menor consumo de memoria por nodo Pilas, colas simples, almacenamiento básico
Doblemente enlazada Adelante y atrás (siguiente + anterior) Recorrido bidireccional, eliminación O(1) java.util.LinkedList, editores de texto, historial de navegación
Circular El último apunta al primero No hay final; recorrido cíclico natural Turnos rotativos, buffers circulares, juegos por turnos
Diagramas comparativos
Simple:       [A] ──▶ [B] ──▶ [C] ──▶ null

Doble:  null ◀── [A] ◀──▶ [B] ◀──▶ [C] ──▶ null

Circular:     [A] ──▶ [B] ──▶ [C] ──┐
               ▲                      │
               └──────────────────────┘

📦 El nodo: unidad fundamental

El nodo es el bloque de construcción básico de toda lista enlazada. Cada nodo almacena dos elementos: el dato (la información útil) y una referencia al siguiente nodo. En Java, se implementa como una clase interna con genéricos para máxima reutilización:

Java — Nodo.java
/**
 * Nodo genérico para una lista enlazada simple.
 * @param <T> Tipo de dato almacenado en el nodo
 */
public class Nodo<T> {
    private T dato;             // Información almacenada
    private Nodo<T> siguiente;  // Referencia al próximo nodo

    // Constructor: crea un nodo con dato y referencia al siguiente
    public Nodo(T dato, Nodo<T> siguiente) {
        this.dato = dato;
        this.siguiente = siguiente;
    }

    // Constructor: crea un nodo aislado (siguiente = null)
    public Nodo(T dato) {
        this(dato, null);
    }

    // --- Getters y Setters ---
    public T getDato()                    { return dato; }
    public void setDato(T dato)           { this.dato = dato; }
    public Nodo<T> getSiguiente()         { return siguiente; }
    public void setSiguiente(Nodo<T> sig) { this.siguiente = sig; }

    @Override
    public String toString() {
        return "Nodo{" + dato + "}";
    }
}
✅ Buena práctica: Se utiliza el tipo genérico <T> para que el nodo pueda almacenar cualquier tipo de objeto (Integer, String, Empleado…). Esto es preferible a usar Object, porque el compilador puede verificar los tipos en tiempo de compilación y se evitan castings inseguros.

⚙️ Lista enlazada simple: implementación completa

A continuación se presenta una implementación profesional de una lista enlazada simple genérica con todas las operaciones fundamentales. La clase mantiene una referencia a la cabeza (primer nodo) y un contador de tamanio para obtener el tamaño en tiempo O(1):

Java — ListaEnlazada.java
/**
 * Lista enlazada simple genérica.
 * Operaciones: insertar, eliminar, buscar, recorrer, invertir.
 */
public class ListaEnlazada<T> {
    private Nodo<T> cabeza;   // Primer nodo de la lista
    private int tamanio;       // Número de elementos

    public ListaEnlazada() {
        this.cabeza = null;
        this.tamanio = 0;
    }

    // ¿La lista está vacía?
    public boolean esVacia() {
        return cabeza == null;
    }

    // Número de elementos
    public int tamanio() {
        return tamanio;
    }

    // --- INSERCIÓN ---

    /** Inserta al inicio de la lista — O(1) */
    public void insertarInicio(T dato) {
        cabeza = new Nodo<>(dato, cabeza);
        tamanio++;
    }

    /** Inserta al final de la lista — O(n) */
    public void insertarFinal(T dato) {
        Nodo<T> nuevo = new Nodo<>(dato);
        if (esVacia()) {
            cabeza = nuevo;
        } else {
            Nodo<T> actual = cabeza;
            while (actual.getSiguiente() != null) {
                actual = actual.getSiguiente();
            }
            actual.setSiguiente(nuevo);
        }
        tamanio++;
    }

    /** Inserta en una posición específica (0-indexada) — O(n) */
    public void insertarEn(int posicion, T dato) {
        if (posicion < 0 || posicion > tamanio) {
            throw new IndexOutOfBoundsException(
                "Posición " + posicion + " fuera de rango [0, " + tamanio + "]");
        }
        if (posicion == 0) {
            insertarInicio(dato);
            return;
        }
        Nodo<T> actual = cabeza;
        for (int i = 0; i < posicion - 1; i++) {
            actual = actual.getSiguiente();
        }
        actual.setSiguiente(new Nodo<>(dato, actual.getSiguiente()));
        tamanio++;
    }

    // --- ELIMINACIÓN ---

    /** Elimina el primer nodo — O(1) */
    public T eliminarInicio() {
        if (esVacia()) {
            throw new RuntimeException("Lista vacía");
        }
        T dato = cabeza.getDato();
        cabeza = cabeza.getSiguiente();
        tamanio--;
        return dato;
    }

    /** Elimina el último nodo — O(n) */
    public T eliminarFinal() {
        if (esVacia()) {
            throw new RuntimeException("Lista vacía");
        }
        if (tamanio == 1) {
            return eliminarInicio();
        }
        Nodo<T> actual = cabeza;
        while (actual.getSiguiente().getSiguiente() != null) {
            actual = actual.getSiguiente();
        }
        T dato = actual.getSiguiente().getDato();
        actual.setSiguiente(null);
        tamanio--;
        return dato;
    }

    // --- BÚSQUEDA Y ACCESO ---

    /** Busca un dato y devuelve true si existe — O(n) */
    public boolean contiene(T dato) {
        Nodo<T> actual = cabeza;
        while (actual != null) {
            if (actual.getDato().equals(dato)) {
                return true;
            }
            actual = actual.getSiguiente();
        }
        return false;
    }

    /** Obtiene el dato en una posición — O(n) */
    public T obtener(int posicion) {
        if (posicion < 0 || posicion >= tamanio) {
            throw new IndexOutOfBoundsException(
                "Posición " + posicion + " fuera de rango [0, " + (tamanio - 1) + "]");
        }
        Nodo<T> actual = cabeza;
        for (int i = 0; i < posicion; i++) {
            actual = actual.getSiguiente();
        }
        return actual.getDato();
    }

    // --- UTILIDADES ---

    /** Invierte la lista in-place — O(n) */
    public void invertir() {
        Nodo<T> anterior = null;
        Nodo<T> actual = cabeza;
        while (actual != null) {
            Nodo<T> siguiente = actual.getSiguiente();
            actual.setSiguiente(anterior);
            anterior = actual;
            actual = siguiente;
        }
        cabeza = anterior;
    }

    /** Representación textual de la lista */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        Nodo<T> actual = cabeza;
        while (actual != null) {
            sb.append(actual.getDato());
            if (actual.getSiguiente() != null) {
                sb.append(" → ");
            }
            actual = actual.getSiguiente();
        }
        return sb.append("]").toString();
    }
}

🛠️ Operaciones básicas

Veamos en acción cada una de las operaciones de la lista enlazada con un programa principal que demuestra su funcionamiento:

Java — DemoListaEnlazada.java
public class DemoListaEnlazada {
    public static void main(String[] args) {
        ListaEnlazada<String> colores = new ListaEnlazada<>();

        // --- Inserción ---
        colores.insertarInicio("verde");         // [verde]
        colores.insertarInicio("rojo");          // [rojo → verde]
        colores.insertarFinal("azul");           // [rojo → verde → azul]
        colores.insertarEn(1, "amarillo");       // [rojo → amarillo → verde → azul]

        System.out.println("Lista: " + colores);
        System.out.println("Tamaño: " + colores.tamanio());

        // --- Búsqueda ---
        System.out.println("¿Contiene 'verde'? " + colores.contiene("verde"));
        System.out.println("Elemento en pos. 2: " + colores.obtener(2));

        // --- Eliminación ---
        String primero = colores.eliminarInicio();
        System.out.println("Eliminado inicio: " + primero);
        String ultimo = colores.eliminarFinal();
        System.out.println("Eliminado final: " + ultimo);
        System.out.println("Lista: " + colores);

        // --- Inversión ---
        colores.insertarFinal("naranja");
        colores.insertarFinal("morado");
        System.out.println("Antes de invertir: " + colores);
        colores.invertir();
        System.out.println("Después de invertir: " + colores);
    }
}
// Salida:
// Lista: [rojo → amarillo → verde → azul]
// Tamaño: 4
// ¿Contiene 'verde'? true
// Elemento en pos. 2: verde
// Eliminado inicio: rojo
// Eliminado final: azul
// Lista: [amarillo → verde]
// Antes de invertir: [amarillo → verde → naranja → morado]
// Después de invertir: [morado → naranja → verde → amarillo]

🔹 Complejidad temporal de las operaciones

Operación Lista simple Con ref. al final Lista doble
Insertar al inicioO(1)O(1)O(1)
Insertar al finalO(n)O(1)O(1)
Eliminar al inicioO(1)O(1)O(1)
Eliminar al finalO(n)O(n)O(1)
Buscar / Acceder por índiceO(n)O(n)O(n)
Insertar en posición conocidaO(1)*O(1)*O(1)*

* O(1) si ya se dispone de la referencia al nodo anterior. Localizar esa posición cuesta O(n).

↔️ Lista doblemente enlazada

En una lista doblemente enlazada (doubly linked list), cada nodo contiene tres campos: el dato, una referencia al nodo siguiente y otra al nodo anterior. Esto permite recorrer la lista en ambas direcciones y eliminar un nodo en O(1) si se conoce su referencia, ya que no es necesario localizar el nodo previo.

Java — NodoDoble.java
/**
 * Nodo para lista doblemente enlazada.
 */
public class NodoDoble<T> {
    private T dato;
    private NodoDoble<T> siguiente;
    private NodoDoble<T> anterior;

    public NodoDoble(T dato) {
        this.dato = dato;
        this.siguiente = null;
        this.anterior = null;
    }

    // Getters y setters
    public T getDato()                          { return dato; }
    public NodoDoble<T> getSiguiente()          { return siguiente; }
    public void setSiguiente(NodoDoble<T> sig)  { this.siguiente = sig; }
    public NodoDoble<T> getAnterior()           { return anterior; }
    public void setAnterior(NodoDoble<T> ant)   { this.anterior = ant; }
}
Java — ListaDoble.java (operaciones clave)
public class ListaDoble<T> {
    private NodoDoble<T> cabeza;
    private NodoDoble<T> cola;    // Referencia al último nodo
    private int tamanio;

    public ListaDoble() {
        cabeza = null;
        cola = null;
        tamanio = 0;
    }

    /** Inserta al inicio — O(1) */
    public void insertarInicio(T dato) {
        NodoDoble<T> nuevo = new NodoDoble<>(dato);
        if (cabeza == null) {
            cabeza = cola = nuevo;
        } else {
            nuevo.setSiguiente(cabeza);
            cabeza.setAnterior(nuevo);
            cabeza = nuevo;
        }
        tamanio++;
    }

    /** Inserta al final — O(1) gracias a la referencia 'cola' */
    public void insertarFinal(T dato) {
        NodoDoble<T> nuevo = new NodoDoble<>(dato);
        if (cola == null) {
            cabeza = cola = nuevo;
        } else {
            cola.setSiguiente(nuevo);
            nuevo.setAnterior(cola);
            cola = nuevo;
        }
        tamanio++;
    }

    /** Elimina un nodo específico — O(1) si se conoce la referencia */
    public void eliminarNodo(NodoDoble<T> nodo) {
        if (nodo == null) return;
        if (nodo.getAnterior() != null) {
            nodo.getAnterior().setSiguiente(nodo.getSiguiente());
        } else {
            cabeza = nodo.getSiguiente(); // era la cabeza
        }
        if (nodo.getSiguiente() != null) {
            nodo.getSiguiente().setAnterior(nodo.getAnterior());
        } else {
            cola = nodo.getAnterior();    // era la cola
        }
        tamanio--;
    }

    /** Recorre de inicio a fin */
    public String recorrerAdelante() {
        StringBuilder sb = new StringBuilder("[");
        NodoDoble<T> actual = cabeza;
        while (actual != null) {
            sb.append(actual.getDato());
            if (actual.getSiguiente() != null) sb.append(" ↔ ");
            actual = actual.getSiguiente();
        }
        return sb.append("]").toString();
    }

    /** Recorre de fin a inicio */
    public String recorrerAtras() {
        StringBuilder sb = new StringBuilder("[");
        NodoDoble<T> actual = cola;
        while (actual != null) {
            sb.append(actual.getDato());
            if (actual.getAnterior() != null) sb.append(" ↔ ");
            actual = actual.getAnterior();
        }
        return sb.append("]").toString();
    }
}
💡 Ventaja clave: En una lista doblemente enlazada, eliminar un nodo del que ya se tiene referencia es O(1), porque se puede acceder tanto a su anterior como a su siguiente directamente. En una lista simple, esta operación sería O(n) porque habría que recorrer la lista para encontrar el nodo anterior.

🔄 Lista circular

En una lista circular, el último nodo no apunta a null sino que apunta de vuelta al primer nodo, formando un ciclo cerrado. Esta variante resulta especialmente útil para modelar estructuras cíclicas como turnos rotativos, buffers circulares o planificadores de procesos round-robin.

Java — ListaCircular.java
/**
 * Lista circular simple.
 * El último nodo siempre apunta al primero.
 */
public class ListaCircular<T> {
    private Nodo<T> ultimo;   // Referencia al último nodo
    private int tamanio;

    public ListaCircular() {
        ultimo = null;
        tamanio = 0;
    }

    /** Inserta al inicio (después de último, antes del primero) — O(1) */
    public void insertarInicio(T dato) {
        Nodo<T> nuevo = new Nodo<>(dato);
        if (ultimo == null) {
            ultimo = nuevo;
            nuevo.setSiguiente(nuevo);  // Apunta a sí mismo
        } else {
            nuevo.setSiguiente(ultimo.getSiguiente());  // nuevo → primero
            ultimo.setSiguiente(nuevo);                  // ultimo → nuevo
        }
        tamanio++;
    }

    /** Inserta al final — O(1) */
    public void insertarFinal(T dato) {
        insertarInicio(dato);
        ultimo = ultimo.getSiguiente();  // El nuevo pasa a ser el último
    }

    /** Recorrido completo (una vuelta) */
    @Override
    public String toString() {
        if (ultimo == null) return "[]";
        StringBuilder sb = new StringBuilder("[");
        Nodo<T> actual = ultimo.getSiguiente(); // Empieza en el primero
        do {
            sb.append(actual.getDato());
            actual = actual.getSiguiente();
            if (actual != ultimo.getSiguiente()) sb.append(" → ");
        } while (actual != ultimo.getSiguiente());
        return sb.append("] (circular)").toString();
    }
}
⚠️ Cuidado: En una lista circular, un recorrido con while (actual != null) produciría un bucle infinito, ya que ningún nodo apunta a null. Siempre se debe usar una condición de parada basada en comparar con el nodo de inicio: do { ... } while (actual != inicio).

☕ LinkedList en la API de Java

Java proporciona la clase java.util.LinkedList<E> como parte del Java Collections Framework. Internamente es una lista doblemente enlazada que implementa las interfaces List, Deque y Queue, lo que la convierte en una estructura extremadamente versátil.

Java
import java.util.LinkedList;

public class DemoLinkedListAPI {
    public static void main(String[] args) {
        LinkedList<String> ciudades = new LinkedList<>();

        // --- Como List ---
        ciudades.add("Madrid");            // Inserta al final
        ciudades.add("Barcelona");
        ciudades.addFirst("Sevilla");      // Inserta al inicio
        ciudades.add(1, "Valencia");       // Inserta en posición 1

        System.out.println("Lista: " + ciudades);
        // [Sevilla, Valencia, Madrid, Barcelona]

        // --- Como Deque (doble cola) ---
        ciudades.offerFirst("Bilbao");     // Inserta al inicio
        ciudades.offerLast("Málaga");      // Inserta al final
        System.out.println("Primero: " + ciudades.peekFirst());  // Bilbao
        System.out.println("Último: " + ciudades.peekLast());    // Málaga

        // --- Eliminación ---
        ciudades.removeFirst();            // Elimina Bilbao
        ciudades.removeLast();             // Elimina Málaga
        ciudades.remove("Madrid");         // Elimina Madrid por valor

        // --- Recorrido con for-each ---
        System.out.println("Ciudades restantes:");
        for (String ciudad : ciudades) {
            System.out.println("  • " + ciudad);
        }

        // --- Búsqueda ---
        System.out.println("¿Contiene Valencia? " + ciudades.contains("Valencia"));
        System.out.println("Índice de Barcelona: " + ciudades.indexOf("Barcelona"));
    }
}
// Salida:
// Lista: [Sevilla, Valencia, Madrid, Barcelona]
// Primero: Bilbao
// Último: Málaga
// Ciudades restantes:
//   • Sevilla
//   • Valencia
//   • Barcelona
// ¿Contiene Valencia? true
// Índice de Barcelona: 2

⚖️ ArrayList vs. LinkedList

Una de las preguntas más frecuentes en entrevistas técnicas y exámenes de programación es cuándo usar ArrayList y cuándo LinkedList. La respuesta depende del patrón de operaciones predominante:

Operación ArrayList LinkedList Ganador
Acceso por índice (get(i))O(1)O(n)ArrayList
Insertar al final (add(e))O(1) amortizadoO(1)Empate
Insertar al inicioO(n)O(1)LinkedList
Eliminar al inicioO(n)O(1)LinkedList
Insertar en medioO(n)O(n)*Depende
Uso de memoriaCompactoMayor (refs. extra)ArrayList
Localidad de cachéExcelentePobreArrayList

* LinkedList tiene O(1) para insertar si ya se dispone de un iterador en la posición, pero localizar esa posición cuesta O(n).

✅ Regla práctica: En la mayoría de aplicaciones, ArrayList es la mejor elección por defecto. Use LinkedList solo cuando las inserciones y eliminaciones frecuentes en los extremos sean la operación dominante (por ejemplo, implementando una cola o un deque), o cuando implemente pilas y colas manuales con propósitos académicos.

🏗️ Ejemplo integrador: gestión de tareas con prioridad

El siguiente ejemplo reúne todos los conceptos del artículo en un sistema práctico de gestión de tareas que utiliza una lista enlazada para mantener las tareas ordenadas por prioridad. Cada tarea se inserta en la posición correcta según su prioridad, manteniendo la lista siempre ordenada.

Java — GestorTareas.java
import java.util.LinkedList;
import java.util.Iterator;

/**
 * Sistema de gestión de tareas con prioridad.
 * Utiliza LinkedList de la API de Java para demostrar
 * inserción ordenada, eliminación y recorrido.
 */
public class GestorTareas {

    // Clase interna: representa una tarea
    static class Tarea implements Comparable<Tarea> {
        private String nombre;
        private int prioridad;   // 1 = máxima, 5 = mínima
        private boolean completada;

        public Tarea(String nombre, int prioridad) {
            this.nombre = nombre;
            this.prioridad = prioridad;
            this.completada = false;
        }

        public void completar() { this.completada = true; }

        @Override
        public int compareTo(Tarea otra) {
            return Integer.compare(this.prioridad, otra.prioridad);
        }

        @Override
        public String toString() {
            String estado = completada ? "✓" : "○";
            return String.format("[%s] P%d: %s", estado, prioridad, nombre);
        }
    }

    private LinkedList<Tarea> tareas = new LinkedList<>();

    /** Inserta tarea manteniendo orden por prioridad */
    public void agregarTarea(String nombre, int prioridad) {
        Tarea nueva = new Tarea(nombre, prioridad);
        int posicion = 0;
        for (Tarea t : tareas) {
            if (nueva.compareTo(t) < 0) break;
            posicion++;
        }
        tareas.add(posicion, nueva);
        System.out.println("+ Añadida: " + nueva);
    }

    /** Completa la tarea de mayor prioridad (primera) */
    public void completarSiguiente() {
        if (tareas.isEmpty()) {
            System.out.println("No hay tareas pendientes.");
            return;
        }
        Tarea t = tareas.getFirst();
        t.completar();
        System.out.println("Completada: " + t);
        tareas.removeFirst();
    }

    /** Elimina todas las tareas completadas */
    public void limpiarCompletadas() {
        Iterator<Tarea> it = tareas.iterator();
        int eliminadas = 0;
        while (it.hasNext()) {
            if (it.next().completada) {
                it.remove();
                eliminadas++;
            }
        }
        System.out.println("Limpiadas " + eliminadas + " tareas completadas.");
    }

    /** Muestra el estado actual */
    public void mostrarTareas() {
        System.out.println("\n=== Tareas pendientes (" + tareas.size() + ") ===");
        if (tareas.isEmpty()) {
            System.out.println("  (ninguna)");
        } else {
            for (Tarea t : tareas) {
                System.out.println("  " + t);
            }
        }
        System.out.println();
    }

    // --- Programa principal ---
    public static void main(String[] args) {
        GestorTareas gestor = new GestorTareas();

        // Inserción ordenada por prioridad
        gestor.agregarTarea("Corregir bug crítico en producción", 1);
        gestor.agregarTarea("Escribir documentación del API", 3);
        gestor.agregarTarea("Actualizar dependencias Maven", 4);
        gestor.agregarTarea("Revisar pull request del equipo", 2);
        gestor.agregarTarea("Preparar demo para el cliente", 1);

        gestor.mostrarTareas();

        // Completar las dos tareas más urgentes
        gestor.completarSiguiente();
        gestor.completarSiguiente();

        gestor.mostrarTareas();
    }
}
// Salida:
// + Añadida: [○] P1: Corregir bug crítico en producción
// + Añadida: [○] P3: Escribir documentación del API
// + Añadida: [○] P4: Actualizar dependencias Maven
// + Añadida: [○] P2: Revisar pull request del equipo
// + Añadida: [○] P1: Preparar demo para el cliente
//
// === Tareas pendientes (5) ===
//   [○] P1: Corregir bug crítico en producción
//   [○] P1: Preparar demo para el cliente
//   [○] P2: Revisar pull request del equipo
//   [○] P3: Escribir documentación del API
//   [○] P4: Actualizar dependencias Maven
//
// Completada: [✓] P1: Corregir bug crítico en producción
// Completada: [✓] P1: Preparar demo para el cliente
//
// === Tareas pendientes (3) ===
//   [○] P2: Revisar pull request del equipo
//   [○] P3: Escribir documentación del API
//   [○] P4: Actualizar dependencias Maven

✏️ Ejercicios resueltos

📝 Ejercicio 1 — Contar nodos de una lista enlazada

Implementa un método contar() que recorra una lista enlazada simple y devuelva el número de nodos, sin usar la variable tamanio. El método debe funcionar recorriendo los nodos secuencialmente.

Ver solución
Java
/** Cuenta los nodos recorriendo la lista — O(n) */
public int contar() {
    int contador = 0;
    Nodo<T> actual = cabeza;
    while (actual != null) {
        contador++;
        actual = actual.getSiguiente();
    }
    return contador;
}

// Versión recursiva
public int contarRecursivo(Nodo<T> nodo) {
    if (nodo == null) return 0;
    return 1 + contarRecursivo(nodo.getSiguiente());
}

📝 Ejercicio 2 — Detectar si una lista tiene un ciclo

Implementa un método que determine si una lista enlazada contiene un ciclo (es decir, algún nodo apunta a un nodo anterior). Utiliza el algoritmo de Floyd (tortuga y liebre) con dos punteros que avanzan a distinta velocidad.

Ver solución
Java
/**
 * Detecta ciclos usando el algoritmo de Floyd.
 * - Tortuga avanza 1 nodo por paso
 * - Liebre avanza 2 nodos por paso
 * - Si se encuentran, hay ciclo
 * Complejidad: O(n) tiempo, O(1) espacio
 */
public boolean tieneCiclo() {
    if (cabeza == null) return false;

    Nodo<T> tortuga = cabeza;
    Nodo<T> liebre = cabeza;

    while (liebre != null && liebre.getSiguiente() != null) {
        tortuga = tortuga.getSiguiente();           // 1 paso
        liebre = liebre.getSiguiente().getSiguiente(); // 2 pasos

        if (tortuga == liebre) {
            return true;  // Se encontraron → hay ciclo
        }
    }
    return false;  // Liebre llegó a null → no hay ciclo
}

El algoritmo funciona porque si hay un ciclo, la liebre (más rápida) acabará «dando la vuelta» y alcanzando a la tortuga dentro del ciclo. Si no hay ciclo, la liebre llegará a null y el bucle terminará.

📝 Ejercicio 3 — Encontrar el n-ésimo nodo desde el final

Dado un número n, encuentra el n-ésimo nodo contando desde el final de la lista en una sola pasada (sin conocer previamente el tamaño). Pista: utiliza la técnica de dos punteros con separación fija.

Ver solución
Java
/**
 * Devuelve el n-ésimo nodo desde el final (1-indexado).
 * Técnica: dos punteros separados por n posiciones.
 * Cuando el adelantado llega a null, el atrasado
 * está en la posición deseada.
 */
public T obtenerDesdeElFinal(int n) {
    if (n <= 0) throw new IllegalArgumentException("n debe ser > 0");

    Nodo<T> adelantado = cabeza;
    Nodo<T> atrasado = cabeza;

    // Adelantar el primer puntero n posiciones
    for (int i = 0; i < n; i++) {
        if (adelantado == null) {
            throw new IndexOutOfBoundsException(
                "La lista tiene menos de " + n + " elementos");
        }
        adelantado = adelantado.getSiguiente();
    }

    // Avanzar ambos hasta que el adelantado llegue al final
    while (adelantado != null) {
        adelantado = adelantado.getSiguiente();
        atrasado = atrasado.getSiguiente();
    }

    return atrasado.getDato();
}

// Ejemplo de uso:
// Lista: [10 → 20 → 30 → 40 → 50]
// obtenerDesdeElFinal(1) → 50 (último)
// obtenerDesdeElFinal(3) → 30 (tercero desde el final)

❓ Preguntas frecuentes sobre Listas enlazadas en Java: implementación paso a paso

Las dudas más comunes respondidas de forma clara y directa.

Una lista enlazada es una estructura de datos lineal formada por nodos conectados mediante referencias (punteros). A diferencia de un array, no almacena los elementos en posiciones contiguas de memoria, lo que permite inserciones y eliminaciones en tiempo O(1) en cualquier posición conocida, pero el acceso por índice requiere tiempo O(n) porque hay que recorrer los nodos secuencialmente.
LinkedList es preferible cuando las operaciones más frecuentes son inserciones y eliminaciones en los extremos o en posiciones intermedias ya localizadas, especialmente con grandes volúmenes de datos. ArrayList es mejor para acceso aleatorio por índice y cuando el tamaño de la colección no cambia frecuentemente. En la práctica, ArrayList suele ser más rápido en la mayoría de escenarios gracias a la localidad de caché de memoria.
Los tres tipos principales son: lista enlazada simple (cada nodo apunta al siguiente), lista doblemente enlazada (cada nodo apunta al siguiente y al anterior, lo que permite recorrido bidireccional) y lista circular (el último nodo apunta de vuelta al primero, formando un ciclo). Java implementa internamente LinkedList como una lista doblemente enlazada.
La inserción y eliminación al inicio son O(1). La inserción y eliminación al final son O(1) si se mantiene una referencia al último nodo, o O(n) si no. La búsqueda y el acceso por índice son siempre O(n) porque requieren recorrer la lista. En una lista doblemente enlazada, la eliminación de un nodo conocido es O(1).
Se utilizan genéricos (generics) de Java para crear una clase Nodo que almacene un dato de tipo T y una referencia al siguiente nodo, y una clase ListaEnlazada que gestione las operaciones. El parámetro de tipo T permite que la misma implementación funcione con Integer, String, o cualquier otro tipo de objeto.
La clase java.util.LinkedList es una lista doblemente enlazada que además implementa las interfaces List y Deque. Cada nodo interno tiene referencias tanto al siguiente como al anterior, lo que permite operaciones eficientes en ambos extremos y recorrido bidireccional mediante iteradores.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Listas enlazadas en Java: implementación paso a paso? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!