Esercizio 7ricorsione su file e stringhe
In questa pagina 4
Testo (appello del 31 gennaio 2023, esercizio 1, 10 punti, e appello del 23 febbraio 2023, esercizio 1, 10 punti, di Fondamenti di Informatica, Ingegneria dell'Informazione UniPD; adattato da un tema d'esame in Java, qui in Python e per il secondo anche in C).
- Scrivere un metodo ricorsivo che conta le righe di un file di testo che iniziano con i due caratteri
->. Provarlo sul filesupereroi.txt:
Ironman:Malibu
Spider man:New York
->Batman:Gotham
->Superman:Metropolis
->Flash:Central City
->Green Arrow:Starling City
Thor:Asgard
X-Men:Salem Center- Scrivere un programma che inverte una stringa in modo ricorsivo (l'inverso di
"Hello"è"olleH"): la stringa è passata come argomento sulla riga di comando e il risultato è stampato sullo standard output. Esempio:Rovescia FestivaldiCannesstampasennaCidlavitseF.
Teoria: RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →, File di testo in Pythonopen con modalità e codifica, il costrutto with, lettura riga per riga, read e readlines, scrittura con write e print, percorsi relativi, file CSV con il modulo csv.File di testo in Python →, Moduli e libreria standardOgni file .py è un modulo; forme di import; il blocco if name == "main"; moduli della libreria standard più usati (math, random, sys, os, time, collections).Moduli e libreria standard → (per sys.argv).
1. Contare le righe con la ricorsione
Schema. Una ricorsione sul file ha la stessa forma di una ricorsione su una lista: si consuma un elemento (una riga) e si risolve lo stesso problema sul resto.
- Caso base: non ci sono più righe → il conteggio è .
- Passo: leggo una riga; il conteggio è se la riga inizia con
->, altrimenti , più il conteggio sul resto del file.
Il "resto del file" è lo stesso oggetto file, la cui posizione di lettura è avanzata: non serve passare un indice.
import io
def conta_righe(f):
riga = f.readline()
if riga == "": # caso base: fine del file
return 0
c = 1 if riga.startswith("->") else 0
return c + conta_righe(f) # passo: stesso problema sul resto
testo = ("Ironman:Malibu\nSpider man:New York\n->Batman:Gotham\n->Superman:Metropolis\n"
"->Flash:Central City\n->Green Arrow:Starling City\nThor:Asgard\nX-Men:Salem Center\n")
print(conta_righe(io.StringIO(testo))) # 4(Con un file vero: with open("supereroi.txt") as f: print(conta_righe(f)).)
Punti delicati:
readline()restituisce""(stringa vuota) solo alla fine del file; una riga vuota del file è"\n", che non è"": così le righe vuote non fermano la ricorsione.riga.startswith("->")funziona anche per righe più corte di due caratteri (una riga-o vuota). La soluzione Java di partenza conline.charAt(0)eline.charAt(1)fallisce su una riga vuota o di un solo carattere.- La ricorsione non è in coda ( resta da fare dopo la chiamata): ogni riga occupa un frame sullo stack. Python ha un limite di circa chiamate annidate: per file di migliaia di righe questo programma darebbe
RecursionError. La versione iterativasum(1 for r in f if r.startswith("->"))non ha il problema; l'esercizio chiede però la ricorsione, che per un file piccolo va bene.
2. Rovesciare una stringa
Definizione ricorsiva. Per s di lunghezza 0 o 1 l'inverso è s stessa (caso base). Altrimenti l'inverso di s è l'inverso del resto (tutti i caratteri tranne il primo) seguito dal primo carattere:
import sys
def rovescia(s):
if len(s) < 2: # caso base: "" e un carattere
return s
return rovescia(s[1:]) + s[0] # passo: inverso del resto + primo carattere
if __name__ == "__main__":
if len(sys.argv) != 2:
print("uso: python rovescia.py <stringa>")
else:
print(rovescia(sys.argv[1])) # python rovescia.py FestivaldiCannes -> sennaCidlavitseFTraccia di rovescia("abc"):
rovescia("abc") = rovescia("bc") + "a"
= (rovescia("c") + "b") + "a"
= (("c") + "b") + "a" = "cba"Costo. Ogni chiamata crea una nuova stringa con s[1:] e +: per chiamata, in totale; la profondità è . Una variante che divide a metà ha profondità :
def rovescia2(s):
if len(s) < 2:
return s
m = len(s) // 2
return rovescia2(s[m:]) + rovescia2(s[:m]) # l'inverso è: inverso della seconda metà + inverso della primaCon una stringa di caratteri rovescia supera il limite di ricorsione di Python, rovescia2 no (profondità ).
In C: in loco
In C le stringhe sono array di char modificabili: si può rovesciare senza creare altre stringhe, scambiando gli estremi e restringendo l'intervallo.
#include <stdio.h>
#include <string.h>
/* rovescia s[i..j] scambiando gli estremi e restringendo l'intervallo */
void rovescia(char *s, int i, int j) {
if (i >= j) /* caso base: 0 o 1 carattere */
return;
char t = s[i]; s[i] = s[j]; s[j] = t;
rovescia(s, i + 1, j - 1); /* passo: problema più piccolo di 2 */
}
int main(int argc, char *argv[]) {
if (argc != 2) { fprintf(stderr, "uso: %s <stringa>\n", argv[0]); return 1; }
char buf[256];
strncpy(buf, argv[1], sizeof buf - 1);
buf[sizeof buf - 1] = '\0';
rovescia(buf, 0, (int)strlen(buf) - 1);
printf("%s\n", buf);
return 0;
}./rovescia FestivaldiCannes stampa sennaCidlavitseF. Il cast (int)strlen(buf) - 1 è necessario: strlen restituisce un tipo senza segno e per la stringa vuota strlen(buf) - 1 darebbe un numero enorme invece di . La ricorsione è in coda (l'ultima azione è la chiamata): un compilatore può trasformarla in un ciclo. Per le stringhe letterali come "Hello" non si può modificare in loco (char *s = "Hello" punta a memoria di sola lettura): serve una copia in un array, come buf.
Verifica
import random, string
assert conta_righe(io.StringIO("")) == 0
assert conta_righe(io.StringIO("\n-\n->\n")) == 1 # righe vuote o troppo corte
for _ in range(300):
s = "".join(random.choice(string.ascii_letters) for _ in range(random.randint(0, 60)))
assert rovescia(s) == s[::-1] == rovescia2(s)
print(rovescia("FestivaldiCannes"), rovescia2("FestivaldiCannes")) # sennaCidlavitseF sennaCidlavitseF
try:
rovescia("x" * 2000)
except RecursionError:
print("RecursionError")
print(rovescia2("x" * 2000) == "x" * 2000) # TrueErrori comuni
- Dimenticare il caso base (o metterlo troppo tardi):
RecursionErrorin Python, stack overflow in C. - Chiamare ricorsivamente con lo stesso problema (
rovescia(s)invece dirovescia(s[1:])). - Confondere riga vuota (
"\n") con fine file (""). - Accedere a
riga[0]eriga[1]senza controllare la lunghezza. - In C, modificare una stringa letterale o usare
strlen(s) - 1senza cast.
Versione ripasso
Appelli del 31 gennaio 2023 e del 23 febbraio 2023 (UniPD, in Java; adattato a Python e C). Teoria: RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →, File di testo in Pythonopen con modalità e codifica, il costrutto with, lettura riga per riga, read e readlines, scrittura con write e print, percorsi relativi, file CSV con il modulo csv.File di testo in Python →.
Conta righe ->. Caso base: readline() == "" → 0. Passo: (1 se riga.startswith("->") altrimenti 0) + conta_righe(f). Con la riga vuota "\n" ≠ ""; startswith regge anche righe corte (charAt(1) no). Non in coda: un frame per riga, limite di Python ~1000 (RecursionError).
Sul file di esempio il risultato è 4.
Rovescia. , base: lunghezza . rovescia(s[1:]) + s[0]: per le copie, profondità . Variante a metà rovescia2(s[m:]) + rovescia2(s[:m]): profondità . rovescia("FestivaldiCannes") → sennaCidlavitseF.
In C: rovescia(s, i, j) con base i >= j, scambio di s[i] e s[j], chiamata su (i+1, j-1); si usa un array copia (non un letterale) e il cast (int)strlen(s) - 1.
Errori comuni: caso base mancante; stesso problema richiamato; "\n" confuso con fine file; riga[1] senza controllo; stringa letterale modificata in C.