Dos punteros
Dos índices que recorren el array a la vez (desde los extremos hacia el centro, o uno rápido y otro lento) resuelven en una pasada problemas que con dos bucles anidados serían 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.
Dos punteros
Escribe un array ordenado y una suma: dos índices, uno en cada extremo, encuentran la pareja en una sola pasada.
- los dos punteros
- en la zona de trabajo
Paso 1
Un puntero en cada extremo: izq en el menor (1) y der en el mayor (15). Como el array está ordenado, mover izq a la derecha sube la suma y mover der a la izquierda la baja.
1static int[] parQueSuma(int[] a, int objetivo) { // a está ordenado
2 int izq = 0, der = a.length - 1; // objetivo = 17, izq = 0, der = 7
3 while (izq < der) {
4 int suma = a[izq] + a[der];
5 if (suma == objetivo) return new int[] {izq, der};
6 if (suma < objetivo) izq++;
7 else der--;
8 }
9 return null;
10}Variables
- objetivo
- 17
- izq
- 0
- der
- 7
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Muchos problemas sobre arrays parecen necesitar comparar cada elemento con todos los demás: buscar dos números que sumen algo, comprobar si una palabra es palíndroma, quitar repetidos. Con dos bucles anidados son O(n²).
La técnica de los dos punteros usa dos índices que se mueven a la vez en un solo recorrido. La versión más común pone uno en cada extremo y los va acercando: en cada paso se decide cuál mover según lo que se ve, y cada decisión descarta un elemento para siempre. Como cada índice solo avanza en un sentido, como mucho hay n pasos: O(n).
El ejemplo clásico es buscar dos números que sumen un objetivo en un array ORDENADO. Si a[izq] + a[der] se queda corto, ni siquiera con el mayor que queda (a[der]) llega a[izq]: se puede descartar y avanzar izq. Si se pasa, a[der] sobra incluso con el menor: se retrocede der. El orden es lo que hace segura cada decisión.
La otra variante usa un puntero lento y uno rápido que van en el mismo sentido: el rápido explora y el lento marca dónde escribir (quitar repetidos en el sitio) o va a la mitad de velocidad (el medio de una lista enlazada).
Cuándo usarlo
- Parejas o tríos que cumplen una condición de suma en un array ordenado.
- Comprobar palíndromos, invertir o comparar secuencias desde los dos extremos.
- Quitar repetidos, compactar o particionar un array en el sitio, sin array auxiliar.
- Mezclar dos secuencias ordenadas o calcular su intersección.
Cuándo no
- Si los datos no están ordenados y el problema lo necesita (la suma de parejas): o se ordena antes (O(n log n)) o se usa un
HashSet(O(n)). - Cuando no hay una regla segura para decidir qué puntero mover: entonces no se puede descartar nada.
Paso a paso
- Colocar los punteros. En los extremos (
izq = 0,der = n − 1) o los dos al principio (lento = 0,rapido = 1). - Mirar. Se calcula lo que importa con los dos elementos señalados: su suma, si son iguales…
- Decidir y mover. Según lo visto, se mueve uno de los dos (o ambos). La regla tiene que garantizar que lo que se deja atrás ya no sirve.
- Parar. Cuando se cruzan (
izq >= der) o cuando el rápido llega al final.
El código
Invertir, palíndromo y pareja que suma
Tres usos de los dos punteros desde los extremos.
1import java.util.Arrays;
2
3public class Main {
4 /** Da la vuelta al array en el sitio: un puntero en cada extremo que se acercan. */
5 static void invertir(int[] a) {
6 for (int i = 0, j = a.length - 1; i < j; i++, j--) {
7 int t = a[i]; a[i] = a[j]; a[j] = t;
8 }
9 }
10
11 /** ¿Se lee igual al derecho que al revés? (Sin contar espacios ni mayúsculas.) */
12 static boolean palindromo(String s) {
13 String t = s.replace(" ", "").toLowerCase();
14 for (int i = 0, j = t.length() - 1; i < j; i++, j--)
15 if (t.charAt(i) != t.charAt(j)) return false;
16 return true;
17 }
18
19 /** Posiciones de dos números del array ORDENADO que suman objetivo, o null. */
20 static int[] parQueSuma(int[] a, int objetivo) {
21 int izq = 0, der = a.length - 1;
22 while (izq < der) {
23 int suma = a[izq] + a[der];
24 if (suma == objetivo) return new int[] {izq, der};
25 if (suma < objetivo) izq++; // hace falta más: el de la izquierda no sirve con nadie
26 else der--; // sobra: el de la derecha no sirve con nadie
27 }
28 return null;
29 }
30
31 public static void main(String[] args) {
32 int[] a = {1, 2, 3, 4, 5};
33 invertir(a);
34 System.out.println("Invertido: " + Arrays.toString(a));
35 System.out.println("«anita lava la tina» es palíndromo: " + (palindromo("anita lava la tina") ? "sí" : "no"));
36 int[] p = parQueSuma(new int[] {1, 3, 4, 6, 8, 11, 13, 15}, 17);
37 System.out.println("Suman 17 las posiciones " + p[0] + " y " + p[1]);
38 }
39}def invertir(a):
"""Da la vuelta a la lista en el sitio: un puntero en cada extremo que se acercan."""
i, j = 0, len(a) - 1
while i < j:
a[i], a[j] = a[j], a[i]
i, j = i + 1, j - 1
def palindromo(s):
"""¿Se lee igual al derecho que al revés? (Sin contar espacios ni mayúsculas.)"""
t = s.replace(" ", "").lower()
i, j = 0, len(t) - 1
while i < j:
if t[i] != t[j]:
return False
i, j = i + 1, j - 1
return True
def par_que_suma(a, objetivo):
"""Posiciones de dos números de la lista ORDENADA que suman objetivo, o None."""
izq, der = 0, len(a) - 1
while izq < der:
suma = a[izq] + a[der]
if suma == objetivo:
return izq, der
if suma < objetivo:
izq += 1 # hace falta más: el de la izquierda no sirve con nadie
else:
der -= 1 # sobra: el de la derecha no sirve con nadie
return None
a = [1, 2, 3, 4, 5]
invertir(a)
print("Invertido:", a)
print("«anita lava la tina» es palíndromo:", "sí" if palindromo("anita lava la tina") else "no")
p = par_que_suma([1, 3, 4, 6, 8, 11, 13, 15], 17)
print(f"Suman 17 las posiciones {p[0]} y {p[1]}")/** Da la vuelta al array en el sitio: un puntero en cada extremo que se acercan. */
function invertir(a) {
for (let i = 0, j = a.length - 1; i < j; i++, j--) [a[i], a[j]] = [a[j], a[i]];
}
/** ¿Se lee igual al derecho que al revés? (Sin contar espacios ni mayúsculas.) */
function palindromo(s) {
const t = s.replaceAll(" ", "").toLowerCase();
for (let i = 0, j = t.length - 1; i < j; i++, j--)
if (t[i] !== t[j]) return false;
return true;
}
/** Posiciones de dos números del array ORDENADO que suman objetivo, o null. */
function parQueSuma(a, objetivo) {
let izq = 0, der = a.length - 1;
while (izq < der) {
const suma = a[izq] + a[der];
if (suma === objetivo) return [izq, der];
if (suma < objetivo) izq++; // hace falta más: el de la izquierda no sirve con nadie
else der--; // sobra: el de la derecha no sirve con nadie
}
return null;
}
const a = [1, 2, 3, 4, 5];
invertir(a);
console.log("Invertido: [" + a.join(", ") + "]");
console.log("«anita lava la tina» es palíndromo: " + (palindromo("anita lava la tina") ? "sí" : "no"));
const p = parQueSuma([1, 3, 4, 6, 8, 11, 13, 15], 17);
console.log(`Suman 17 las posiciones ${p[0]} y ${p[1]}`);using System;
class Program {
// Da la vuelta al array en el sitio: un puntero en cada extremo que se acercan.
static void Invertir(int[] a) {
for (int i = 0, j = a.Length - 1; i < j; i++, j--) (a[i], a[j]) = (a[j], a[i]);
}
// ¿Se lee igual al derecho que al revés? (Sin contar espacios ni mayúsculas.)
static bool Palindromo(string s) {
string t = s.Replace(" ", "").ToLower();
for (int i = 0, j = t.Length - 1; i < j; i++, j--)
if (t[i] != t[j]) return false;
return true;
}
// Posiciones de dos números del array ORDENADO que suman objetivo, o null.
static (int, int)? ParQueSuma(int[] a, int objetivo) {
int izq = 0, der = a.Length - 1;
while (izq < der) {
int suma = a[izq] + a[der];
if (suma == objetivo) return (izq, der);
if (suma < objetivo) izq++; // hace falta más: el de la izquierda no sirve con nadie
else der--; // sobra: el de la derecha no sirve con nadie
}
return null;
}
static void Main() {
int[] a = { 1, 2, 3, 4, 5 };
Invertir(a);
Console.WriteLine("Invertido: [" + string.Join(", ", a) + "]");
Console.WriteLine("«anita lava la tina» es palíndromo: " + (Palindromo("anita lava la tina") ? "sí" : "no"));
var (i, j) = ParQueSuma(new[] { 1, 3, 4, 6, 8, 11, 13, 15 }, 17)!.Value;
Console.WriteLine(quot;Suman 17 las posiciones {i} y {j}");
}
}<?php
/** Da la vuelta al array en el sitio: un puntero en cada extremo que se acercan. */
function invertir(array &$a): void {
for ($i = 0, $j = count($a) - 1; $i < $j; $i++, $j--) [$a[$i], $a[$j]] = [$a[$j], $a[$i]];
}
/** ¿Se lee igual al derecho que al revés? (Sin contar espacios ni mayúsculas.) */
function palindromo(string $s): bool {
$t = strtolower(str_replace(" ", "", $s));
for ($i = 0, $j = strlen($t) - 1; $i < $j; $i++, $j--)
if ($t[$i] !== $t[$j]) return false;
return true;
}
/** Posiciones de dos números del array ORDENADO que suman $objetivo, o null. */
function parQueSuma(array $a, int $objetivo): ?array {
$izq = 0;
$der = count($a) - 1;
while ($izq < $der) {
$suma = $a[$izq] + $a[$der];
if ($suma === $objetivo) return [$izq, $der];
if ($suma < $objetivo) $izq++; // hace falta más: el de la izquierda no sirve con nadie
else $der--; // sobra: el de la derecha no sirve con nadie
}
return null;
}
$a = [1, 2, 3, 4, 5];
invertir($a);
echo "Invertido: [" . implode(", ", $a) . "]\n";
echo "«anita lava la tina» es palíndromo: " . (palindromo("anita lava la tina") ? "sí" : "no") . "\n";
[$i, $j] = parQueSuma([1, 3, 4, 6, 8, 11, 13, 15], 17);
echo "Suman 17 las posiciones $i y $j\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Invertido: [5, 4, 3, 2, 1] «anita lava la tina» es palíndromo: sí Suman 17 las posiciones 2 y 6
Quitar repetidos con un puntero lento y uno rápido
La otra variante: los dos avanzan en el mismo sentido y el lento señala dónde escribir el siguiente distinto.
1/** Quita los repetidos de un array ORDENADO sin otro array: un puntero lento marca dónde escribir
2 y uno rápido recorre. Devuelve cuántos distintos hay (quedan en a[0..k-1]). */
3static int sinRepetidos(int[] a) {
4 if (a.length == 0) return 0;
5 int lento = 0;
6 for (int rapido = 1; rapido < a.length; rapido++) {
7 if (a[rapido] != a[lento]) a[++lento] = a[rapido]; // uno nuevo: se copia detrás del último distinto
8 }
9 return lento + 1;
10}def sin_repetidos(a):
"""Quita los repetidos de una lista ORDENADA sin otra lista: un puntero lento marca dónde escribir
y uno rápido recorre. Devuelve cuántos distintos hay (quedan en a[0..k-1])."""
if not a:
return 0
lento = 0
for rapido in range(1, len(a)):
if a[rapido] != a[lento]: # uno nuevo: se copia detrás del último distinto
lento += 1
a[lento] = a[rapido]
return lento + 1/** Quita los repetidos de un array ORDENADO sin otro array: un puntero lento marca dónde escribir
y uno rápido recorre. Devuelve cuántos distintos hay (quedan en a[0..k-1]). */
function sinRepetidos(a) {
if (a.length === 0) return 0;
let lento = 0;
for (let rapido = 1; rapido < a.length; rapido++) {
if (a[rapido] !== a[lento]) a[++lento] = a[rapido]; // uno nuevo: se copia detrás del último distinto
}
return lento + 1;
}// Quita los repetidos de un array ORDENADO sin otro array: un puntero lento marca dónde escribir
// y uno rápido recorre. Devuelve cuántos distintos hay (quedan en a[0..k-1]).
static int SinRepetidos(int[] a) {
if (a.Length == 0) return 0;
int lento = 0;
for (int rapido = 1; rapido < a.Length; rapido++) {
if (a[rapido] != a[lento]) a[++lento] = a[rapido]; // uno nuevo: se copia detrás del último distinto
}
return lento + 1;
}/** Quita los repetidos de un array ORDENADO sin otro array: un puntero lento marca dónde escribir
y uno rápido recorre. Devuelve cuántos distintos hay (quedan en $a[0..k-1]). */
function sinRepetidos(array &$a): int {
if (!$a) return 0;
$lento = 0;
for ($rapido = 1; $rapido < count($a); $rapido++) {
if ($a[$rapido] !== $a[$lento]) $a[++$lento] = $a[$rapido]; // uno nuevo: se copia detrás del último distinto
}
return $lento + 1;
}Traza: pareja que suma 17 en {1, 3, 4, 6, 8, 11, 13, 15}
| izq | der | Suma | Decisión |
|---|---|---|---|
| 0 | 7 | 1 + 15 = 16 | suma pequeña → izq++ |
| 1 | 7 | 3 + 15 = 18 | suma grande → der−− |
| 1 | 6 | 3 + 13 = 16 | suma pequeña → izq++ |
| 2 | 6 | 4 + 13 = 17 | encontrada |
Cada fila descarta un elemento. Con dos bucles anidados habría que probar hasta 28 parejas.
Complejidad
| Problema | Fuerza bruta | Dos punteros |
|---|---|---|
| Pareja que suma X (ordenado) | O(n²) | O(n) |
| ¿Es palíndromo? | O(n) (copiando invertido) | O(n) sin copiar |
| Quitar repetidos (ordenado) | O(n²) o un array extra | O(n) en el sitio |
| Trío que suma X | O(n³) | O(n²): un bucle + dos punteros |
Memoria extra: O(1). La técnica no cambia el orden de magnitud por arte de magia: lo consigue porque cada paso descarta algo para siempre.
- Mejor caso: O(1)
- Caso medio: O(n)
- Peor caso: O(n)
Cada paso descarta un elemento: como mucho n pasos, frente a las n²/2 parejas de la fuerza bruta. Las curvas grises son las demás clases, para comparar.
En la práctica
- La mezcla de mergesort y el JOIN por mezcla de las bases de datos avanzan dos índices sobre dos listas ordenadas.
- La partición de quicksort es una variante de dos punteros.
- Comprobar palíndromos, comparar versiones de un texto o validar que una cadena es simétrica.
- Es una de las técnicas más preguntadas en entrevistas técnicas: «two sum», «container with most water», «3sum».
Errores típicos
- Usarla con datos sin ordenar en problemas que lo necesitan: la regla de qué puntero mover deja de ser cierta y se pierden soluciones.
- Condición de parada
izq <= dercuando la pareja tiene que ser de dos elementos distintos: un elemento se empareja consigo mismo. - Olvidar mover algún puntero en algún caso: bucle infinito.
- Al buscar todas las parejas, no saltar los repetidos y escribir la misma pareja varias veces.
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. Todas las parejas que suman X
La primera línea es la suma buscada y la segunda, un array de enteros ordenado de menor a mayor (puede tener repetidos). Escribe todas las parejas de valores distintas que suman lo pedido, de la que tiene el menor más pequeño a la que lo tiene mayor, sin repetir ninguna. Usa dos punteros: tras encontrar una pareja, salta todos los valores iguales a los dos. Completa parejas.
- Entrada:
10y luego1 2 3 3 5 7 7 8 9. - Salida:
1 + 9 = 10,2 + 8 = 10,3 + 7 = 10… y al final3 parejas(1 pareja) oNinguna pareja suma 10. - Errores:
Suma no válida: «…»,No hay números,Número no válido: «x»yLos números tienen que estar ordenados.
Ejemplo
10 1 2 3 3 5 7 7 8 9
1 + 9 = 10 2 + 8 = 10 3 + 7 = 10 3 parejas
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** Todas las parejas de VALORES distintas (a ≤ b) del array ordenado que suman objetivo,
5 con dos punteros y saltando los repetidos. Cada pareja como {a, b}. */
6 static List<int[]> parejas(int[] a, int objetivo) {
7 List<int[]> r = new ArrayList<>();
8 int izq = 0, der = a.length - 1;
9 while (izq < der) {
10 int suma = a[izq] + a[der];
11 if (suma < objetivo) izq++;
12 else if (suma > objetivo) der--;
13 else {
14 r.add(new int[] {a[izq], a[der]});
15 int x = a[izq], y = a[der];
16 while (izq < der && a[izq] == x) izq++; // salta los iguales para no repetir la pareja
17 while (izq < der && a[der] == y) der--;
18 }
19 }
20 return r;
21 }
22
23 public static void main(String[] args) {
24 Scanner sc = new Scanner(System.in);
25 String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
26 String segunda = sc.hasNextLine() ? sc.nextLine().trim() : "";
27 if (!primera.matches("-?\\d{1,6}")) {
28 System.out.println("Suma no válida: «" + primera + "»");
29 return;
30 }
31 int objetivo = Integer.parseInt(primera);
32 if (segunda.isEmpty()) {
33 System.out.println("No hay números");
34 return;
35 }
36 String[] t = segunda.split("\\s+");
37 int[] a = new int[t.length];
38 for (int i = 0; i < t.length; i++) {
39 if (!t[i].matches("-?\\d{1,6}")) {
40 System.out.println("Número no válido: «" + t[i] + "»");
41 return;
42 }
43 a[i] = Integer.parseInt(t[i]);
44 if (i > 0 && a[i] < a[i - 1]) {
45 System.out.println("Los números tienen que estar ordenados");
46 return;
47 }
48 }
49 List<int[]> r = parejas(a, objetivo);
50 for (int[] p : r) System.out.println(p[0] + " + " + p[1] + " = " + objetivo);
51 System.out.println(r.isEmpty() ? "Ninguna pareja suma " + objetivo : r.size() + (r.size() == 1 ? " pareja" : " parejas"));
52 }
53}Saltar los iguales es lo que evita repetir parejas sin tener que guardar las ya escritas en un conjunto.
Sigue siendo O(n): los saltos solo hacen avanzar los punteros, que nunca retroceden.
2. El contenedor con más agua
Cada número es la altura de un poste vertical, a un metro del anterior. Dos postes y el suelo forman un contenedor: el agua llega hasta el más bajo de los dos, así que el área es min(h[i], h[j]) · (j − i). Encuentra los dos postes que encierran más agua sin probar todas las parejas: dos punteros en los extremos y, en cada paso, se mueve el más bajo. Completa mejor.
- Entrada: una línea con las alturas (enteros de 0 a 9999), por ejemplo
1 8 6 2 5 4 8 3 7. - Salida:
Más agua: 49 (postes 2 y 9, alturas 8 y 7)(los postes se numeran desde 1; con empate, la primera pareja encontrada). - Errores:
Hacen falta al menos dos postesyAltura no válida: «x».
Ejemplo
1 8 6 2 5 4 8 3 7
Más agua: 49 (postes 2 y 9, alturas 8 y 7)
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** El contenedor con más agua: dos postes i < j encierran min(h[i], h[j]) · (j − i).
5 Devuelve {área, i, j}; con empate, la primera pareja que se encuentra. */
6 static int[] mejor(int[] h) {
7 int izq = 0, der = h.length - 1;
8 int[] r = {0, 0, 0};
9 while (izq < der) {
10 int area = Math.min(h[izq], h[der]) * (der - izq);
11 if (area > r[0]) r = new int[] {area, izq, der};
12 if (h[izq] < h[der]) izq++; // el más bajo limita: moverlo es la única forma de mejorar
13 else der--;
14 }
15 return r;
16 }
17
18 public static void main(String[] args) {
19 Scanner sc = new Scanner(System.in);
20 String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
21 String[] t = linea.isEmpty() ? new String[0] : linea.split("\\s+");
22 if (t.length < 2) {
23 System.out.println("Hacen falta al menos dos postes");
24 return;
25 }
26 int[] h = new int[t.length];
27 for (int i = 0; i < t.length; i++) {
28 if (!t[i].matches("\\d{1,4}")) {
29 System.out.println("Altura no válida: «" + t[i] + "»");
30 return;
31 }
32 h[i] = Integer.parseInt(t[i]);
33 }
34 int[] r = mejor(h);
35 System.out.println("Más agua: " + r[0] + " (postes " + (r[1] + 1) + " y " + (r[2] + 1) + ", alturas " + h[r[1]] + " y " + h[r[2]] + ")");
36 }
37}Cada paso descarta el poste más bajo: con él, cualquier contenedor más estrecho tendría como mucho su altura y menos anchura, así que ya no puede mejorar el que acabamos de medir.
n − 1 pasos en vez de las n(n − 1)/2 parejas: con 100.000 postes, 100.000 pasos frente a 5.000 millones.
Test
Test: Dos punteros
0/5 respondidas · 0 aciertosElige 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.¿Qué condición necesitan los datos para buscar con dos punteros una pareja que sume X?
2.Si
a[izq] + a[der]es menor que el objetivo, ¿qué se hace?3.¿Cuál es el coste de buscar una pareja con dos punteros en un array ordenado?
4.¿Qué variante usa un puntero «lento» y otro «rápido»?
5.Los datos no están ordenados y hay que encontrar una pareja que sume X en O(n). ¿Qué usas?