Rim (nadaljevanje naloge Nim)
Rekurzivni nim - Rim
Napisali boste nekaj funkcij, ki ste jih pisali za nalogo Nim - le da bodo tokrat rekurzivne.
Obvezna naloga
Napiši naslednje funkcije:
vsota(s)vrne vsoto števil v podanem seznamu. To seveda že poznamo s predavanj, vendar vseeno sprogramiraj še enkrat. Sam(a). Vsoto, vemo, izračunamo tako, da k prvemu elementu prištejemo vsoto ostalih.vsebuje(s, x)vrneTrue, če seznamsvsebuje elementxinFalse, če ga ne. Stvar je preprosta: prazen seznam očitno ne vsebujex. Sicer pa preverimo, ali je prvi element enakx, ali pa jexvsebovan v preostanku seznama.najvecja_potenca(n)vrne največjo potenco števila 2, ki je manjša ali enaka podanemu številan. Klicnajvecja_potenca(22)vrne16, saj bi bilo32že preveč. Klicnajvecja_potenca(32)pa vrne32. Predpostaviti smeš, da jenpozitivno celo število.Kako to narediti rekurzivno? Preprosto: največja potenca, ki gre v
n, je dvakrat večja od največje potence, ki gre vnpolovic (če uporabljamo celoštevilsko deljenje).Je to res?
- največja potenca, ki gre v 22 je dvakrat večja od največje potence, ki gre v 11;
- največja potenca, ki gre v 11, je dvakrat večja od največje potence, ki gre v 5;
- največja potenca, ki gre v 5, je dvakrat večja od največje potence, ki gre v 2;
- največja, potenca, ki gre v 2, je dvakrat večja od največje potence, ki gre v 1;
- največja potenca, ki gre v 1, je 1; (... torej je največja potenca, ki gre v 2, 2; torej gre v 5 4, torej gre v 11 8, torej gre v 22 16).
Rešitev
Vsota števil v seznamu je enaka vrednosti prvega elementa, ki ji prištejemo vsoto ostalih elementov. Reč se ustavi, ko je seznam prazen; vsota elementov v njem je 0.
def vsota(s):
if not s:
return 0
else:
return s[0] + vsota(s[1:])
Rokohitrsko, zavedajoč se, da je False isto kot 0:
def vsota(s):
return s != [] and s[0] + vsota(s[1:])
Seznam s vsebuje x, če ni prazen in je x prvi element ali pa ostanek vsebuje x. Reč se ustavi, ko je seznam prazen in Python ne gre preverjat ostanka pogoja.
def vsebuje(s, x):
return s != [] and (s[0] == x or vsebuje(s[1:], x))
Z največjo potenco pa opravimo, kot svetuje naloga: "največja potenca, ki gre v n, je dvakrat večja od največje potence, ki gre v n polovic (če uporabljamo celoštevilsko deljenje)." Reč se ustavi, če je n enak 1; tedaj je največja potenca enaka 1.
def najvecja_potenca(n):
if n == 1:
return 1
return 2 * najvecja_potenca(n // 2)
Dodatna naloga
razbij(n)vrne seznam potenc števila 2, ki se seštejejo v podano številon; vsaka potenca lahko nastopa le enkrat. Klicrazbij(22)vrne[16, 4, 2]. Števila naj bodo urejena v padajočem vrstnem redu. Nalogo reši tako, da najprej s prejšnjo funkcijo poiščeš največjo potenco. Nato poiščeš seznam, ki predstavlja razbitje ostanka, in mu (spredaj) dodaš to potenco.brez(s, x)prejme nek seznamsin vrne nov seznam, ki je enak podanemu seznamu, a brezx-ov. Klicbrez([5, 4, 1, 2, 1, 1, 3], 1)vrne[5, 4, 2, 3]. Te funkcije ni bilo v originalni nalogi Nim, a ti lahko pride prav pri naslednjem paketku nalog.
Rešitev
Spet naredimo natančno tako, kot pravi namig v nalogi: poiščemo največjo potenco (p), ki gre v n in potem razbijemo, kar ostane od n-ja. Končamo, ko je n nič in ni česa razbijati, tako da vrnemo prazen seznam.
def razbij(n):
if n == 0:
return []
p = najvecja_potenca(n)
return [p] + razbij(n - p)
Funkcijo brez bomo razpisali lepo počasi. Če je s prazen, vrnemo prazen seznam. Sicer pa imamo dve možnosti: če je prvi element ravno x, vrnemo ostanek brez x-a. Če je prvi elemement kaj drugega, pa vrnemo prvi element in ostanek brez x-a.
def brez(s, x):
if not s:
return []
prvi, ostali = s[0], s[1:]
if prvi == x:
return brez(ostali, x)
else:
return [prvi] + brez(ostali, x)
Čisto dodatne naloge, v vaše veselje
razlika(s, t)prejme dva seznama in vrne nov seznam, ki vsebuje elemente, ki se pojavijo v enem od podanih seznamov, ne pa v obeh. Klicrazlika([16, 4, 2], [4, 1])vrne[16, 2, 1](v tem ali poljubnem drugem vrstnem redu), saj so to števila, ki se pojavijo le v prvem ali v drugem seznamu. Vsak element seznamas(alit) se v tem seznamu pojavi le enkrat. Pri reševanju se ti morda (a ne nujno) splača uporabiti funkcijo, ki si jo sprogramiral prejle.razbij_seznam(s)prejme seznam števil in vrne seznam seznamov njihovih razbitij. Klicrazbij([22, 5, 13])vrne[[16, 4, 2], [4, 1], [8, 4, 1]].zdruzi_sezname(s)prejme seznam seznamov (takšen, kot ga vrača funkcijarazbij_seznam) in vrne vse elemente, ki se pojavijo lihokrat.
Rešitev
razlika je ena lepših. Rekurzija bo tekla po s-ju: v vsakem klicu se bomo znebili prvega elementa s-ja.
Ustavila se bo, ko je t prazen. V tem primeru je razlika med s in t očitno s.
Če s ni prazen, moramo ločiti dva primera. Če je prvi element t-ja v s-ju (uporabili bomo rekurzivno funkcijo vsebuje), bomo vrnili razliko med s-jem brez tega elementa (ker ga je potrebno odstraniti od tam) in t-jem brez prvega elementa.
Če prvega elementa t-ja ni v s-ju, pa mora rezultat vsebovati ta element in še razliko med s in ostankom t-ja.
def razlika(s, t):
if not t:
return s
prvi, ostali = t[0], t[1:]
if vsebuje(s, prvi):
return razlika(brez(s, prvi), ostali)
else:
return [prvi] + razlika(s, ostali)
Ostali dve čisto dodatni funkciji sta v primerjavi z njo trivialni.
def razbij_seznam(s):
if not s:
return []
return [razbij(s[0])] + razbij_seznam(s[1:])
def zdruzi_sezname(ss):
if not ss:
return []
return razlika(ss[0], zdruzi_sezname(ss[1:]))