Si estás preparándote para entrevistas técnicas, tarde o temprano vas a encontrarte con problemas de arrays y strings. Y varios se resuelven con una misma familia de técnicas: two pointers.
En los artículos anteriores vimos cómo funcionan los arrays y los strings por debajo: memoria contigua, acceso por índice, operaciones y sus costos. Hoy toca usar todo eso para resolver problemas reales.
Lo que vas a encontrar en este artículo:
- Qué son las entrevistas técnicas y por qué te piden resolver problemas
- Qué es two pointers y qué técnicas incluye
- Cómo identificar cuál técnica usar
- Un problema resuelto paso a paso por cada técnica
¿Qué son los problemas de entrevistas técnicas?
Son ejercicios de programación que te ponen en entrevistas para puestos de desarrollo de software. Ojo, las entrevistas técnicas existen para muchos roles (arquitectura, DevOps, SRE, developer) y no todas se ven igual. Este artículo se enfoca en las entrevistas para roles de developer.
El objetivo no es que te sepas la respuesta de memoria. Lo que evalúan es cómo piensas: cómo descompones un problema, qué estructura de datos eliges y si tu solución es eficiente.
Y aquí viene lo interesante: existen técnicas que se crearon específicamente para resolver estos problemas de forma óptima. No es magia, es práctica y reconocimiento de patrones.
¿Qué es Two Pointers?
Two pointers es un paradigma general para resolver problemas con arrays y strings. La idea es simple: usas dos variables (los "punteros") que recorren la estructura de datos de formas específicas.
Ojo, cuando digo "punteros" no me refiero a punteros de memoria como en C. Aquí son simplemente variables que guardan índices para acceder a posiciones del array o string.
¿Por qué funciona? Porque con dos punteros moviéndose de forma inteligente, puedes evitar soluciones de fuerza bruta O(n²) y resolverlo en O(n). Eso es exactamente lo que buscan en una entrevista.
¿Qué técnicas incluye?
Two pointers incluye tres variantes principales:
- Fast and slow pointers (lento y rápido): un puntero explora y otro se queda marcando una posición.
- Opposite direction pointers (punteros convergentes): empiezan en los extremos y se acercan al centro.
- Parallel pointers (punteros paralelos): cada puntero recorre un array diferente al mismo tiempo.
Lo que todas tienen en común: usan al menos 2 variables para recorrer un arreglo o string, y nos ayudan a pasar de O(n²) a O(n).
Vamos a usar los nombres en inglés porque así los encuentras en la mayoría de recursos, LeetCode y entrevistas.
¿Cómo saber cuál técnica usar?
Antes de entrar a los problemas, una guía rápida. Si en el enunciado ves algo de esto, probablemente necesitas two pointers:
- Te dan uno o dos arreglos/strings.
- Te piden espacio O(1) o modificar el array en su lugar.
- La solución obvia sería O(n²) pero necesitas algo mejor.
Ahora, ¿cuál variante? Aquí va un resumen:
| Técnica | Úsala cuando... |
|---|---|
| Fast and slow | Necesitas comparar o modificar elementos dentro del mismo array |
| Opposite direction | Necesitas verificar propiedades simétricas, encontrar pares, o trabajar desde los extremos |
| Parallel pointers | Tienes dos arrays y necesitas compararlos o fusionarlos |
Sé que puede parecer mucho, pero conforme resolvamos los problemas vas a ver que es más intuitivo de lo que parece. Vamos a eso.
Opposite direction: Verificar un palíndromo
Empecemos con opposite direction porque es la más visual.
Tomemos el problema "Valid Palindrome" de LeetCode:
Problema:
Una frase es un palíndromo si, tras convertir todas las letras a minúsculas y eliminar los caracteres no alfanuméricos, se lee igual de izquierda a derecha que de derecha a izquierda.
Dada una cadena s, devuelve true si es un palíndromo, o false en caso contrario.
Ejemplos
Input: s = "A man, a plan, a canal: Panama"
Output: true
// Porque "amanaplanacanalpanama" se lee igual en ambas direcciones
Input: s = "race a car"
Output: false
// Porque "raceacar" no es palíndromo
Input: s = " "
Output: true
// Un string vacío se lee igual al derecho y al revés
¿Cómo lo pensamos?
La solución de fuerza bruta sería limpiar el string, invertirlo y comparar. Funciona, pero crea strings nuevos en memoria (recuerda que los strings son inmutables, así que cada operación crea uno nuevo).
Con opposite direction pointers hacemos algo más elegante: ponemos un puntero al inicio y otro al final. Los vamos acercando al centro comparando los caracteres. Si en algún momento no coinciden, no es palíndromo. Si los punteros se cruzan sin encontrar diferencias, sí lo es.
Solución paso a paso
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function (s) {
let l = 0;
let r = s.length - 1;
while (l < r) {
if (!isAlphanumeric(s[l])) {
l++;
} else if (!isAlphanumeric(s[r])) {
r--;
} else if (s[l].toLowerCase() !== s[r].toLowerCase()) {
return false;
} else {
l++;
r--;
}
}
return true;
};
// función auxiliar: ¿es letra o número?
function isAlphanumeric(char) {
const code = char.charCodeAt(0);
return (
(code >= 48 && code <= 57) || // 0-9
(code >= 65 && code <= 90) || // A-Z
(code >= 97 && code <= 122) // a-z
);
}
Vamos línea por línea:
-
larranca en0(inicio del string) yrens.length - 1(final). Son nuestros dos punteros que se van a acercar al centro. - El
while (l < r)se ejecuta mientras los punteros no se hayan cruzado. - Dentro del loop hay cuatro casos:
- Si
s[l]no es alfanumérico (un espacio, coma, dos puntos...), lo saltamos moviendola la derecha. - Si
s[r]no es alfanumérico, lo saltamos moviendora la izquierda. - Si ambos son alfanuméricos pero no coinciden (comparando en minúsculas), no es palíndromo. Retornamos
false. - Si coinciden, avanzamos ambos punteros hacia el centro (
l++,r--).
- Si
- Si el loop termina sin retornar
false, los punteros se cruzaron sin encontrar diferencias. Es palíndromo, retornamostrue.
La función auxiliar isAlphanumeric revisa el código ASCII del carácter: 48-57 son dígitos, 65-90 letras mayúsculas, 97-122 letras minúsculas. Todo lo demás (espacios, puntuación, etc.) retorna false.
💡 Complejidad: O(n) en tiempo, O(1) en espacio. Un solo recorrido del string, sin crear copias ni estructuras extra.
Veamos cómo se ve con s = "A man, a plan, a canal: Panama":
Fast and slow: Remove Duplicates
Ahora veamos fast and slow con el clásico "Remove Duplicates from Sorted Array" de LeetCode:
Problema:
Te dan una lista de números nums ya ordenada de menor a mayor, pero con algunos repetidos. Tienes que dejar cada número una sola vez, moviendo los únicos al inicio de la misma lista (sin crear otra lista nueva).
Por ejemplo, si te dan [1, 1, 2, 3, 3], la lista debe quedar así: [1, 2, 3, ?, ?]. Los primeros tres espacios tienen los números únicos en orden, y los últimos dos espacios no importan (pueden quedar con cualquier valor, por eso los marcamos con ?).
Además, tu función debe devolver cuántos números únicos quedaron al inicio. En el ejemplo anterior, devolverías 3 porque hay tres números únicos (1, 2, 3). A ese número lo llaman k en el enunciado.
Ejemplos
Ejemplo 1
Entrada: nums = [1,1,2]
Salida: 2, nums = [1,2,_]
Explicación: Tu función debe devolver k = 2, y los primeros dos elementos de nums deben ser 1 y 2 respectivamente. No importa qué quede después de los primeros k espacios (por eso aparecen como guiones bajos).
Ejemplo 2
Entrada: nums = [0,0,1,1,1,2,2,3,3,4]
Salida: 5, nums = [0,1,2,3,4,_,_,_,_,_]
Explicación: Tu función debe devolver k = 5, y los primeros cinco elementos de nums deben ser 0, 1, 2, 3 y 4 respectivamente. No importa qué quede después de los primeros k espacios (por eso aparecen como guiones bajos).
¿Cómo lo pensamos?
La fuerza bruta sería crear un nuevo array y copiar solo los valores que no se repiten. Pero el problema dice explícitamente que lo hagas "in-place", o sea, modificando el mismo array sin crear otro. Ahí es donde entra fast and slow.
La idea es esta: usamos dos punteros que arrancan casi juntos. slow marca la posición donde va el siguiente número único. fast se adelanta recorriendo todo el array buscando valores nuevos.
Como el array ya está ordenado, los duplicados siempre están juntos. Entonces fast solo necesita comparar su valor con el de slow. Si son diferentes, encontramos un número nuevo: avanzamos slow una posición y copiamos ahí el valor de fast. Si son iguales, fast simplemente sigue avanzando.
Piénsalo así: slow es el que construye la respuesta, y fast es el explorador que le dice "oye, encontré uno nuevo".
Solución paso a paso
/**
* @param {number[]} nums
* @return {number}
*/
var removeDuplicates = function (nums) {
let slow = 0;
for (let fast = 1; fast < nums.length; fast++) {
if (nums[fast] !== nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
};
Vamos línea por línea:
-
slowarranca en0, apuntando al primer elemento. Ese siempre es único porque no hay nada antes con qué comparar. -
fastarranca en1y recorre el array completo con elfor. - En cada paso, comparamos
nums[fast]connums[slow]:- Si son iguales, es un duplicado.
fastavanza yslowse queda donde está. No hacemos nada. - Si son diferentes, encontramos un valor nuevo. Avanzamos
slowuna posición (slow++) y copiamos el valor defastahí (nums[slow] = nums[fast]).
- Si son iguales, es un duplicado.
- Al final,
slowqueda apuntando al último elemento único. Como los índices empiezan en0, el total de únicos esslow + 1.
Veamos cómo se ve con nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]:
Los primeros 5 elementos son [0, 1, 2, 3, 4], exactamente los valores únicos en orden. Lo que queda después del índice 4 no importa.
¿Ves el patrón? fast recorre todo el array una sola vez, y slow solo avanza cuando encuentra algo nuevo. Eso nos da O(n) en tiempo y O(1) en espacio, porque no creamos ninguna estructura extra.
💡 Complejidad: O(n) en tiempo, O(1) en espacio. Un solo recorrido, sin estructuras extra.
Parallel pointers: Intersección de dos arrays
Por último, parallel pointers con "Intersection of Two Arrays" de LeetCode:
Problema:
Dados dos arreglos de enteros, devuelve un arreglo con su intersección. Cada elemento del resultado debe ser único y puede estar en cualquier orden.
Ejemplos
Input: nums1 = [1, 2, 2, 1], nums2 = [2, 2]
Output: [2]
Input: nums1 = [4, 9, 5], nums2 = [9, 4, 9, 8, 4]
Output: [9, 4] // el orden no importa
¿Cómo lo pensamos?
La fuerza bruta sería usar dos loops anidados: por cada elemento de nums1, recorrer todo nums2 buscando coincidencias.
Con parallel pointers hacemos algo más inteligente: primero ordenamos ambos arrays y después los recorremos una sola vez con dos punteros. Como están ordenados, podemos comparar los valores y decidir cuál puntero mover:
- Si los valores son iguales, encontramos una intersección. Avanzamos ambos.
- Si el valor de
p1es menor, lo avanzamos porque no puede haber coincidencia más atrás ennums2. - Si el valor de
p2es menor, lo avanzamos por la misma razón.
Es como hacer merge de dos listas ordenadas, pero solo nos quedamos con los valores que aparecen en ambas.
Solución paso a paso
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function (nums1, nums2) {
let p1 = 0,
p2 = 0;
// ordenar arreglos
nums1.sort((a, b) => a - b);
nums2.sort((a, b) => a - b);
const intersection = [];
while (p1 < nums1.length && p2 < nums2.length) {
if (nums1[p1] === nums2[p2]) {
// Evita duplicados en el resultado
if (
intersection.length === 0 ||
intersection[intersection.length - 1] !== nums1[p1]
) {
intersection.push(nums1[p1]);
}
p1++;
p2++;
} else if (nums1[p1] < nums2[p2]) {
p1++;
} else {
p2++;
}
}
return intersection;
};
Vamos línea por línea:
- Ordenamos ambos arrays con
.sort(). Esto es clave: sin el orden, no podríamos saber hacia dónde mover los punteros. -
p1yp2arrancan en0, cada uno apuntando al inicio de su array. - El
whilese ejecuta mientras ambos punteros estén dentro de sus arrays. - Dentro del loop hay tres casos:
- Si
nums1[p1] === nums2[p2], encontramos un valor en común. Antes de agregarlo, verificamos que no sea un duplicado (comparando con el último elemento del resultado). Avanzamos ambos punteros. - Si
nums1[p1] < nums2[p2], el valor dep1es más chico. Como ambos arrays están ordenados, ese valor no puede existir más adelante ennums2. Avanzamosp1. - Si
nums1[p1] > nums2[p2], misma lógica pero al revés. Avanzamosp2.
- Si
- Cuando alguno de los dos punteros llega al final de su array, el loop termina. Ya no puede haber más intersecciones.
Veamos con nums1 = [4, 9, 5], nums2 = [9, 4, 9, 8, 4]:
¿Ves cómo cada puntero solo avanza hacia adelante? Nunca retroceden. El recorrido en sí es O(n + m), pero el ordenamiento inicial domina: O(n log n + m log m) en total.
💡 Complejidad: O(n log n + m log m) en tiempo por el ordenamiento. O(n) en espacio para el resultado.
Resumen
Estas tres técnicas son variantes de un mismo concepto: usar dos punteros para recorrer arrays o strings de forma eficiente.
| Técnica | Movimiento de punteros | Problema resuelto |
|---|---|---|
| Opposite direction | De los extremos al centro | Valid Palindrome |
| Fast and slow | Uno explora, otro referencia | Remove Duplicates |
| Parallel pointers | Cada uno en su array | Intersection of Two Arrays |
La clave no es memorizar las soluciones. Es reconocer el patrón en el enunciado y saber qué técnica aplicar. Con práctica, vas a leer un problema y saber qué variante de two pointers usar antes de escribir una línea de código.
Si quieres seguir practicando, aquí te dejo más problemas de LeetCode:
¿Cuál técnica te costó más entender? ¿Hay algún problema que quieras que resolvamos juntos? Déjamelo en los comentarios. 🙌🏻




Top comments (0)