qsort e bsearch: Ordenando e Buscando com a Biblioteca

[391] qsort e bsearch: Ordenando e Buscando com a Biblioteca

O atalho return x - y na função de comparação funciona até um inteiro grande estourar e inverter o sinal. Passado esse cuidado, a aula mostra como o void * e o callback dão generalidade ao qsort, como ordenar struct por qualquer campo, e por que o bsearch exige o vetor ordenado antes.
Linguagem C

12 min de leitura

Na aula anterior, aprendemos a passar comportamento como argumento através de ponteiros para função. Hoje colhemos o fruto mais imediato dessa ideia. A biblioteca padrão do C oferece duas funções poderosas — qsort, que ordena, e bsearch, que busca — capazes de trabalhar com qualquer tipo de dado. E o segredo de como elas conseguem essa generalidade é exatamente o ponteiro para função: você fornece o critério de comparação, e elas cuidam do algoritmo. Ao dominá-las, você evita reescrever ordenação a cada projeto e entende um padrão de design que atravessa toda a programação.

O problema da ordenação genérica

Ordenar um vetor de inteiros é simples. Mas e ordenar structs por um campo? E strings em ordem alfabética? E números em ordem decrescente? Se cada caso exigisse um algoritmo de ordenação próprio, seria um trabalho repetitivo e propenso a erros. A biblioteca resolve isso com uma abordagem elegante: qsort implementa o algoritmo de ordenação uma vez, de forma genérica, e delega a você apenas a decisão de como comparar dois elementos. Essa separação — algoritmo genérico, critério específico — é a razão de sua flexibilidade.

A assinatura de qsort

A função qsort, de <stdlib.h>, recebe quatro argumentos:

void qsort(void *base, size_t n, size_t tamanho,
           int (*comparar)(const void *, const void *));

Decifrando cada um: base é o endereço do vetor (por que void *, veremos já já); n é o número de elementos; tamanho é o tamanho de cada elemento em bytes (via sizeof); e comparar é um ponteiro para função — o callback que diz como ordenar. O tipo de retorno e os parâmetros de qsort usam void *, o "ponteiro genérico" do C, que pode apontar para qualquer tipo. É esse void * que permite a qsort trabalhar com inteiros, structs ou o que for — ela não precisa saber o tipo, só o tamanho e como comparar.

A função de comparação: o coração de tudo

O callback de comparação segue um contrato preciso, herdado da matemática da ordenação. Ele recebe ponteiros para dois elementos e deve retornar: um número negativo se o primeiro vem antes do segundo, zero se são equivalentes, e positivo se o primeiro vem depois. Vamos ordenar um vetor de inteiros:

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

// compara dois inteiros para ordem crescente
int comparar_int(const void *a, const void *b) {
    int x = *(const int *)a; // converte void* para int* e desreferencia
    int y = *(const int *)b;
    return x - y; // negativo, zero ou positivo
}

int main(void) {
    int v[] = {42, 7, 13, 99, 1, 27};
    int n = sizeof(v) / sizeof(v[0]);

    qsort(v, n, sizeof(int), comparar_int);

    for (int i = 0; i < n; i++) printf("%d ", v[i]); // 1 7 13 27 42 99
    printf("\n");
    return 0;
}

A parte que exige atenção é o corpo de comparar_int. Ela recebe const void *a e const void *b — ponteiros genéricos. Para usá-los como inteiros, primeiro os convertemos para const int * (com o cast (const int *)a) e depois desreferenciamos (*), obtendo os valores. O return x - y explora o contrato de forma esperta: se x < y, a diferença é negativa; se iguais, zero; se x > y, positiva — exatamente o que qsort espera. Toda a magia de ordenar está terceirizada; você só disse como comparar.

Um cuidado com o "x - y"

O truque x - y é conciso, mas tem uma armadilha que vale conhecer: para inteiros muito grandes, a subtração pode estourar (overflow) e produzir um sinal errado. A forma robusta usa comparações explícitas:

int comparar_int_seguro(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    if (x < y) return -1;
    if (x > y) return  1;
    return 0;
}

Para valores pequenos, x - y funciona bem e é comum em exemplos; para código de produção com inteiros que podem ser grandes, prefira as comparações explícitas. É a mesma disciplina defensiva que o C sempre pede: pensar nos casos extremos.

Ordenando em ordem decrescente ou por outros critérios

A beleza do design fica evidente quando mudamos o critério sem tocar no algoritmo. Para ordem decrescente, basta inverter a comparação:

int comparar_desc(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return y - x; // invertido: agora ordena do maior para o menor
}

Trocar x - y por y - x inverte toda a ordenação. Uma mesma qsort, comportamentos opostos, apenas pela escolha do callback — a demonstração viva do poder dos ponteiros para função da aula anterior.

Ordenando structs por um campo

Aqui o valor de qsort explode: ordenar um vetor de structs por qualquer campo é trivial. Vamos ordenar pessoas por idade:

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

typedef struct {
    char nome[50];
    int idade;
} Pessoa;

// compara duas pessoas pela idade
int comparar_por_idade(const void *a, const void *b) {
    const Pessoa *p1 = (const Pessoa *)a;
    const Pessoa *p2 = (const Pessoa *)b;
    return p1->idade - p2->idade;
}

int main(void) {
    Pessoa turma[] = {
        {"Ana", 30}, {"Bruno", 25}, {"Carla", 35}, {"Diego", 28}
    };
    int n = sizeof(turma) / sizeof(turma[0]);

    qsort(turma, n, sizeof(Pessoa), comparar_por_idade);

    for (int i = 0; i < n; i++) {
        printf("%s: %d anos\n", turma[i].nome, turma[i].idade);
    }
    // Bruno: 25, Diego: 28, Ana: 30, Carla: 35
    return 0;
}

O callback converte os void * para const Pessoa * e compara pelo campo idade. Para ordenar por nome em vez de idade, você escreveria outro callback usando strcmp(p1->nome, p2->nome) — e note que strcmp já segue o mesmo contrato (negativo/zero/positivo), encaixando-se perfeitamente. Trocar o critério de ordenação é trocar o callback; o vetor, o tamanho e a qsort permanecem idênticos.

bsearch: busca binária pronta

A companheira de qsort é bsearch, que faz uma busca binária — localiza um elemento num vetor já ordenado com enorme eficiência (examinando o meio e descartando metade a cada passo). Sua assinatura é quase idêntica à de qsort, mais o elemento procurado:

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

int comparar_int(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int main(void) {
    int v[] = {1, 7, 13, 27, 42, 99}; // JÁ ORDENADO — pré-requisito!
    int n = sizeof(v) / sizeof(v[0]);

    int chave = 27;
    int *achado = bsearch(&chave, v, n, sizeof(int), comparar_int);

    if (achado != NULL) {
        printf("Encontrado %d na posição %ld\n", *achado, achado - v); // posição 3
    } else {
        printf("Não encontrado.\n");
    }
    return 0;
}

O bsearch devolve um ponteiro para o elemento encontrado (ou NULL). Repare que ele reutiliza a mesma função de comparação de qsort — as duas foram feitas para trabalhar juntas. Mas há um pré-requisito absoluto: o vetor precisa estar ordenado para bsearch funcionar; a busca binária depende disso. O fluxo idiomático é: ordene uma vez com qsort, depois busque muitas vezes com bsearch. A eficiência compensa enormemente em vetores grandes: uma busca linear examinaria, no pior caso, todos os elementos; a binária examina apenas o logaritmo desse número.

Por que aprender isso importa além da conveniência

Poderíamos escrever nossa própria ordenação — e faremos, nas aulas de estruturas de dados, para entender os algoritmos por dentro. Mas há três razões para usar qsort/bsearch no dia a dia: elas são testadas e otimizadas (mais confiáveis que uma versão caseira às pressas), genéricas (um único código serve a qualquer tipo), e idiomáticas (todo programador C as reconhece). Mais do que isso, elas ensinam um padrão de design fundamental — o algoritmo genérico parametrizado por um callback — que você reencontrará em incontáveis contextos. Compreender por que elas usam void * e ponteiros para função é compreender como o C, apesar de não ter recursos de linguagens modernas, alcança generalidade.

Fechando a Fase 4

Com esta aula, encerramos a fase de entrada, saída e biblioteca padrão. Você aprendeu a fazer o programa conversar com o usuário e com arquivos (texto e binário), a aproveitar a rica biblioteca padrão, a manipular texto com segurança, e a usar ponteiros para função para escrever código genérico — culminando em qsort e bsearch. Na próxima fase, viramos uma chave importante: em vez de usar algoritmos e estruturas prontos, vamos construí-los do zero. Começamos pela recursão, a técnica em que uma função chama a si mesma — elegante, poderosa, e a base natural para percorrer as estruturas encadeadas que dominarão a Fase 5.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Use qsort para ordenar um vetor de 8 inteiros em ordem crescente. Depois, escreva um segundo callback e ordene o mesmo vetor em ordem decrescente. Imprima ambos os resultados.

Ver resposta

✓ Resposta:

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

int crescente(const void *a, const void *b) {
    return (*(const int *)a > *(const int *)b) - (*(const int *)a < *(const int *)b);
}
int decrescente(const void *a, const void *b) {
    return crescente(b, a); // inverte os argumentos
}

int main(void) {
    int v[8] = {5, 2, 8, 1, 9, 3, 7, 4};
    int n = 8;

    qsort(v, n, sizeof(int), crescente);
    for (int i = 0; i < n; i++) printf("%d ", v[i]); // 1 2 3 4 5 7 8 9
    printf("\n");

    qsort(v, n, sizeof(int), decrescente);
    for (int i = 0; i < n; i++) printf("%d ", v[i]); // 9 8 7 5 4 3 2 1
    printf("\n");
    return 0;
}

(Usei a forma (x>y)-(x<y), uma maneira segura contra overflow de produzir -1/0/1; x-y também funcionaria para valores pequenos.)

Exercício 2

Defina uma struct Produto (com nome e preco) e um vetor com 4 produtos. Use qsort para ordená-los por preço (do mais barato ao mais caro) e imprima o resultado.

Ver resposta

✓ Resposta:

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

typedef struct { char nome[40]; double preco; } Produto;

int por_preco(const void *a, const void *b) {
    double pa = ((const Produto *)a)->preco;
    double pb = ((const Produto *)b)->preco;
    if (pa < pb) return -1;
    if (pa > pb) return  1;
    return 0;
}

int main(void) {
    Produto v[4] = {{"Mochila", 149.9}, {"Caneta", 2.5}, {"Caderno", 12.9}, {"Estojo", 25.0}};
    qsort(v, 4, sizeof(Produto), por_preco);
    for (int i = 0; i < 4; i++) printf("%s: R$ %.2f\n", v[i].nome, v[i].preco);
    return 0;
}

Para double, usamos comparações explícitas (não a subtração, que não produz o inteiro esperado com ponto flutuante).

Exercício 3

Escreva um callback que ordene um vetor de strings (char *nomes[]) em ordem alfabética, usando strcmp dentro dele. (Dica: os elementos são char *, então o callback recebe ponteiros para char *, exigindo um cast para char ** e uma desreferência.)

Ver resposta

✓ Resposta:

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

int alfabetico(const void *a, const void *b) {
    // cada elemento é um char*, então recebemos ponteiros para char*
    const char *s1 = *(const char **)a;
    const char *s2 = *(const char **)b;
    return strcmp(s1, s2);
}

int main(void) {
    const char *nomes[] = {"Carla", "Ana", "Diego", "Bruno"};
    int n = 4;
    qsort(nomes, n, sizeof(char *), alfabetico);
    for (int i = 0; i < n; i++) printf("%s ", nomes[i]); // Ana Bruno Carla Diego
    printf("\n");
    return 0;
}

O detalhe sutil: como cada elemento do vetor é um char *, os ponteiros que o callback recebe são char ** — daí o cast (const char **) e a desreferência para obter cada string, que então passamos a strcmp (cujo contrato já casa com o de qsort).

Exercício 4

Ordene um vetor de inteiros com qsort e, em seguida, use bsearch para procurar três valores: um que existe, um que não existe, e o menor do vetor. Imprima o resultado de cada busca.

Ver resposta

✓ Resposta:

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

int cmp(const void *a, const void *b) {
    return (*(const int*)a > *(const int*)b) - (*(const int*)a < *(const int*)b);
}

int main(void) {
    int v[] = {42, 7, 13, 99, 1, 27};
    int n = 6;
    qsort(v, n, sizeof(int), cmp); // ordena primeiro: 1 7 13 27 42 99

    int chaves[] = {27, 50, 1};
    for (int i = 0; i < 3; i++) {
        int *r = bsearch(&chaves[i], v, n, sizeof(int), cmp);
        if (r != NULL) printf("%d encontrado (pos %ld)\n", chaves[i], r - v);
        else           printf("%d não encontrado\n", chaves[i]);
    }
    return 0;
}

Saída: 27 encontrado (pos 3), 50 não encontrado, 1 encontrado (pos 0). O bsearch só funciona porque ordenamos antes com qsort.

Exercício 5

Explique por que a função de comparação de qsort recebe parâmetros do tipo const void * em vez de, por exemplo, const int *. Como isso se relaciona com a capacidade de qsort ordenar qualquer tipo de dado?

Ver resposta

✓ Resposta: A função de comparação recebe const void * porque qsort é genérica: ela precisa funcionar com qualquer tipo de elemento — inteiros, double, structs, strings. Se os parâmetros fossem const int *, a função só serviria para inteiros. O void * é o "ponteiro genérico" do C, capaz de apontar para um dado de qualquer tipo sem conhecê-lo. A qsort trabalha apenas com endereços e tamanhos de bytes (o argumento sizeof), sem nunca saber o que os bytes significam; quem dá sentido a eles é o seu callback, que faz o cast do void * de volta para o tipo real ((const int *), (const Pessoa *), etc.) e compara adequadamente. Essa combinação — qsort manipulando bytes genéricos, o callback interpretando-os — é precisamente o que permite a uma única implementação de ordenação servir a todos os tipos. É o polimorfismo possível em C, construído sobre void * e ponteiros para função.

Comentários

Mais em Linguagem C

Vazamentos de Memória e Como Caçá-los
Vazamentos de Memória e Como Caçá-los

Noventa e nove execuções corretas e a centésima corrompida: é assim que um…

Variáveis, Tipos e a Memória por Trás Deles
Variáveis, Tipos e a Memória por Trás Deles

Um int não tem 4 bytes garantidos: o padrão fixa apenas mínimos e uma ordem, e…

Explorando a Biblioteca Padrão: stdlib, math, ctype, time
Explorando a Biblioteca Padrão: stdlib, math, ctype, time

Antes de escrever uma função, vale perguntar se ela já existe — e quase sempre…