Salta al contenuto
Note per Studenti Esercizio 28 · C, palindromi in una lista

Esercizio 28C, palindromi in una lista

In questa pagina 6

Testo (scritto del 05/02/2024, parte 3, in C). Palindromi. Implementiamo un algoritmo per determinare se la parola contenuta in una lista concatenata è palindroma (parole che restano uguali se lette al contrario: "anna" e "bob" sono palindromi, "ciao" e "bod" non lo sono). Esistono vari modi per farlo; noi implementiamo un semplice algoritmo che crea una copia in ordine invertito della lista da controllare e verifica se la lista di origine e quella invertita sono identiche. Si chiede di implementare le seguenti funzioni, con typedef struct nodo { char value; struct nodo *next; } Nodo;:

c
void add(Nodo **lis, char c);
int compareLists(Nodo *lis, Nodo *comp);
void copyReversed(Nodo *lis, Nodo **copy);
int checkPalindrome(Nodo *lis);
  • add serve ad aggiungere un nodo in coda alla lista lis contenente il carattere c come valore.
  • compareLists deve restituire vero se lis e comp sono due liste identiche (stesso numero di nodi e contenuto dei nodi identico). Esistono più modi di eseguire questo controllo, ma si consiglia di usare la ricorsione.
  • copyReversed(lis, copy): Input: lis (una lista), copy (una lista vuota). Result: nessun ritorno; side effect: copy contiene una lista di lunghezza uguale a lis, in cui i nodi contengono gli stessi caratteri di quelli di lis ma in ordine inverso. Pseudocodice: se lis è una lista vuota, ritorna; copyReversed(lista a partire dal nodo successivo di lis, copy); add(copy, carattere contenuto nel primo nodo di lis).
  • checkPalindrome(lis): Result: vero se la lista contiene una sequenza di caratteri palindroma, falso altrimenti. Pseudocodice: inv <- nuova lista vuota; copyReversed(lis, inv); return compareLists(lis, inv).

Non è permesso modificare le firme (parametri e tipi di ritorno) delle funzioni. Lo pseudocodice non è codice C completo (tipi, puntatori, operatore ->). Nota del testo: copyReversed ha la chiamata ricorsiva a sé stessa prima di effettuare la chiamata ad add; questo ordine è importante. (Perché?) Si consiglia di usare la funzione di stampa per verificare che la copia invertita funzioni prima di procedere con il resto.


Richiami

Liste concatenate in C, aggiunta in coda con Nodo ** e ricorsione sulle liste (vedi Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C →); passaggio per riferimento (vedi Puntatori, struct e memoria dinamica in CPuntatori e passaggio per riferimento in C, array e aritmetica dei puntatori, stringhe, struct e typedef con l'operatore ->, malloc e free, puntatore a puntatore per modificare una testa; compilazione con Makefile; errori tipici (puntatori pendenti, perdite di memoria, off-by-one).Puntatori, struct e memoria dinamica in C →).

Soluzione

c
void add(Nodo **lista, char c) {                  /* aggiunge in coda */
    while (*lista != NULL)
        lista = &(*lista)->next;                  /* avanza il puntatore al puntatore */
    Nodo *n = malloc(sizeof(Nodo));
    n->value = c;
    n->next = NULL;
    *lista = n;
}

int compareLists(Nodo *a, Nodo *b) {
    if (a == NULL && b == NULL) return 1;         /* finite insieme: uguali */
    if (a == NULL || b == NULL) return 0;         /* lunghezze diverse */
    if (a->value != b->value) return 0;
    return compareLists(a->next, b->next);
}

void copyReversed(Nodo *src, Nodo **copy) {
    if (src == NULL)
        return;
    copyReversed(src->next, copy);                /* prima il resto della lista... */
    add(copy, src->value);                        /* ...poi il primo carattere, in coda */
}

int checkPalindrome(Nodo *lista) {
    Nodo *inv = NULL;
    copyReversed(lista, &inv);
    int r = compareLists(lista, inv);
    while (inv != NULL) {                         /* libera la copia temporanea */
        Nodo *t = inv->next;
        free(inv);
        inv = t;
    }
    return r;
}

Perché la chiamata ricorsiva viene prima di add

add inserisce in coda. Per costruire la lista inversa il primo elemento da accodare deve essere l'ultimo della lista originale. Con la chiamata ricorsiva prima di add, la ricorsione scende fino al fondo e gli add avvengono risalendo: l'ultimo nodo viene accodato per primo, poi il penultimo, e così via fino al primo, che viene accodato per ultimo. Per "ciao": le chiamate scendono su c, i, a, o; risalendo si aggiungono o, a, i, c: la copia è oaic (verificato). Se add fosse prima della chiamata ricorsiva si otterrebbe una copia identica all'originale (ciao) e ogni parola risulterebbe palindroma.

Complessità

  • compareLists: Θ(min⁡(na,nb)+1)\Theta(\min(n_a, n_b) + 1), al più Θ(n)\Theta(n) (una chiamata per coppia di nodi).
  • add: Θ(k)\Theta(k) se la lista ha già kk nodi (scorre fino alla coda). Quindi copyReversed esegue nn add su liste di lunghezza 0,1,…,n−10, 1, \dots, n-1: ∑k=0n−1(k+1)=Θ(n2)\sum_{k=0}^{n-1} (k + 1) = \Theta(n^2).
  • checkPalindrome: Θ(n2)\Theta(n^2) per la copia più Θ(n)\Theta(n) per il confronto: Θ(n2)\Theta(n^2) in tempo, Θ(n)\Theta(n) in spazio (la copia e lo stack della ricorsione).

Si può scendere a Θ(n)\Theta(n) inserendo in testa (addHead, Θ(1)\Theta(1)): scorrendo la lista originale dall'inizio e inserendo ogni carattere in testa alla copia si ottiene l'ordine inverso senza ricorsione.

Prove eseguite

checkPalindrome restituisce 11 per anna, bob, a, abcba, abccba e la lista vuota (vuota: due liste vuote sono uguali); 00 per ciao, bod, ab, abcda.

Errori comuni

  • Invertire l'ordine ricorsione/add in copyReversed: la copia non è invertita.
  • In compareLists confrontare i valori senza controllare prima che nessuno dei due puntatori sia NULL (dereferenzia NULL), o dimenticare il caso di liste di lunghezza diversa ("ab" e "abc").
  • In add usare Nodo * invece di Nodo **: quando la lista è vuota la testa del chiamante non viene aggiornata.
  • Non inizializzare n->next = NULL nel nuovo nodo.
  • Non liberare la copia temporanea (perdita di memoria).

Versione ripasso

Testo. Lista concatenata di caratteri (Nodo { char value; Nodo *next; }); add (in coda), compareLists, copyReversed, checkPalindrome (copia invertita e confronto). Perché in copyReversed la ricorsione precede add?

  • add(Nodo **lista, char c) (vedi Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C →): scorre lista = &(*lista)->next fino a NULL, poi crea il nodo con next = NULL.
  • compareLists: entrambe NULL ⇒ 11; una sola NULL ⇒ 00; valori diversi ⇒ 00; altrimenti ricorsione su next.
  • copyReversed: NULL ⇒ ritorno; prima copyReversed(src->next, copy), poi add(copy, src->value): gli add avvengono risalendo, quindi l'ultimo carattere entra per primo (ciao ⇒ oaic); invertendo l'ordine si avrebbe una copia identica.
  • checkPalindrome: copia, confronto, free della copia.
  • Costi: compareLists Θ(n)\Theta(n); copyReversed Θ(n2)\Theta(n^2) perché add è Θ(k)\Theta(k); con addHead si arriva a Θ(n)\Theta(n).
  • Prove: palindrome anna, bob, a, abcba, abccba, vuota; non palindrome ciao, bod, ab.
  • Codice: add: while (*lista != NULL) lista = &(*lista)->next; poi malloc, next = NULL, *lista = n; compareLists: due NULL →1\to 1, una sola →0\to 0, valori diversi →0\to 0, altrimenti ricorsione; checkPalindrome: copyReversed(lista, &inv), compareLists(lista, inv), free della copia.
  • Perché la ricorsione prima di add: gli add avvengono risalendo, quindi l'ultimo carattere entra per primo.
  • Errori: ordine ricorsione/add scambiato; NULL non controllato; Nodo * in add; next non inizializzato; copia non liberata.

Teoria collegata