Salta al contenuto
Note per Studenti Ordinare e cercare in C - qsort, bsearch e puntatori a funzione

Ordinare e cercare in C - qsort, bsearch e puntatori a funzione

In questa pagina 5
In questa pagina 4

In Python si scrive sorted(v, key=...) e bisect. In C la libreria standard (<stdlib.h>) offre qsort e bsearch, che lavorano su qualsiasi tipo di array e ricevono dal chiamante una funzione di confronto: serve quindi il puntatore a funzione (Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →, Funzioni in CDefinizione e prototipo di una funzione C, tipo di ritorno e void, passaggio dei parametri sempre per valore, variabili locali, globali e static, file header e compilazione separata, funzioni ricorsive.Funzioni in C →).

Puntatori a funzione

Il nome di una funzione, senza parentesi, è il suo indirizzo. Un puntatore a funzione si dichiara indicando firma e nome tra parentesi tonde con *:

c
#include <stdio.h>

int somma(int a, int b) { return a + b; }
int prodotto(int a, int b) { return a * b; }

int applica(int (*f)(int, int), int x, int y) {   /* f: puntatore a funzione */
    return f(x, y);                                /* chiamata tramite il puntatore */
}

int main(void) {
    printf("%d %d\n", applica(somma, 3, 4), applica(prodotto, 3, 4));   /* 7 12 */
    return 0;
}

int (*f)(int, int) è un puntatore a una funzione con due int che restituisce int; senza le parentesi, int *f(int, int) sarebbe una funzione che restituisce int *. Con typedef int (*Confronto)(const void *, const void *); il tipo ha un nome.

qsort

c
void qsort(void *base, size_t n, size_t dim, int (*cmp)(const void *, const void *));
  • base: indirizzo del primo elemento; n: numero di elementi; dim: dimensione in byte di un elemento (sizeof v[0]).
  • cmp(a, b) riceve puntatori agli elementi (come const void *) e restituisce un numero < 0, 0 o > 0 se *a va prima, pari o dopo *b. Si converte il puntatore al tipo vero dell'elemento.
c
#include <stdlib.h>
#include <string.h>

int cmp_int(const void *a, const void *b) {
    int x = *(const int *)a, y = *(const int *)b;
    return (x > y) - (x < y);          /* mai x - y: può andare in overflow */
}

int cmp_str(const void *a, const void *b) {          /* array di char *: elementi di tipo char * */
    return strcmp(*(const char * const *)a, *(const char * const *)b);
}

typedef struct { char nome[32]; long num; } Voce;

int cmp_voce(const void *a, const void *b) {         /* per nome, poi per numero */
    const Voce *p = a, *q = b;
    int c = strcmp(p->nome, q->nome);
    return c != 0 ? c : (p->num > q->num) - (p->num < q->num);
}
c
int v[] = {5, -2, 9, 0};
qsort(v, 4, sizeof v[0], cmp_int);                    /* -2 0 5 9 */

const char *nomi[] = {"Zeno", "Ada", "Mia"};
qsort(nomi, 3, sizeof nomi[0], cmp_str);              /* Ada Mia Zeno */

Voce rubrica[100]; int n = 0;                         /* ... riempita ... */
qsort(rubrica, n, sizeof rubrica[0], cmp_voce);

qsort non è stabile (elementi uguali possono cambiare ordine relativo), costa tipicamente O(nlog⁡n)O(n \log n) e usa la stessa funzione cmp per ogni coppia: deve essere un ordine coerente (cmp(a,b) e cmp(b,a) di segno opposto).

bsearch

c
void *bsearch(const void *chiave, const void *base, size_t n, size_t dim,
              int (*cmp)(const void *, const void *));

Ricerca binaria (Ricerca lineare e binariaRicerca lineare su sequenze qualsiasi in O(n); ricerca binaria su sequenze ordinate in O(log n), versione iterativa e ricorsiva, invariante e errori di indice; modulo bisect.Ricerca lineare e binaria →) in O(log⁡n)O(\log n): restituisce il puntatore all'elemento trovato o NULL. L'array deve essere già ordinato con lo stesso cmp. La chiave è un puntatore a un elemento "di prova" (anche un Voce con solo il campo chiave riempito); l'indice si ricava con la differenza di puntatori.

c
int cmp_nome(const void *a, const void *b) {         /* confronta solo il campo chiave */
    return strcmp(((const Voce *)a)->nome, ((const Voce *)b)->nome);
}

qsort(rubrica, n, sizeof rubrica[0], cmp_nome);       /* ordinata per nome */
Voce cerca = { .nome = "Mia" };                       /* basta riempire il campo chiave */
Voce *p = bsearch(&cerca, rubrica, n, sizeof rubrica[0], cmp_nome);
if (p != NULL)
    printf("indice %ld, numero %ld\n", (long)(p - rubrica), p->num);

Con cmp_voce (che guarda anche num) la ricerca per solo nome fallirebbe: la chiave di prova viene confrontata con lo stesso cmp dell'ordinamento, quindi cmp deve guardare solo i campi che identificano l'elemento cercato.

Dizionario ordinato su array dinamico di struct

È l'analogo C del dizionario su array ordinato di Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →: ricerca O(log⁡n)O(\log n) con bsearch, inserimento O(n)O(n) con memmove, capacità che raddoppia con realloc (Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →).

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

typedef struct { char nome[32]; long num; } Voce;
typedef struct { Voce *v; size_t n, cap; } Rubrica;

static int cmp_nome(const void *a, const void *b) {
    return strcmp(((const Voce *)a)->nome, ((const Voce *)b)->nome);
}

void rubrica_init(Rubrica *r) { r->v = NULL; r->n = r->cap = 0; }
void rubrica_libera(Rubrica *r) { free(r->v); rubrica_init(r); }

Voce *rubrica_cerca(const Rubrica *r, const char *nome) {      /* O(log n) */
    Voce chiave;
    strncpy(chiave.nome, nome, sizeof chiave.nome - 1);
    chiave.nome[sizeof chiave.nome - 1] = '\0';
    return bsearch(&chiave, r->v, r->n, sizeof(Voce), cmp_nome);
}

int rubrica_inserisci(Rubrica *r, const char *nome, long num) {   /* O(n); 0 = ok */
    Voce *p = rubrica_cerca(r, nome);
    if (p != NULL) { p->num = num; return 0; }                     /* nome presente: sostituisce */
    if (r->n == r->cap) {                                          /* pieno: raddoppia */
        size_t nuova = r->cap ? 2 * r->cap : 4;
        Voce *t = realloc(r->v, nuova * sizeof(Voce));
        if (t == NULL) return -1;
        r->v = t; r->cap = nuova;
    }
    size_t i = 0;                                                  /* posto: primo elemento >= nome */
    while (i < r->n && strcmp(r->v[i].nome, nome) < 0) i++;
    memmove(&r->v[i + 1], &r->v[i], (r->n - i) * sizeof(Voce));    /* sposta a destra (memmove, non memcpy) */
    strncpy(r->v[i].nome, nome, sizeof r->v[i].nome - 1);
    r->v[i].nome[sizeof r->v[i].nome - 1] = '\0';
    r->v[i].num = num;
    r->n++;
    return 0;
}

int main(void) {
    Rubrica r; rubrica_init(&r);
    rubrica_inserisci(&r, "Zeno", 3);
    rubrica_inserisci(&r, "Ada", 1);
    rubrica_inserisci(&r, "Mia", 2);
    rubrica_inserisci(&r, "Ada", 9);                               /* sostituisce */
    for (size_t i = 0; i < r.n; i++) printf("%s %ld\n", r.v[i].nome, r.v[i].num);
    Voce *p = rubrica_cerca(&r, "Mia");
    printf("%s\n", p ? "trovata" : "assente");
    rubrica_libera(&r);
    return 0;
}

Output: Ada 9, Mia 2, Zeno 3, trovata. Il posto d'inserimento è cercato qui in modo lineare per brevità; con una ricerca binaria "del punto d'inserimento" (come bisect_left) l'intero inserisci resta comunque O(n)O(n) per lo spostamento.

Errori tipici

  • Funzione di confronto che restituisce x - y: overflow con interi grandi.
  • Dimenticare il cast del void * al tipo vero, o usare un livello di puntatore sbagliato (negli array di stringhe l'elemento è un char *, il confronto riceve char **).
  • Chiamare bsearch su un array non ordinato o ordinato con un confronto diverso: risultato imprevedibile.
  • Passare a qsort il numero di byte invece del numero di elementi, o sizeof v (dimensione dell'array intero) al posto di sizeof v[0].
  • Usare memcpy per spostare zone che si sovrappongono: serve memmove.
  • Perdere il vecchio puntatore con v = realloc(v, ...) quando realloc fallisce (restituisce NULL): si usa un puntatore temporaneo.

Versione ripasso

Puntatori a funzione

int (*f)(int, int): puntatore a funzione con due int che restituisce int (senza parentesi sarebbe una funzione che restituisce int *); il nome della funzione è il suo indirizzo; chiamata f(x, y) (Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →, Funzioni in CDefinizione e prototipo di una funzione C, tipo di ritorno e void, passaggio dei parametri sempre per valore, variabili locali, globali e static, file header e compilazione separata, funzioni ricorsive.Funzioni in C →).

qsort

qsort(base, n, sizeof base[0], cmp); cmp(const void *a, const void *b) riceve puntatori agli elementi e restituisce <0, 0, >0. Mai x - y (overflow): (x > y) - (x < y). Array di stringhe: elementi char *, quindi strcmp(*(const char * const *)a, *(const char * const *)b); struct: confronto sul campo chiave. Non stabile, O(nlog⁡n)O(n\log n).

bsearch

bsearch(&chiave, base, n, sizeof base[0], cmp): puntatore all'elemento o NULL, O(log⁡n)O(\log n); l'array dev'essere ordinato con lo stesso cmp; indice = p - base (Ricerca lineare e binariaRicerca lineare su sequenze qualsiasi in O(n); ricerca binaria su sequenze ordinate in O(log n), versione iterativa e ricorsiva, invariante e errori di indice; modulo bisect.Ricerca lineare e binaria →).

Dizionario ordinato su array dinamico di struct

struct { Voce *v; size_t n, cap; }: cerca con bsearch O(log⁡n)O(\log n); inserisci: nome presente → sostituisce; pieno → realloc raddoppiando (puntatore temporaneo); memmove per fare posto, O(n)O(n) (Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →, Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →).

Errori tipici: confronto x - y; cast o livello di puntatore sbagliato; bsearch su array non ordinato; sizeof v al posto di sizeof v[0]; memcpy su zone sovrapposte; v = realloc(v, ...) che perde il blocco se fallisce.

Esercizi su questo argomento