Apuntes DAM
Volver al inicio

Búsqueda binaria

AlgoritmosBúsquedaNivel básicoTambién: búsqueda dicotómica, binary search

Encuentra un valor en un array ordenado mirando el elemento del centro y descartando la mitad que no puede contenerlo: un millón de datos en solo 20 comparaciones.

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.

Búsqueda binaria

Escribe un array ordenado (hasta 16 números) y el valor que buscas.

  • en la zona de trabajo

Paso 1

Buscamos el 23 entre 10 números ordenados. Al principio puede estar en cualquier sitio: la zona va de izq = 0 a der = 9.

1static int buscar(int[] a, int x) {
2    int izq = 0, der = a.length - 1;  // x = 23, izq = 0, der = 9
3    while (izq <= der) {
4        int medio = izq + (der - izq) / 2;
5        if (a[medio] == x) return medio;
6        if (a[medio] < x) izq = medio + 1;
7        else der = medio - 1;
8    }
9    return -1;
10}

Variables

x
23
izq
0
der
9
comparaciones
0

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Para buscar una palabra en un diccionario nadie empieza por la primera página: se abre por la mitad, se mira si la palabra va antes o después y se descarta la otra mitad. Se repite con la mitad que queda hasta encontrarla o quedarse sin páginas.

La búsqueda binaria hace exactamente eso con un array ordenado: compara el valor buscado con el elemento central; si es igual, lo ha encontrado; si es menor, sigue por la mitad izquierda; si es mayor, por la derecha. Cada comparación descarta la mitad de lo que queda, así que con n elementos hacen falta como mucho ⌊log₂ n⌋ + 1 comparaciones.

La condición imprescindible es que los datos estén ordenados, y por el mismo criterio con el que se compara. Sobre un array desordenado no da ningún error: da respuestas equivocadas.

La misma idea sirve para mucho más que buscar un valor: para encontrar la primera posición que cumple una condición (la primera nota aprobada en una lista ordenada) o para encontrar el valor mínimo que hace posible algo, probando por la mitad del rango de respuestas posibles (la búsqueda binaria sobre la respuesta).

Cuándo usarlo

  • Los datos están ordenados y se puede ir directo a cualquier posición (arrays, ArrayList).
  • Hay que buscar muchas veces en los mismos datos: ordenarlos una vez (O(n log n)) y buscar después en O(log n) compensa enseguida.
  • Hay que encontrar una frontera: la primera posición con un valor mayor o igual que x, el primer día en que se supera un umbral, la primera versión que falla.
  • Hay que encontrar el mínimo (o máximo) valor que cumple una condición que, una vez se cumple, se cumple para todos los valores mayores.

Cuándo no

  • Los datos están desordenados y solo se va a buscar una vez: ordenar cuesta más que mirar todos (búsqueda lineal, O(n)).
  • En listas enlazadas (LinkedList): llegar al centro cuesta recorrer media lista.
  • Si los datos cambian todo el rato: mantener el array ordenado al insertar cuesta O(n); es mejor un TreeSet o, si no hace falta el orden, un HashSet.

Paso a paso

  1. Delimitar la zona. Dos índices, izq = 0 y der = n - 1, marcan la zona del array donde todavía puede estar el valor.
  2. Mirar el centro. Mientras la zona no esté vacía (izq <= der), se calcula medio = izq + (der - izq) / 2 y se compara a[medio] con el valor buscado.
  3. Descartar la mitad. Si a[medio] es el valor, se ha encontrado. Si es menor, el valor solo puede estar a la derecha: izq = medio + 1. Si es mayor, a la izquierda: der = medio - 1.
  4. Zona vacía. Si izq supera a der, no está. En ese momento izq es justo la posición donde habría que insertarlo para mantener el orden.

El código

Búsqueda binaria iterativa

La versión que se usa en la práctica: un bucle, dos índices y memoria constante.

Java
1public class Main {
2    /** Posición de x en el array ordenado a, o -1 si no está. */
3    static int buscar(int[] a, int x) {
4        int izq = 0, der = a.length - 1;          // x solo puede estar en a[izq..der]
5        while (izq <= der) {
6            int medio = izq + (der - izq) / 2;    // así la suma no se desborda
7            if (a[medio] == x) return medio;
8            if (a[medio] < x) izq = medio + 1;    // x está a la derecha del medio
9            else der = medio - 1;                 // x está a la izquierda
10        }
11        return -1;                                // la zona se ha quedado vacía
12    }
13
14    public static void main(String[] args) {
15        int[] a = {3, 8, 15, 23, 42, 57, 61, 78, 90};
16        System.out.println("buscar(a, 57) = " + buscar(a, 57));
17        System.out.println("buscar(a, 4) = " + buscar(a, 4) + " (no está)");
18    }
19}

Salida al ejecutarlo (la misma en los 5 lenguajes)

buscar(a, 57) = 5
buscar(a, 4) = -1 (no está)

Versión recursiva

La misma idea escrita con recursividad: más cercana a la definición, pero gasta un marco de la pila por cada llamada.

Java
1/** La misma búsqueda, recursiva: cada llamada mira una zona la mitad de grande. */
2static int buscar(int[] a, int x, int izq, int der) {
3    if (izq > der) return -1;                     // caso base: zona vacía
4    int medio = izq + (der - izq) / 2;
5    if (a[medio] == x) return medio;
6    return a[medio] < x ? buscar(a, x, medio + 1, der)
7                        : buscar(a, x, izq, medio - 1);
8}

Con valores repetidos: la primera posición mayor o igual

La búsqueda clásica devuelve cualquiera de las apariciones de un valor repetido. Para encontrar la primera se busca la frontera: la primera posición cuyo valor es mayor o igual que x. Es lo que en C++ se llama lower_bound.

Java
1/** Primera posición con a[i] >= x (a.length si no hay ninguna): sirve con valores repetidos. */
2static int primeraMayorOIgual(int[] a, int x) {
3    int izq = 0, der = a.length;          // ojo: aquí der es exclusivo, la zona es a[izq..der)
4    while (izq < der) {
5        int medio = izq + (der - izq) / 2;
6        if (a[medio] < x) izq = medio + 1;
7        else der = medio;                  // a[medio] >= x: el medio puede ser la respuesta
8    }
9    return izq;                            // izq == der: la frontera
10}

Traza: buscar 23 en {3, 8, 15, 23, 42, 57, 61, 78, 90}

Pasoizqdermedioa[medio]Decisión
10844223 < 42 → der = 3
2031823 > 8 → izq = 2
32321523 > 15 → izq = 3
433323encontrado en la posición 3

Cuatro comparaciones para nueve elementos; una búsqueda lineal habría necesitado otras cuatro para llegar al 23, pero para el 90 habría necesitado nueve y la binaria, cuatro.

Complejidad

Datos (n)Búsqueda lineal (peor caso)Búsqueda binaria (peor caso)
1.0001.000 comparaciones10 comparaciones
1.000.0001.000.00020
1.000.000.0001.000.000.00030

La búsqueda binaria es O(log n): duplicar los datos solo añade una comparación. El mejor caso es O(1) (el valor está justo en el centro). En memoria, la versión iterativa es O(1) y la recursiva O(log n), por las llamadas que se apilan.

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(log n)
  • Peor caso: O(log n)

Duplicar los datos solo añade una comparación. Las curvas grises son las demás clases, para comparar.

En la práctica

  • Arrays.binarySearch(a, x) y Collections.binarySearch(lista, x): si el valor no está devuelven -(punto de inserción) - 1, un número negativo que dice dónde iría.
  • Los índices de las bases de datos (árboles B+) bajan por niveles descartando ramas, la misma idea con muchos hijos por nodo.
  • git bisect encuentra el commit que introdujo un error con una búsqueda binaria entre un commit bueno y uno malo.
  • Muchos problemas de optimización («la capacidad mínima», «el tiempo mínimo») se resuelven con búsqueda binaria sobre la respuesta.

Errores típicos

  • Calcular el medio con (izq + der) / 2: con índices enormes la suma se desborda y sale negativa. izq + (der - izq) / 2 no se desborda (este fallo estuvo años en la propia biblioteca de Java).
  • Mezclar las dos convenciones: con der inclusivo el bucle es while (izq <= der) y se actualiza con medio ± 1; con der exclusivo es while (izq < der) y der = medio. Mezclarlas deja un elemento sin mirar o un bucle infinito.
  • Actualizar con izq = medio en la versión inclusiva: cuando izq y der quedan juntos, el medio no cambia y el bucle no termina.
  • Usarla sobre datos sin ordenar, u ordenados con otro criterio (por ejemplo, nombres ordenados sin tener en cuenta mayúsculas y buscados con compareTo).
  • Esperar la primera aparición de un valor repetido: la versión clásica devuelve una cualquiera.

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. Primera y última aparición

La primera línea trae un array de enteros ordenado de menor a mayor (puede tener repetidos). Cada línea siguiente es una consulta: un número. Para cada consulta, di cuántas veces aparece y entre qué posiciones, usando dos búsquedas binarias: la primera posición con un valor mayor o igual y la primera con un valor mayor. El main ya está escrito: completa las dos funciones.

  • Si aparece: x aparece N veces, de la posición I a la J (o x aparece 1 vez, en la posición I).
  • Si no aparece: x no está; iría en la posición P (donde habría que insertarlo para que siga ordenado).
  • Una consulta que no es un número: Consulta no válida: «texto». Si el array no está ordenado: El array no está ordenado y nada más.
☕JavaPrimera y última apariciónMedio

Ejemplo

Entrada (lo que se escribe por teclado)
1 3 3 3 5 8 8 13
3
8
4
13
0
20
Salida esperada
3 aparece 3 veces, de la posición 1 a la 3
8 aparece 2 veces, de la posición 5 a la 6
4 no está; iría en la posición 4
13 aparece 1 vez, en la posición 7
0 no está; iría en la posición 0
20 no está; iría en la posición 8
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 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    /** Primera posición con a[i] >= x (a.length si no hay ninguna). */
5    static int primeraMayorOIgual(int[] a, int x) {
6        int izq = 0, der = a.length;
7        while (izq < der) {
8            int medio = izq + (der - izq) / 2;
9            if (a[medio] < x) izq = medio + 1;
10            else der = medio;
11        }
12        return izq;
13    }
14
15    /** Primera posición con a[i] > x (a.length si no hay ninguna). */
16    static int primeraMayor(int[] a, int x) {
17        int izq = 0, der = a.length;
18        while (izq < der) {
19            int medio = izq + (der - izq) / 2;
20            if (a[medio] <= x) izq = medio + 1;
21            else der = medio;
22        }
23        return izq;
24    }
25
26    public static void main(String[] args) {
27        Scanner sc = new Scanner(System.in);
28        String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
29        int[] a;
30        try {
31            a = primera.isEmpty() ? new int[0] : Arrays.stream(primera.split("\\s+")).mapToInt(Integer::parseInt).toArray();
32        } catch (NumberFormatException e) {
33            System.out.println("Array no válido");
34            return;
35        }
36        for (int i = 1; i < a.length; i++) {
37            if (a[i] < a[i - 1]) {
38                System.out.println("El array no está ordenado");
39                return;
40            }
41        }
42        while (sc.hasNextLine()) {
43            String linea = sc.nextLine().trim();
44            if (linea.isEmpty()) continue;
45            int x;
46            try {
47                x = Integer.parseInt(linea);
48            } catch (NumberFormatException e) {
49                System.out.println("Consulta no válida: «" + linea + "»");
50                continue;
51            }
52            int desde = primeraMayorOIgual(a, x), hasta = primeraMayor(a, x);
53            int veces = hasta - desde;
54            if (veces <= 0) System.out.println(x + " no está; iría en la posición " + desde);
55            else if (veces == 1) System.out.println(x + " aparece 1 vez, en la posición " + desde);
56            else System.out.println(x + " aparece " + veces + " veces, de la posición " + desde + " a la " + (hasta - 1));
57        }
58    }
59}

primeraMayorOIgual y primeraMayor buscan dos fronteras del array: dónde empiezan los valores ≥ x y dónde empiezan los > x. Entre las dos están exactamente las apariciones de x, y si coinciden, x no está y esa es su posición de inserción.

Cada consulta cuesta dos búsquedas de O(log n), aunque x aparezca miles de veces: recorrer las repeticiones desde una aparición cualquiera sería O(n).

2. La capacidad mínima del camión

Un camión reparte paquetes en un orden fijo: cada día carga paquetes seguidos hasta que el siguiente ya no cabe, y no puede saltarse ninguno. ¿Cuál es la capacidad mínima del camión para repartirlo todo en D días como mucho? Resuélvelo con búsqueda binaria sobre la respuesta: la capacidad está entre el paquete más pesado y la suma de todos, y si una capacidad sirve, también sirven todas las mayores. El reparto y la salida ya están escritos: completa cabe y capacidadMinima.

  • Línea 1: el número de días D. Línea 2: los pesos de los paquetes, en orden, separados por espacios.
  • Salida: Capacidad mínima: C y, por cada día, Día N: p1 + p2 + … = total; si sobran días, Sobran K días (1 día).
  • Errores: Días no válidos, No hay paquetes o Peso no válido: «texto» (los pesos son enteros mayores que 0).
☕JavaLa capacidad mínima del camiónDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
5
1 2 3 4 5 6 7 8 9 10
Salida esperada
Capacidad mínima: 15
Día 1: 1 + 2 + 3 + 4 + 5 = 15
Día 2: 6 + 7 = 13
Día 3: 8 = 8
Día 4: 9 = 9
Día 5: 10 = 10
⏳
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    /** Reparte los paquetes en orden, llenando cada día hasta la capacidad (al menos un paquete por día). */
5    static List<List<Integer>> reparto(int[] p, int capacidad) {
6        List<List<Integer>> dias = new ArrayList<>();
7        List<Integer> hoy = new ArrayList<>();
8        int carga = 0;
9        for (int x : p) {
10            if (!hoy.isEmpty() && carga + x > capacidad) {
11                dias.add(hoy);
12                hoy = new ArrayList<>();
13                carga = 0;
14            }
15            hoy.add(x);
16            carga += x;
17        }
18        if (!hoy.isEmpty()) dias.add(hoy);
19        return dias;
20    }
21
22    /** ¿Se pueden repartir los paquetes en como mucho «dias» días con esa capacidad? */
23    static boolean cabe(int[] p, int dias, int capacidad) {
24        int usados = 1, carga = 0;
25        for (int x : p) {
26            if (x > capacidad) return false;
27            if (carga + x > capacidad) {
28                usados++;
29                carga = 0;
30            }
31            carga += x;
32        }
33        return usados <= dias;
34    }
35
36    /** Búsqueda binaria sobre la respuesta: la capacidad mínima está entre el paquete más pesado y la suma. */
37    static int capacidadMinima(int[] p, int dias) {
38        int izq = Arrays.stream(p).max().getAsInt(), der = Arrays.stream(p).sum();
39        while (izq < der) {
40            int medio = izq + (der - izq) / 2;
41            if (cabe(p, dias, medio)) der = medio;      // vale: quizá también una menor
42            else izq = medio + 1;                       // no vale: hace falta más
43        }
44        return izq;
45    }
46
47    public static void main(String[] args) {
48        Scanner sc = new Scanner(System.in);
49        int dias;
50        try {
51            dias = Integer.parseInt(sc.nextLine().trim());
52        } catch (Exception e) {
53            dias = 0;
54        }
55        if (dias <= 0) {
56            System.out.println("Días no válidos");
57            return;
58        }
59        String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
60        if (linea.isEmpty()) {
61            System.out.println("No hay paquetes");
62            return;
63        }
64        String[] partes = linea.split("\\s+");
65        int[] p = new int[partes.length];
66        for (int i = 0; i < partes.length; i++) {
67            try {
68                p[i] = Integer.parseInt(partes[i]);
69            } catch (NumberFormatException e) {
70                p[i] = 0;
71            }
72            if (p[i] <= 0) {
73                System.out.println("Peso no válido: «" + partes[i] + "»");
74                return;
75            }
76        }
77        int c = capacidadMinima(p, dias);
78        System.out.println("Capacidad mínima: " + c);
79        List<List<Integer>> r = reparto(p, c);
80        for (int i = 0; i < r.size(); i++) {
81            List<Integer> d = r.get(i);
82            int suma = d.stream().mapToInt(Integer::intValue).sum();
83            System.out.println("Día " + (i + 1) + ": " + String.join(" + ", d.stream().map(String::valueOf).toList()) + " = " + suma);
84        }
85        if (r.size() < dias) System.out.println("Sobran " + (dias - r.size()) + (dias - r.size() == 1 ? " día" : " días"));
86    }
87}

La clave es la monotonía: si con capacidad C se puede, con C + 1 también. Eso convierte «encontrar la mínima» en buscar una frontera en el rango de capacidades posibles, como en un array ordenado de «no, no, no, sí, sí, sí».

Cada comprobación recorre los n paquetes, y hacen falta log₂(suma − máximo) comprobaciones: O(n log S). Probar todas las capacidades una a una sería O(n · S).

Test

Test: Búsqueda binaria

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 tienen que cumplir los datos para usar la búsqueda binaria?

  2. 2.¿Cuántas comparaciones necesita, como mucho, para buscar en un array ordenado de un millón de elementos?

  3. 3.¿Por qué se calcula el medio con izq + (der - izq) / 2 en lugar de (izq + der) / 2?

  4. 4.Arrays.binarySearch(new int[]{2, 4, 6}, 5) devuelve:

  5. 5.Tienes un array desordenado y vas a buscar un único valor. ¿Qué es mejor?

Relacionado