Upload
ogrdomat
View
37
Download
9
Embed Size (px)
DESCRIPTION
rekurzije
Citation preview
PROGRAMIRANJE 2
LABORATORIJSKE VJEŽBE
LV9 Rekurzije i pokazivači na funkcije
Elektrotehnički fakultet Osijek
Kneza Trpimira 2b
www.etfos.unios.hr
1 UVOD
Do sada smo obradili razne mogućnosti prilikom definiranja funkcija, objasnili smo razne
tipove varijabli koje mogu biti predani kao parametri funkcije te isto tako različite tipove
varijabli koji mogu biti postavljeni za povratnu vrijednost funkcije.
Danas ćemo se pozabaviti rekurzijama te ćemo pogledati kako izgledaju funkcije koje za
parametar imaju poziv druge funkcije.
2 REKURZIJE
Kada funkcija unutar svoje definicije poziva samu sebe, takva se funkcija naziva rekurzija.
Pogledajmo vrlo jednostavan primjer :
#include <stdio.h>
void func(void){
printf("\n Ovo je rekurzija \n");
func(); //funkcija poziva samu sebe
return;
}
int main(void){
func();
return 0;
}
U gornjem kodu, možemo vidjeti da funkcija func(), u svojoj definiciji, poziva samu sebe.
Upravo iz tog razloga je ova funkcija rekurzija. Nakon što bismo pokrenuli ovaj program,
dobili bismo sljedeći ispis na ekran:
Ovo je rekurzija
Ovo je rekurzija
.... ....
U gornjem primjeru vidimo da se funkcija vrti beskonačno, odnosno dok se program ne
sruši. Razlog tome je što se za svaki novi poziv funkcije kreira novi funkcijski stog, a kako se
funkcija konstantno poziva, tako se i konstantno kreiraju funkcijski stogovi. Na kraju se
prepuni stog (eng. stack overflow) i program se sruši.
Pozivom funkcije formiraju se unutar nje lokalne varijable koje su nedohvatljive izvan same
funkcije. Neovisnost lokalnih varijabli od okolnog programa omogućava da funkcija poziva
samu sebe - ponovnim pozivom se stvaraju nove lokalne varijable potpuno neovisne o
varijablama u pozivajućoj funkciji. Razmotrimo rekurziju na sljedećem jednostavnom
primjeru funkcije za računanje faktorijela:
int faktorijel(int n) {
return (n > 1) ? (n * faktorijel (n - 1)> : 1;
}
Pozovemo li tu funkciju iz main funkcije naredbom
int nf = faktorijel(3) ;
funkcija faktorijel() će pozivati samu sebe u tri "razine":
1. Prilikom prvog poziva formalni argument će biti inicijaliziran na vrijednost n = 3. U
uvjetnom izrazu će funkcija faktorijel() biti pozvana po drugi put, ali sada sa
stvarnim argumentom 2.
2. U drugom pozivu formalni argument se inicijalizira na n = 2. Argument pozivajuće
funkcije (n = 3) još uvijek postoji, ali je on nedohvatljiv i potpuno neovisan od
argumenta drugopozvane funkcije. U uvjetnom izrazu će funkcija faktorijel() biti
treći puta pozvana, ali ovaj puta sa stvarnim argumentom 1.
3. U trećem pozivu funkcije faktorijel() formalni argument se inicijalizira na
vrijednost 1 (u pozivajućim funkcijama formalni argumenti još uvijek postoje s
neizmijenjenim vrijednostima). Budući da nije zadovoljen uvjet u uvjetnom izrazu,
funkcija faktorijel() se više ne poziva, već se kao povratna vrijednost pozivnoj
funkciji šalje broj 1.
Slijedi postepeni povratak do početnog poziva funkcije:
1. Vrijednost 1 koju je vratila trećepozvana funkcija množi se s vrijednošću formalnog
argumenta u drugopozvanoj funkciji (n = 2), te se taj umnožak vraća kao rezultat
prvopozvanoj funkciji.
2. Vrijednost 2 koju je vratila drugopozvana funkcija množi se s vrijednošću formalnog
argumenta u početnoj funkciji (n = 3), te se taj umnožak vraća kao rezultat kodu
koji je prvi pozvao funkciju faktorijel() s argumentom 3.
Izvođenje koda može se simbolički prikazati slikom 1.
Slika 1. Primjer rekurzije
Mnoge matematičke formule se mogu implementirati primjenom rekurzije. Međutim, kod
primjene rekurzija valja biti krajnje oprezan: lokalne varijable unutar funkcija i adrese s
kojih se obavlja poziv funkcije se pohranjuju na stog, pa preveliki broj uzastopnih poziva
funkcije unutar funkcije može vrlo brzo prepuniti stog i onemogućiti daljnje izvođenje
programa. Daleko je sigurnije (i nerijetko efikasnije) takve formule implementirati
jednostavnim petljama. Rekurzije mogu biti vrlo efikasno sredstvo za rukovanje posebnim
strukturama podataka.
Na primjer, računanje faktorijela se može riješiti i bez rekurzije na sljedeći način, što je
efikasnije jer trebamo samo jedan poziv funkcije:
long faktorijeli(long n) {
long f=1;
for(;n>1;n--) f*=n;
return f;
}
0. p
ozi
v
faktorijel(3);
1. p
ozi
v
n = 3;
faktorijel(n-1);
2. p
ozi
v
n = 2;
faktorijel(n-1);
3. p
ozi
v
n = 1;
return 1;
nf=faktorijel(3); return
n*faktorijel(n-1);
return
n*faktorijel(n-1);
Pogledajmo još jedan primjer rekurzivne funkcije:
#include <stdio.h>
void unos(void);
int main(void){
printf("\n Unesite niz znakova: ");
unos();
return 0;
}
void unos(void){
char znak;
if((znak=getchar())!=’\n’) unos();
putchar(znak);
}
Funkcija učitava znakove sa standardnog ulaza sve dok ne naiđe do prijelaza u novu liniju i
ispisuje učitane znakove. Ako izvršimo program
Unesite niz znakova: Bok!
!koB
Prvo je ispisan znak za prijelaz u novi red, a zatim niz Bok! u obrnutom redoslijedu.
Svaki poziv funkcije unos učita jedan znak sa standardnog ulaza i spremi ga u lokalnu
varijablu znak. Ukoliko učitani znak nije znak prijelaza u novi red, ponovo se poziva funkcija
unos.
Svaka varijabla definirana unutar funkcije vidljiva je samo u toj funkciji. Ona se kreira kod
poziva funkcije i nestaje nakon izlaza iz funkcije. Stoga pri svakom novom pozivu funkcije
unos kreira se nova varijabla s imenom znak. Budući da varijabla definirana u funkciji
(unutarnja varijabla) nije vidljiva izvan te funkcije, prisutnost više različitih varijabli s
imenom znak je dozvoljeno. Dakle, pri svakom novom pozivu funkcije unos bit će pročitan i
pohranjen jedan znak sve dok ne naiđemo do znaka za prijelaz u novi red. Tada pozvana
funkcija ispiše znak za prijelaz u novi red i izađe. Funkcija koja ju je pozvala ispisat će znak
koji je pročitala (znak ’!’) i predaje kontrolu funkciji koja ju je pozvala. Na taj se način svi
upisani znakovi ispisuju u obrnutom redoslijedu.
U ovom jednostavnom primjeru vidi se drugi nedostatak rekurzivnog programa. Pri svakom
novom rekurzivnom pozivu funkcije kreiraju se ponovo sve lokalne varijable. U gornjem
primjeru koristili smo onoliko varijabli koliko je bilo upisanih znakova, mada bi jedna bila
sasvim dovoljna.
Rekurzija se najčešće koristi za povezane liste, binarna stabla, ispitivanje mogućih scenarija
u igrama poput šaha. Naučili smo da se rekurzija uvijek sastoji od dva dijela: uvjet završetka
rekurzije i dio u kojem funkcija poziva samu sebe pri čemu parametri koji se predaju vode
ka završetku rekurzije.
3 POKAZIVAČI NA FUNKCIJE
Do sada smo naučili koristiti pokazivače na razne tipove podataka, preostalo nam je naučiti
kako pokazivači mogu biti usmjereni na funkcije. Funkcija također ima svoju početnu
memorijsku adresu gdje počinje njena izvedba te ćemo tu adresu predati pokazivaču. No,
ako će se funkcija pozivat preko pokazivača, moraju joj biti dostupne informacije o broju i
tipu argumenata koji se trebaju predati te tipu povrate vrijednosti koji se očekuje. Baš radi
ovoga, pokazivač na funkciju je malo kompleksnije deklarirati od ostalih pokazivača.
3.1 DEKLARACIJA POKAZIVAČA NA FUNKCIJU
Pogledajmo deklaraciju jednostavne funkcije:
int (*pfunction) (int);
Na ovaj način smo deklarirali pokazivač imena pfunction koji pokazuje na funkciju koja
prima jedan parametar tipa int i vraća vrijednost tipa int pozivajućem programu. Ovaj
pokazivač još ne pokazuje na neku funkciju, no kada ga „usmjerimo“ na konkretnu funkciju,
ta funkcija će morati imati upravo jedan argument tipa int i povratnu vrijednost isto tipa
int. Ako se izostave oble zagrade (prioritet operatora), imali bismo deklaraciju na funkciju
pfunction koja vraća vrijednost koja je pokazivač na tip int.
3.2 POZIVANJE FUNKCIJE PREKO POKAZIVAČA NA FUNKCIJU
Pretpostavimo funkciju koja ima sljedeći prototip:
int sum(int a, int b); /* racuna a+b */
Ova funkcija ima dva parametra tipa int te vraća rezultat koji je također tipa int. Možemo
deklarirati pokazivač na funkciju kojem ćemo predati adresu funkcije sum na sljedeći način:
int (*pfunction) (int);
pokazivač može
biti dodijeljen
samo
funkcijama koje
imaju ovaj
povratni tip
podatka
ime pokazivača
na funkciju
pokazivač može
biti dodijeljen
samo funkcijama
koje imaju ove
tipove
argumenata
int (*pfun)(int, int) = sum;
Sada smo deklarirali pokazivač na funkciju sa imenom pfun kojem smo predali adresu
funkcije sum() koja ima dva parametra tipa int i vraća vrijednost int.
Sada možemo pozvati funkciju sum() preko pokazivača:
int result = pfun(45, 55);
U gornjoj naredbi smo pozvali funkciju sum() preko pokazivača pfun sa vrijednostima
argumenata 45 i 55. Vrijednost koju će vratiti funkcija sum() će se spremiti u varijablu
result.
Možemo primijetiti da smo funkciju preko pokazivača pozvali na isti način kao kod
direktnog poziva funkcije, nije bilo potrebno koristiti operator dereferenciranja.
Pretpostavimo da imamo još jednu funkciju sljedećeg prototipa:
int product(int a, int b); /* racuna a*b */
Pokazivač možemo sada usmjeriti na funkciju product jer ima jednaki broj argumenata istog
tipa kao i funkcija sum(), te je i povratni tip isto int:
pfun = product;
3.3 POLJA POKAZIVAČA NA FUNKCIJE
Pokazivač na funkciju je varijabla kao bilo koja druga te stoga možemo kreirati i polje
pokazivača na funkcije. Na sljedećem primjeru smo pokazali kako možemo deklarirati polje
pokazivača na funkcije:
int (*pfunctions[10]) (int, int);
Dakle, deklarirali smo polje, pfunctions, od 10 elemenata gdje svaki element može spremiti
adresu funkcija koja ima povratni tip int te prima dva parametra tipa int.
#include <stdio.h>
/* Prototip funkcija */
int sum(int, int);
int product(int, int);
int difference(int, int);
int main(void){
int a = 10;
int b = 5;
int result = 0;
int (*pfun[3])(int, int);
/* Inicijalizacija pokazivaca */
pfun[0] = sum;
pfun[1] = product;
pfun[2] = difference;
/* Izvrsi svaku funkciju */
for(int i = 0 ; i < 3 ; i++){
result = pfun[i](a, b); /* poziv funkcije preko pokazivaca */
printf("\nresult = %d", result);
}
/* poziv sve tri funkcije preko pokazivaca */
result = pfun[1](pfun[0](a, b), pfun[2](a, b));
printf("\n\nProdukt sume i razlike = %d\n",result);
return 0;
}
int sum(int x, int y){
return x + y;
}
int product(int x, int y){
return x * y;
}
int difference(int x, int y){
return x - y;
}
3.4 POKAZIVAČI NA FUNKCIJE KAO ARGUMENTI FUNKCIJE
Moguće je predati pokazivač na funkciju kao argument neke funkcije. To nam omogućava
poziv raznih funkcija, ovisno o adresi koja se preda pokazivaču na funkciju. Primjer je dan
niže.
#include <stdio.h>
int sum(int,int);
int product(int,int);
int difference(int,int);
int any_function(int(*pfun)(int, int), int x, int y);
int main(void){
int a = 10;
int b = 5;
int result = 0;
int (*pf)(int, int) = sum;
result = any_function(pf, a, b);
printf("\nresult = %d", result );
result = any_function(product,a, b);
printf("\nresult = %d", result );
printf("\nresult = %d\n", any_function(difference, a, b));
return 0;
}
int any_function(int(*pfun)(int, int), int x, int y){
return pfun(x, y);
}
int sum(int x, int y){
return x + y;
}
int product(int x, int y){
return x * y;
}
int difference(int x, int y){
return x - y;
}
4 RIJEŠENI PRIMJERI
Primjer 1.
Napisati C program koji će omogućiti zbrajanje brojeva na sljedeći način:
0 + 1 = 1
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
15 + 6 = 21
21 + 7 =28
28 + 8 = 36
36 + 9 = 45
Početne vrijednosti za zbrajanje su 0 i 1, potrebno je ispisati konačnu sumu.
#include <stdio.h>
int count = 1;
void func(int sum){
sum = sum + count;
count ++;
if(count <= 9){
func(sum);
}
else{
printf("\nSuma je [%d] \n", sum);
}
return;
}
int main(void)
{
int sum = 0;
func(sum);
return 0;
}
Kratko obrazloženje [Primjer 1]: Pozivamo funkciju func kojoj predajemo početnu
vrijednost 0. Funkcija func će pozivati samu sebe dokle god je varijabla count manja ili
jednaka 9 (početna vrijednost varijable count je 1) i zbrajati u varijablu sum sa uvećanom
vrijednošću varijable count. Kada se varijabla count poveća na 10, ispisuje se konačni
rezultat koji je spremljen u varijabli sum i iznosi 45.
Primjer 2.
Napisati C program koji će u funkciji pomoću rekurzivnog zbrajanja izvesti operaciju množenja
dva broja.
#include <stdio.h>
unsigned int multiply(unsigned int x, unsigned int y){
if (x == 1){
/* Kraj rekurzije */
return y;
}
else if (x > 1){
/* Korak rekurzije */
return y + multiply(x-1, y);
}
/* Ukoliko je x = 0 */
return 0;
}
int main() {
printf("3 puta 5 je %d", multiply(3, 5));
return 0;
}
Kratko obrazloženje [Primjer 2]: Funkcija multiply ima 3 uvjeta:
ako je prvi predani argument funkcije, x, jednak 1, funkcija vraća drugi argument y
(npr. 1*3=3)
ako je prvi argument veći od 1, funkcija poziva samu sebe te umanjuje prvi
argument za 1 uz sumiranje sa drugim argumentom (npr. 2*3 -> 3 + multiply(1,3)= 3)
ako je prvi argument funkcije 0, povratna vrijednost je 0
Primjer 3.
Napisati C program koji će za uneseni broj ispisati Fibonacci niz. Za ispis Fibonacci niza koristiti
rekurziju.
#include<stdio.h>
void printFibonacci(int);
int main(){
int k,n;
long int i=0,j=1,f;
printf("Unesite raspon Fibonacci reda: ");
scanf("%d",&n);
printf("Fibonacci red: ");
printf("%d %d ",0,1);
printFibonacci(n);
return 0;
}
void printFibonacci(int n){
static long int first=0,second=1,sum;
if(n>0){
sum = first + second;
first = second;
second = sum;
printf("%ld ",sum);
printFibonacci(n-1);
}
}
Kratko obrazloženje [Primjer 3]: Kod Fibonaccijevog reda svaki broj je zbroj dvaju
prethodnih brojeva. U funkciji printFibonacci, varijabla first se postavlja na 0, a second na 1,
što su početne vrijednosti F. reda. Nakon toga se radi suma prva dva broja te se ta suma
predaje varijabli second, dok se varijabla first postavi na vrijednost second. Funkcija poziva
samu sebe sa argumentom umanjenim za 1 sve dok je argument veći od 0.
5 ZADACI
Pri rješavanju zadataka potrebno je ispisati rezultate u točno određenom formatu. Prvi
zadatak je potrebno predati preko aktivnosti na Loomenu u koju će Vas uputiti nastavnici
na vježbama, a drugi trebate pokazati nastavniku na satu.
1. Rekurzija prima jednu cjelobrojnu vrijednost koja je visina piramide. U datoteku
piramida, koju ćete kreirati iz programa, „nacrtati“ piramidu od znakova '*' koja
može imati maksimalnu visinu 30 redova. U main() dijelu učitati visinu piramide te
pozvati rekurziju. Primjer piramide je prikazan na slici niže.
2. Napisati C program koji će učitati cijeli broj te u datoteku naziva suma.txt upisati
sumu njegovih znamenaka. Za traženje sume znamenaka broja korijjstiti rekurziju.
NAPOMENA: U ovom slučaju nije potrebno ispisivati na ekran rezultate, VPL aktivnost
direktno provjerava zapisane datoteke.