qsort y bsearch: ordenación y búsqueda genéricas

Para manejar tipos de datos arbitrarios sin duplicar código, C utiliza la abstracción de punteros void *. Las funciones qsort y bsearch [stdlib.h] operan sobre bloques de memoria genéricos, delegando la lógica de comparación a una función que el programador debe proveer. Mientras que qsort realiza una ordenación in-place (modificando el array original), bsearch implementa una búsqueda binaria eficiente, siempre que el array ya esté ordenado bajo el mismo criterio. Si intentas usar bsearch en un array desordenado, el resultado será errático o simplemente fallará en encontrar elementos existentes. Si fallas al calcular el tamaño de los elementos o la lógica de comparación, causarás comportamiento indefinido, desbordamientos de memoria o corrupción de datos.

Cuando utilizas qsort, le entregas un puntero al primer elemento, el número de elementos, el tamaño en bytes de cada uno y un puntero a una función de comparación. El uso de void * permite que el algoritmo de ordenación no necesite conocer la estructura de los datos; solo necesita saber cuánto mide cada bloque para saltar al siguiente. bsearch sigue un patrón similar, pero su función de comparación recibe un “par” distinto: el primer argumento es la clave de búsqueda (la “llave”) y el segundo es el elemento actual del array.

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int id;
    float salario;
} Empleado;

/* Comparación para qsort: Ordenar por ID (ascendente) */
int comparar_ids(const void *a, const void *b) {
    const Empleado *e1 = a;
    const Empleado *e2 = b;
    return (e1->id > e2->id) - (e1->id < e2->id);
}

/* Comparación para qsort: Ordenar por Salario (descendente) */
int comparar_salarios_desc(const void *a, const void *b) {
    const Empleado *e1 = a;
    const Empleado *e2 = b;
    if (e1->salario < e2->salario) return 1;
    if (e1->salario > e2->salario) return -1;
    return 0;
}

/* Comparación para bsearch: Buscar por ID */
int comparar_bsearch_id(const void *key, const void *elem) {
    /* El 'key' es un puntero al valor que buscamos (en este caso, un int) */
    int id_buscado = *(const int *)key;
    const Empleado *e = elem;
    return id_buscado - e->id;
}

int main(void) {
    Empleado empleados[] = {
        {105, 1800.75f},
        {101, 3100.00f},
        {103, 2500.50f},
        {102, 4200.20f}
    };
    size_t n = sizeof(empleados) / sizeof(empleados);

    /* 1. Ordenar por ID para permitir la búsqueda binaria */
    qsort(empleados, n, sizeof(Empleado), comparar_ids);

    /* 2. Buscar un empleado por su ID */
    int target_id = 102;
    Empleado *encontrado = bsearch(&target_id, empleados, n, sizeof(Empleado), comparar_bsearch_id);

    if (encontrado) {
        printf("Empleado encontrado: ID %d, Salario: %.2f\n", encontrado->id, encontrado->salario);
    } else {
        printf("Empleado con ID %d no encontrado.\n", target_id);
    }

    /* 3. Ordenar por salario de mayor a menor */
    qsort(empleados, n, sizeof(Empleado), comparar_salarios_desc);

    printf("Top salarios:\n");
    for (size_t i = 0; i < n; i++) {
        printf("ID: %d | Salario: %.2f\n", empleados[i].id, empleados[i].salario);
    }

    return 0;
}

En el ejemplo anterior, observa cómo qsort utiliza sizeof(Empleado) para saber exactamente cuántos bytes debe saltar en la memoria para pasar del elemento empleados[0] al empleados[1]. Sin este valor exacto, el puntero interno de qsort perdería la alineación de la estructura y leería basura o provocaría un error de segmentación.

En la función comparar_bsearch_id, hay un detalle crítico que suele confundir: los argumentos de la función de comparación de bsearch no son ambos elementos del array. El primer argumento (key) es un puntero al objeto que estás buscando. Como estamos buscando por un int, pasamos la dirección de target_id (&target_id) y dentro de la función la desreferenciamos mediante un cast: *(const int *)key. El segundo argumento (elem) sí es el puntero al elemento actual del array, que debemos convertir a Empleado *.

Finalmente, fíjate en la lógica (e1->id > e2->id) - (e1->id < e2->id) en comparar_ids. Esta es una forma elegante y segura de obtener -1, 0 o 1 sin recurrir a sentencias if complejas, evitando problemas de desbordamiento que ocurrirían si intentaras simplemente restar los valores.

El error frecuente

Un error clásico al implementar funciones de comparación es la resta directa para tipos con signo:

/* ERROR: Puede causar desbordamiento de enteros */
int comparar_erroneo(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}

Si *(int *)a es INT_MAX y *(int *)b es un valor negativo pequeño, la operación a - b provocará un desbordamiento de entero (integer overflow), lo cual es comportamiento indefinido en C. Esto puede causar que el ordenamiento falle estrepitosamente. Siempre es preferible usar comparaciones lógicas explícitas o la técnica de la resta de booleanos. Además, herramientas como UBSan (Undefined Behavior Sanitizer) detectarán este desbordamiento durante las pruebas.

84

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio