Apuntes DAM
Volver al inicio

Dos punteros

AlgoritmosBúsquedaNivel intermedioTambién: two pointers, técnica de los dos índices

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.

De 2 a 16 enteros, de menor a mayor
  • 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

  1. Colocar los punteros. En los extremos (izq = 0, der = n − 1) o los dos al principio (lento = 0, rapido = 1).
  2. Mirar. Se calcula lo que importa con los dos elementos señalados: su suma, si son iguales…
  3. 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.
  4. 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.

Java
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}

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.

Java
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}

Traza: pareja que suma 17 en {1, 3, 4, 6, 8, 11, 13, 15}

izqderSumaDecisión
071 + 15 = 16suma pequeña → izq++
173 + 15 = 18suma grande → der−−
163 + 13 = 16suma pequeña → izq++
264 + 13 = 17encontrada

Cada fila descarta un elemento. Con dos bucles anidados habría que probar hasta 28 parejas.

Complejidad

ProblemaFuerza brutaDos 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 extraO(n) en el sitio
Trío que suma XO(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.

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)

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 <= der cuando 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: 10 y luego 1 2 3 3 5 7 7 8 9.
  • Salida: 1 + 9 = 10, 2 + 8 = 10, 3 + 7 = 10… y al final 3 parejas (1 pareja) o Ninguna pareja suma 10.
  • Errores: Suma no válida: «…», No hay números, Número no válido: «x» y Los números tienen que estar ordenados.
☕JavaTodas las parejas que suman XMedio

Ejemplo

Entrada (lo que se escribe por teclado)
10
1 2 3 3 5 7 7 8 9
Salida esperada
1 + 9 = 10
2 + 8 = 10
3 + 7 = 10
3 parejas
⏳
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    /** 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 postes y Altura no válida: «x».
☕JavaEl contenedor con más aguaDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
1 8 6 2 5 4 8 3 7
Salida esperada
Más agua: 49 (postes 2 y 9, alturas 8 y 7)
⏳
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    /** 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 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.¿Qué condición necesitan los datos para buscar con dos punteros una pareja que sume X?

  2. 2.Si a[izq] + a[der] es menor que el objetivo, ¿qué se hace?

  3. 3.¿Cuál es el coste de buscar una pareja con dos punteros en un array ordenado?

  4. 4.¿Qué variante usa un puntero «lento» y otro «rápido»?

  5. 5.Los datos no están ordenados y hay que encontrar una pareja que sume X en O(n). ¿Qué usas?

Relacionado