TL;DR
- El Problema: Optimización de complejidad temporal y espacial para estructuras de datos clave.
- El Enfoque: Encuentra el k-ésimo nodo desde el final de una lista enlazada simple. Recorre el clásico hueco de k con dos punteros y un wrapper recursivo breve, en Java claro.
- Complejidad: Relación óptima de tiempo y espacio con gestión de casos límite.
Tú y un amigo caminan por un sendero en fila india. Tu amigo arranca k pasos por delante. Cuando llega al final del camino, tú estás sobre la k-ésima piedra contando desde el final. No necesitaste la longitud total. Solo el hueco.
Eso es Return Kth to Last: encontrar el nodo que está a k posiciones del final de una lista enlazada simple. Definimos k = 1 como el último elemento.
Este es el problema estilo CTCI 2.2, Capítulo 2 (Linked Lists). Solución principal: dos punteros iterativos. Opcional: recursivo con un wrapper de índice pequeño. Enseñanza original en Java, no un pegado del libro.
Serie: CTCI en Java. Anterior: 2.1 Remove Dups. Siguiente: 2.3 Delete Middle Node.
Imagen cotidiana
Un tren de vagones, de cabeza a cola. Solo avanzas. No hay marcha atrás ni un número pintado en cada vagón.
Alguien pregunta: "Dame el 2.º vagón desde el furgón de cola." Si supieras la longitud n, caminarías n - 2 pasos desde la cabeza. Aún no conoces n. Contar una vez para sacar n y luego caminar otra vez funciona. También son dos pasadas completas.
Mejor: manda un explorador k vagones por delante. Luego mueve al explorador y a ti a la vez, un vagón cada vez. Cuando el explorador se cae al final, tu vagón es el k-ésimo desde el final.
El problema en palabras simples
Entrada: la cabeza de una lista enlazada simple y un entero positivo k.
Salida: el nodo que es el k-ésimo desde el final. Con nuestra convención, k = 1 devuelve el último nodo, k = 2 el penúltimo, y así.
Ejemplos (lista dibujada cabeza → cola):
| Lista | k | Resultado | Por qué |
|---|---|---|---|
1 → 2 → 3 → 4 → 5 |
1 | nodo 5 |
último elemento |
1 → 2 → 3 → 4 → 5 |
2 | nodo 4 |
segundo desde el final |
1 → 2 → 3 → 4 → 5 |
5 | nodo 1 |
k igual a la longitud |
1 → 2 → 3 |
4 | null (o error) | k mayor que la longitud |
7 |
1 | nodo 7 |
un solo nodo, el último es él mismo |
Aclara en voz alta antes de codificar:
- ¿
k = 1es el último nodo? (Sí aquí. Algunos equipos usan base 0. Pregunta.) - ¿Qué pasa si
kes mayor que la longitud? ¿null, excepción o un centinela? Elige uno. Nosotros devolvemosnull. - ¿Devolver el nodo o solo su valor? En entrevistas suele pedirse el nodo para seguir encadenando.
- ¿Cabeza null? Lista vacía → null.
Cómo pensar antes de codificar
Fuerza bruta: longitud y luego caminar
- Recorre la lista una vez, cuenta
n. - Si
k > n, falla. - Recorre otra vez
n - kpasos desde la cabeza.
Correcto. Dos pasadas. Vale si el entrevistador acepta O(n) y dos viajes. Muchos después preguntan: ¿se puede en una pasada?
Una pasada: dos punteros con un hueco de k
- Los punteros
p1yp2empiezan enhead. - Avanza
p1exactamentekpasos. Si te caes antes,kes demasiado grande. - Avanza
p1yp2juntos hasta quep1sea null. p2queda en el k-ésimo desde el final.
Por qué funciona: cuando p1 ha recorrido el resto del sufijo, p2 se ha mantenido exactamente k nodos detrás del "final". El final es uno más allá del último nodo, así que p2 está en el k-ésimo desde el final.
Traza 1 → 2 → 3 → 4 → 5, k = 2:
| Paso | p1 | p2 |
|---|---|---|
| inicio | 1 | 1 |
| avanza p1 una vez | 2 | 1 |
| avanza p1 dos veces | 3 | 1 |
| mueven ambos | 4 | 2 |
| mueven ambos | 5 | 3 |
| mueven ambos | null | 4 |
p2 es 4. Listo.
Idea recursiva (opcional)
Recurre hasta el final. Al volver, cuenta cuántos nodos has pasado. Cuando el contador llega a k, ese nodo es la respuesta. Necesitas un contador compartido (o un wrapper pequeño), porque un int solo no puede llevar a la vez "el conteo" y "el nodo respuesta" en Java sin un tipo auxiliar.
La recursión queda elegante si sabes explicar la pila. Prefiere la versión iterativa de dos punteros como respuesta principal: O(1) de espacio extra, sin riesgo de pila en listas largas.
Solución en Java
Tipo Node
/** Singly linked list node. Original teaching model for this series. */
public class Node {
public int data;
public Node next;
public Node(int data) {
this.data = data;
}
}
Respuesta principal: dos punteros iterativos
/**
* Returns the kth node from the end of the list.
* k = 1 means the last node. Returns null if the list is too short
* or inputs are invalid.
*/
public static Node kthToLast(Node head, int k) {
if (head == null || k < 1) {
return null;
}
Node p1 = head;
Node p2 = head;
// Open a gap of k between p1 and p2.
for (int i = 0; i < k; i++) {
if (p1 == null) {
// k is larger than the number of nodes.
return null;
}
p1 = p1.next;
}
// When p1 walks off the end, p2 is k nodes from the end.
while (p1 != null) {
p1 = p1.next;
p2 = p2.next;
}
return p2;
}
Construye una lista pequeña y llámalo:
// 1 → 2 → 3 → 4 → 5
Node head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
head.next.next.next = new Node(4);
head.next.next.next.next = new Node(5);
Node ans = kthToLast(head, 2); // data == 4
Opcional: recursivo con wrapper de índice
/** Mutable counter so recursion can share one index on the way back. */
static class Index {
int value = 0;
}
/**
* Recursive kth-to-last. Same k convention: k = 1 is the last node.
* Uses O(n) stack space. Prefer kthToLast for production-sized lists.
*/
public static Node kthToLastRecursive(Node head, int k) {
if (k < 1) {
return null;
}
return kthToLastRecursive(head, k, new Index());
}
private static Node kthToLastRecursive(Node head, int k, Index idx) {
if (head == null) {
return null;
}
Node candidate = kthToLastRecursive(head.next, k, idx);
idx.value += 1;
if (idx.value == k) {
return head;
}
return candidate;
}
Al deshacer la pila, el último nodo recibe conteo 1, el anterior 2, y así. Cuando el conteo es k, devuelve ese nodo. Los nodos más cerca de la cabeza siguen devolviendo el candidato que ya encontraron (o null si k era demasiado grande).
Complejidad
| Enfoque | Tiempo | Espacio extra | Notas |
|---|---|---|---|
| Longitud y luego caminar | O(n) | O(1) | Dos pasadas |
| Hueco de dos punteros | O(n) | O(1) | Una pasada, respuesta principal |
| Índice recursivo | O(n) | O(n) pila | Bien mencionarlo, no el default a enviar |
En el peor caso debes mirar cada nodo (o al menos lo bastante para colocar ambos punteros), así que el tiempo lineal es el orden correcto.
Casos borde que tocan los entrevistadores
- Cabeza null. Lista vacía. Devuelve null.
- k menor que 1. Inválido. Devuelve null (o lanza). Di el contrato.
- k mayor que la longitud. Antes de k avances,
p1es null. Devuelve null. - k igual a la longitud. Tras k avances,
p1es null. El paseo conjunto no corre.p2se queda en head. Correcto: head es el k-ésimo desde el final. - k = 1. Último nodo. Hueco de uno:
p1empieza un paso adelante, ambos caminan hasta quep1es null,p2cae en el último nodo real. - Un solo nodo, k = 1. Funciona. Un solo nodo, k = 2: falla.
- No mutes la lista. Este problema es de solo lectura. No toques los
next. - Off-by-one en el hueco. El bug clásico es avanzar
k - 1ok + 1por accidente. Traza k = 1 y k = n en papel antes de hablar.
Errores comunes
- Contar desde el frente como "k-ésimo nodo" en vez del k-ésimo desde el final.
- Usar un modelo base 0 (
k = 0es el último) sin decirlo. La sala se confunde. - Avanzar el corredor
k - 1veces cuando tu definición es k = 1 último. Quédate con "avanza k veces, luego caminan juntos hasta que el corredor sea null." - Olvidar el chequeo de null al abrir el hueco y luego NPE si
kes enorme. - Devolver
p2.datacuando pidieron el nodo.
Resumen para contárselo a un amigo
Quieres el k-ésimo vagón desde el final y solo caminas hacia delante.
Manda un explorador k vagones por delante. Caminen a la par. Cuando el explorador se cae del tren, tú estás en el k-ésimo vagón desde el final. No hace falta una variable de longitud.
Versión recursiva: ve al final, cuenta al volver, agarra el nodo cuando el conteo llega a k. Misma idea, pila en vez de un segundo puntero.
Entrega la versión de dos punteros. Menciona la recursión si piden otro ángulo.
Práctica
- Codifica
kthToLastde memoria. Traza k = 1, k = 2 y k = n sobre1 → 2 → 3 → 4 → 5. - Implementa la versión longitud-y-caminar y demuestra que ambas devuelven el mismo nodo.
- Escribe el wrapper recursivo y explica por qué hace falta un
Indexcompartido (o unint[]) en Java. - Rompe tu propio código con k = 0, lista vacía y k mayor que la longitud.
Anterior: 2.1 Remove Dups. Siguiente: 2.3 Delete Middle Node. Mapa de la serie: CTCI en Java.
