Rim (nadaljevanje naloge Nim)
Rekurzivni nim - Rim
Napisali boste nekaj funkcij, ki ste jih pisali za nalogo Nim - le da bodo tokrat rekurzivne.
Testi
Testi: testi-rim.py
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 seznams
vsebuje elementx
inFalse
, če ga ne. Stvar je preprosta: prazen seznam očitno ne vsebujex
. Sicer pa preverimo, ali je prvi element enakx
, ali pa jex
vsebovan 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 jen
pozitivno celo število.Kako to narediti rekurzivno? Preprosto: največja potenca, ki gre v
n
, je dvakrat večja od največje potence, ki gre vn
polovic (č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 seznams
in 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:]))