• Nenhum resultado encontrado

Índice. Capítulo 1 - Introdução

N/A
N/A
Protected

Academic year: 2021

Share "Índice. Capítulo 1 - Introdução"

Copied!
25
0
0

Texto

(1)

Índice i

Índice

Capítulo 1 - Introdução

1. Estruturas na linguagem C ... 1

1.1. A necessidade de utilizar Estruturas ... 1

1.2. Definição de estrutura ... 1

1.3. Como aceder aos campos de uma estrutura... 2

1.4. Iniciar uma Estrutura ... 3

1.5. Atribuição de estruturas ... 3

2. Ponteiros/apontadores em C ... 4

2.1. Variáveis estáticas e variáveis dinâmicas ... 4

2.2. Ponteiros ... 4

2.3. Ponteiros para estruturas ... 5

2.4. Diagramas ... 5

2.5. Notação na linguagem C ... 6

2.6. Ponteiro para um tipo de dados ... 6

2.7. Criar e destruir variáveis dinâmicas ... 6

2.8. Ponteiros NULL ... 7

2.9. Apontador e apontado ... 8

(2)

3. Memória dinâmica ... 9

3.1. Memória estática ... 9

3.2. Memória dinâmica ... 11

3.2.1. A função sizeof e o operador cast ... 12

3.2.2. A função calloc ... 12 3.2.3. A função malloc ... 13 3.2.4. A função realloc ... 14 3.2.5. A função free ... 14 3.3. Exemplo ... 14 4. Algoritmos recursivos ... 20 4.1. Objetivo ... 20

4.2. Estratégia (para a construção de soluções recursivas) ... 21

4.3. Exercícios ... 22

5. Estruturas de Dados ... 22

5.1. Definição ... 22

(3)

Estruturas na linguagem C 1

Capítulo 1 – Introdução

1.

Estruturas na linguagem C

1.1.

A necessidade de utilizar Estruturas

A estrutura é o conceito mais poderoso nas estruturas de dados e na linguagem C. Quando se pretende organizar um conjunto de dados do mesmo tipo, utiliza-se um “array”. No entanto, quando os dados não são do mesmo tipo e se pretende organizar os dados numa cadeia de elementos, isso torna-se mais complexo e muito ineficiente.

Por exemplo, num Stand de automóveis usados, cada automóvel tem associado um conjunto de informação, tal como: modelo, nome do fabricante, ano de fabrico, nome e telefone do proprietário. Neste caso, pode-se organizar cada um destes tipos de informação utilizando um “array”, o que torna o problema difícil, ineficiente e complexo de tratar.

Em resumo, há a necessidade de utilizar mais conveniente, elegante, eficiente e coletivamente, os dados de diferentes tipos como um grupo. Isto é o objetivo que se pretende atingir com o mecanismo da estrutura. As estruturas são muito úteis, não só porque contém diferentes tipos de dados, mas também porque podem formar muitas estruturas de dados complexas, tais como listas ligadas, árvores, gráficos e bases de dados.

1.2.

Definição de estrutura

Na linguagem C, uma estrutura é um conjunto de variáveis referenciadas por um nome, fornecendo uma maneira conveniente de se ter informações relacionadas e agrupadas. Uma definição de estrutura forma um modelo que pode ser utilizado para criar variáveis de estruturas. As variáveis que constituem a estrutura designam-se por campos (ou elementos) da estrutura.

(4)

A forma geral de uma definição de estrutura é a seguinte: struct nome_estrutura { tipo_1 campo_1; tipo_2 campo_2; ... tipo_N campo_N; } variáveis_estrutura;

onde nome_estrutura e variáveis_estrutura podem ser omitidos, mas não ambos. Normalmente omite-se a declaração de variáveis_estrutura.

Uma outra maneira de criar uma estrutura é definindo-a como um tipo (utilizando

typedef), da seguinte forma:

typedef struct { tipo_1 campo_1; tipo_2 campo_2; ... tipo_N campo_N; } nome_tipo_estrutura;

A partir de uma definição de tipo de estrutura, esta pode ser utilizada tal como os tipos habituais (int, float, char, …).

A linguagem C permite definir explicitamente novos nomes aos tipos de dados, utilizando a palavra-chave typedef. A forma geral de um comando typedef é o seguinte:

typedef tipo nome;

onde tipo é qualquer tipo de dados permitido e nome é o novo nome para esse tipo.

1.3.

Como aceder aos campos de uma estrutura

O acesso a cada uma dos campos duma estrutura é feito através da combinação do nome duma variável do tipo estrutura e do campo que se pretende aceder, separados por um ponto (.). Isto é,

variável_estrutura

.

campo

Se um campo representa um elemento estruturado (“array”), então os elementos desse campo podem ser acedidos incluindo os índices na designação de campo. Por exemplo, se um campo representa um “array” de uma dimensão (vetor), um seu elemento pode ser acedido pela expressão

(5)

Estruturas na linguagem C 3

Se, por exemplo, um campo representa um “array” de duas dimensões (matriz), um seu elemento pode ser acedido pela expressão

variável_estrutura

.

campo[linha][coluna]

Identicamente, se um campo representa uma estrutura, um elemento dessa estrutura pode ser acedido pela expressão

variável_estrutura

.

campo

.

subcampo

onde subcampo refere-se a um campo dentro dessa estrutura.

Se definir-se uma tabela unidimensional (vetor) cujos elementos sejam estruturas, a acesso a uma dessas estruturas é feito através da seguinte forma:

tabela[índice]

e o acesso a um determinado campo é feito da seguinte forma:

tabela[índice]

.

campo

Nota: Os campos de uma estrutura podem ser utilizados da mesma forma que as variáveis normais. As características particulares que se aplicam a cada campo são determinadas pelo seu tipo.

1.4.

Iniciar uma Estrutura

Iniciar uma estrutura implica atribuir valores iniciais aos campos da estrutura. No entanto, deve ter-se presente os tipos e a ordem dos campos pela qual estão declarados na estrutura. Desta forma, os tipos e os valores terão que se assemelhar.

Exemplo: typedef struct { int N; float X; } Registo; Registo A = { 25, 2.7 }; Iniciará implicitamente A.N = 25 e A.X = 2.7

1.5.

Atribuição de estruturas

A atribuição de estruturas é uma característica importante da linguagem C. É possível atribuir o valor de uma variável do tipo estrutura a uma outra variável do tipo estrutura, desde que ambas sejam do mesmo tipo de estrutura. Isto na realidade atribui os valores

(6)

dos campos de uma variável do tipo estrutura aos correspondentes campos da outra variável do tipo estrutura.

Exemplo:

Registo A = { 25, 2.7 }, B = A; Produz o seguinte:

B.N = 25 e B.X = 2.7

2.

Ponteiros/apontadores em C

2.1.

Variáveis estáticas e variáveis dinâmicas

Podem ser usadas duas variedades de variáveis durante a execução de um programa em linguagem C: variáveis estáticas e variáveis dinâmicas.

As variáveis estáticas são declaradas durante a escrita do programa. O espaço para elas existe enquanto o programa em que são declaradas estiver a ser executado.

As variáveis dinâmicas são criadas (e destruídas) durante a execução do programa. Como só existem enquanto o programa estiver a ser executado, não se lhes pode atribuir um nome durante a escrita do programa. A única forma de referenciar uma variável dinâmica é usar um ponteiro.

Após ser criada, a variável dinâmica pode conter dados e possuir um tipo tal como qualquer outra variável. Pode-se então falar em criar uma variável dinâmica do tipo x e estabelecer um ponteiro que aponta para ela, ou em mover um ponteiro de uma variável dinâmica do tipo x para outra (do mesmo tipo) ou em devolver ao sistema o espaço ocupado por uma variável dinâmica.

As variáveis estáticas não podem ser criadas ou destruídas durante a execução do programa e os ponteiros não podem ser usados para apontar para variáveis estáticas. As variáveis estáticas são referenciadas usando o seu nome.

2.2.

Ponteiros

Um ponteiro (pointer) também designado por link ou reference é uma variável que indica a localização de outra variável (normalmente uma estrutura contendo dados). Um ponteiro é uma variável cujo valor é um endereço de uma variável dinâmica de um determinado tipo. Um ponteiro contém uma referência para o endereço de uma célula de memória que contém um elemento.

(7)

Ponteiros/apontadores em C 5

Se for usado um ponteiro para localizar uma estrutura então não é preciso estar-se preocupado onde este está atualmente armazenado, pois usando o ponteiro, o sistema computacional pode localizar a estrutura quando for necessário.

2.3.

Ponteiros para estruturas

A linguagem C permite ponteiros para estruturas exatamente como o permite para outros tipos de variáveis. Tal como nos casos comuns, declara-se um ponteiro para uma estrutura colocando um asterisco (*) antes do nome da variável de estrutura e depois do nome desta, da seguinte forma:

struct nome_estrutura *ponteiro;

Para se aceder a um campo da variável apontada pelo apontador, faz-se o seguinte:

(*ponteiro)

.

campo ou ponteiro

campo

2.4.

Diagramas

Os ponteiros são geralmente desenhados como setas e as estruturas como retângulos.

r • ABEL s • • RUI NULL t • u • ANA EMA v •

Figura 1 - Ponteiros para estruturas.

No diagrama da Figura 1, r é um ponteiro para o registo “ABEL” e v é um ponteiro para o registo “EMA”. Como se pode observar o uso de ponteiros é muito flexível. Dois ponteiros podem referenciar o mesmo registo, como t e u, ou um ponteiro pode mesmo não referenciar registo algum. Esta última situação é representada pelo símbolo NULL como é mostrado para o ponteiro s. Deve-se ter muito cuidado na manipulação de

(8)

ponteiros para não se perder nenhum registo. Na figura o registo “RUI” foi perdido, sem nenhum ponteiro a referenciá-lo, não havendo portanto nenhuma forma de o encontrar.

2.5.

Notação na linguagem C

Se Nodo denotar o tipo dos itens em que se está interessado, então pode-se declarar um tipo de ponteiro para objetos do tipo Nodo com a declaração seguinte:

typedef Nodo *PNodo;

o que significa que uma variável do tipo PNodo é um ponteiro para uma variável do tipo Nodo. O tipo Nodo que o ponteiro refere pode ser arbitrário mas, em muitas aplicações, é uma estrutura. Tal como para qualquer outro tipo de dados podem ser declaradas variáveis do tipo PNodo. Estas variáveis apontam para variáveis dinâmicas do tipo Nodo.

2.6.

Ponteiro para um tipo de dados

Cada ponteiro está limitado ao tipo de variável para a qual ele aponta. O mesmo ponteiro nunca pode ser usado para apontar para variáveis de tipos diferentes. As variáveis ponteiro de tipos diferentes não podem ser misturadas umas com as outras.

A linguagem C permite atribuições entre duas variáveis ponteiro apenas do mesmo tipo, mas não entre de tipos diferentes. Por exemplo, para as seguintes declarações:

Nodo *P, *Q; int *A, *B;

as atribuições P = Q e A = B são corretas, mas a atribuição P = A não é correta.

2.7.

Criar e destruir variáveis dinâmicas

A criação e destruição de variáveis dinâmicas são feitas usando funções padrão. Se P for uma variável declarada como um ponteiro para o tipo Nodo (P é uma variável do tipo

PNodo), então a função:

new(P);

cria uma nova variável dinâmica do tipo Nodo e atribui a sua localização ao ponteiro P (isto é, coloca em P o endereço dessa variável).

De modo similar: free(P);

devolve o espaço usado pela variável ao sistema. Alguns sistemas perdem o espaço e nunca mais o reutilizam, o que significa que existe um mau entendimento entre estas instruções e o Sistema Operativo.

(9)

Ponteiros/apontadores em C 7 NULL P = NULL ; P • new(P) ; P • P = ‘ANA’ ; P • ANA free(P) ; P • ??? ANA

Figura 2 - Criar e destruir variáveis dinâmicas.

Depois da função free(P) ser invocado, a variável ponteiro P fica indefinida, donde não pode ser usada (legalmente) até lhe ser atribuído um novo valor. Estas ações estão ilustradas na Figura 2.

2.8.

Ponteiros NULL

Em certas situações pretende-se que um ponteiro não referencie qualquer variável dinâmica. Esta situação pode ser estabelecida pela atribuição:

P = NULL;

Depois pode ser efetuado um teste ao seu estado: if (P != NULL)

A palavra NULL é uma palavra reservada na linguagem C, sendo usada como uma constante para os dados do tipo ponteiro.

Note-se a distinção entre uma variável ponteiro cujo valor é indefinido e uma variável ponteiro cujo valor é NULL: a asserção P = NULL significa que P atualmente não aponta para nenhuma variável dinâmica. Se o valor de P é indefinido então P pode apontar para qualquer posição aleatória na memória.

Tal como para todas as outras variáveis, quando começa a execução do programa, o valor das variáveis ponteiro é indefinido. Antes de P poder ser usado é necessária uma chamada a new(P) ou uma atribuição tal como P = Q ou P = NULL. Depois de uma chamada a free(P), o valor de P fica indefinido, donde deve-se fazer imediatamente P = NULL para se ter a certeza que P não é usado com um valor indefinido.

(10)

2.9.

Apontador e apontado

A notação *P denota a variável para a qual P aponta. Esta notação pode parecer um pouco confusa, mas a sua lógica torna-se clara se tiver em mente que * significa "aponta". Donde a declaração:

Nodo *P;

indica que P aponta para um elemento do tipo Nodo e *P é o elemento apontado por P.

2.10.

Restrições ao uso de variáveis ponteiro

O único uso de variáveis do tipo PNodo é para encontrar a localização de variáveis do tipo Nodo. As variáveis ponteiro podem participar em instruções de atribuição, podem ser testadas por igualdade e podem aparecer (como parâmetros) em chamadas de rotinas/subprogramas, mas não podem aparecer em qualquer outro lugar.

Note que as restrições no uso de ponteiros não se aplicam às variáveis dinâmicas que eles referem. Se P é um ponteiro então *P não é normalmente um ponteiro (contudo é legal um ponteiro apontar para outro ponteiro), mas sim uma variável do tipo Nodo e portanto *Ppode ser usado em qualquer utilização legítima para o tipo Nodo.

P = ‘ANA’ P • ANA *P Q = ‘RUI’ Q • RUI *Q _____________________________________________ P • ANA *P P = Q Q • RUI *Q _____________________________________________ P • RUI *P *P = *Q Q • RUI *Q

Figura 3 - Atribuições de variáveis ponteiro.

No que respeita a instruções de atribuição é importante lembrar a diferença entre P = Q e *P = *Q. Embora ambas sejam corretas (desde que P e Q apontem para o mesmo tipo

(11)

Memória dinâmica 9

de dados) possuem significados diferentes. A primeira refere-se aos ponteiros e a segunda aos conteúdos apontados pelos ponteiros. A Figura 3 ilustra estas atribuições.

A primeira instrução faz com que P aponte para o mesmo objeto que o ponteiro Q mas não altera o valor de nenhum dos objetos apontados. O objeto apontado por P é perdido (a não ser que haja alguma outra variável ponteiro que ainda o referencie).

A segunda instrução, *P = *Q, copia o valor do objeto *Q para o objeto *P, donde os

dois objetos ficam com o mesmo valor, com P e Q a apontarem para as duas cópias separadamente.

Finalmente, as instruções de atribuição P = *Q e *P = Q envolvem tipos de dados diferentes, donde ambas são ilegais (exceto se tanto P como Q serem ponteiros para ponteiros do mesmo tipo).

3.

Memória dinâmica

3.1.

Memória estática

As variáveis locais às funções são sucessivamente criadas e libertadas numa zona de memória do processo designada pilha (stack). Quando uma função é chamada, a zona da pilha cresce para criar local para as novas variáveis locais, quando uma função retorna a zona da pilha decresce na mesma proporção. A declaração de variáveis em funções leva a que sejam reservados vários endereços de memória na zona da pilha (variáveis locais), as quais são libertadas logo após o término função onde estão declaradas.

Considere-se o seguinte exemplo: void LerVetor () { int V[3000], N; do { printf (“N = ?”); scanf (“%d”, &N); } while ((N < 0) || (N > 3000); for (i = 0; i < N, i++) {

printf (“Insira um inteiro: “); scanf (“%d”, &V[i]);

} }

(12)

#include <stdio.h> Int main() {

LerVetor(); return 1; }

A declaração de variáveis que consta na função LerVetor anterior (int V[3000], N;) leva a que seja reservada vários endereços de memória na zona da pilha. Para a variável N é escolhido um local (endereço) qualquer na pilha, enquanto os elementos do vetor V são reservados um conjunto (bloco) de 3000 endereços contíguos (zona preenchida na figura que se segue) e um endereço ao qual é atribuído o endereço do primeiro elemento de V, V[0].

Com a execução da instrução "do … while" da função, suponha-se que é introduzido, por exemplo, o valor 2000 para a variável N; isto significa que dos 3000 elementos do vetor V reservados apenas 2000 serão usados (pois N serve como tamanho real do vetor V). Com a execução da instrução for da função irão ser atribuídos valores para os primeiros 2000 elementos do vetor V que, suponha-se serem os apresentados na figura seguinte. Desta forma, existe um conjunto de endereços aos quais não foram atribuídos valores, mas que se encontram reservados (desperdício de memória).

(13)

Memória dinâmica 11

Com a execução do programa principal (main), quando a função devolve o controlo para o programa todas as variáveis da função LerVetor são libertadas para o sistema operativo.

3.2.

Memória dinâmica

A linguagem C permite a criação dinâmica de memória à medida das necessidades do programa. A memória dinâmica é gerida numa zona especial da memória designada heap e é permanente, no sentido em que não depende da ativação/desativação de funções ou blocos de programa, podendo ser libertada pelo programa quando a sua utilização deixa de ser necessária.

A memória dinâmica pode evitar, por exemplo, o sobredimensionamento de vetores, permitindo a sua criação à medida das reais necessidades do programa.

Existem várias funções que são usadas para gerir a memória dinâmica: calloc, malloc,

realloc e free. Na utilização de algumas destas funções são utilizados com muita

(14)

3.2.1. A função sizeof e o operador cast

A função sizeof() devolve a dimensão do tipo especificado, geralmente em número de bytes; por exemplo, sizeof(int) = 2 significa que cada valor do tipo inteiro ocupa 2 bytes de memória.

Algumas funções devolvem um apontador genérico (formalmente, do tipo void*). A sua conversão para o tipo desejado efetua-se por meio de um operador de cast. Note-se que a operação de cast pode ser realizada entre tipos incompatíveis (por exemplo, inteiro e apontador, inteiro e real), mas o resultado pode ser dependente do processador.

3.2.2. A função calloc

A sintaxe da função calloc é a seguinte:

void *calloc (size_t nmemb, size_t size); a qual

- reserva um bloco de memória contígua com espaço suficiente para armazenar nmemb elementos de dimensão size cada;

- devolve o endereço (apontador) para a primeira posição do bloco ou NULL quando não for possível alocar memória;

- size_t é o tipo utilizado para especificar as dimensões numéricas em várias funções; - o tipo de retorno void * corresponde a um endereço genérico de memória (permite a

utilização por todo o tipo de ponteiro);

- todas as posições do bloco de memória são inicializadas com zero. Considere-se o seguinte exemplo:

float *p;

p = (float *) calloc (2000, sizeof (float)); o qual

- reserva de memória para um bloco de 2000 reais;

- a partir daqui, p pode ser tratado como um vetor de 2000 posições (para 2000 valores reais);

- p é um ponteiro para o primeiro elemento do vetor;

- sizeof() é um operador que devolve a dimensão (em geral, em bytes) do tipo ou variável indicado no argumento;

- (float *) funciona como um operador de cast (obriga a devolver um ponteiro para um real/float).

(15)

Memória dinâmica 13

A figura seguinte ilustra o que se passa ao nível da memória com a execução do exemplo anterior. A variável p é local (guarda o endereço do primeiro elemento do vetor p), logo a memória reservada para si encontra-se na zona da pilha. A memória reservada para os elementos do vetor p (p[0], p[1], ..., p[1999] ou *p, *(p+1), ..., *(p+1999)) são reservados na zona do heap, pois são variáveis dinâmicas.

3.2.3. A função malloc

A sintaxe da função malloc é a seguinte: void *malloc (size_t total_size); a qual

- reserva um bloco de memória contígua de dimensão total_size expressa em bytes; - devolve o endereço (ponteiro) para a primeira posição do bloco ou NULL quando não

for possível alocar memória;

- size_t é o tipo utilizado para especificar as dimensões numéricas em várias funções; - o tipo de retorno void * corresponde a um endereço genérico de memória (permite a

utilização por todo o tipo de ponteiro);

- calloc(n,d) pode ser simplesmente substituído por malloc(n*d); - as posições do bloco não são inicializadas com qualquer valor.

(16)

3.2.4. A função realloc

A sintaxe da função realloc é a seguinte:

void *realloc (void *ptr, size_t total_new_size); na qual

- ptr é o ponteiro para o bloco de memória reservado antes;

- total_new_size é a dimensão total que se pretende agora para o mesmo bloco; - retorna um apontador para o bloco de memória redimensionado;

- o segundo argumento (size_t total_new_size) tem um significado semelhante ao da função malloc (size_t total_size).

3.2.5. A função free

A sintaxe da função free é a seguinte: void *free (void *ptr);

na qual

- ptr é o ponteiro para o bloco de memória reservado antes, o qual foi devolvido por

malloc, calloc ou realloc.

3.3.

Exemplo

Considere-se o seguinte exemplo: int *p, N;

do {

printf (“Insira a dimensão do vector: “); scanf (“%d”, &N);

} while (N < 0);

p = (int *) malloc (N * sizeof (int)); for (i = 0; i < N; i++) {

printf (“Inserir um valor inteiro: “); scanf (“%d”, &p[i]);

}

p = (int *) realloc (p, (N+1000) * sizeof (int)); free (p);

Considere-se a análise, e ilustração através de esquemas, do resultado da execução de cada subconjunto de instruções do bloco anterior, que se segue.

(17)

Memória dinâmica 15

int *p, N;

do {

printf (“Insira a dimensão do vetor: “); scanf (“%d”, &N);

} while (N < 0); // por exemplo, N = 2000 p = (int *) malloc (N * sizeof (int));

(18)

for (i = 0; i < N; i++) {

printf(“Inserir um valor inteiro: “); scanf(“%d”, &p[i]);

}

p = (int *) realloc (p, (N+1000) * sizeof (int));

1º caso: a zona imediatamente a seguir ao último elemento de p está vazia e pode receber os 1000 elementos adicionais.

(19)

Memória dinâmica 17

2º caso: a zona imediatamente a seguir ao último elemento de p está ocupada e não pode receber os 1000 elementos adicionais.

2º caso (cont): como a zona imediatamente a seguir ao último elemento de p não pode receber os 1000 elementos adicionais, todo o vetor terá que ser realocado para uma zona que possa receber os 3000 elementos.

(20)

free (p);

O bloco de memória reservado para todo o vetor p é libertado.

Considere-se o mesmo exemplo nas versões com memória estática e memória dinâmica. #include <stdio.h> main () { int V[3000], N; do { printf (“N = ?”); scanf (“%d”, &N); } while ((N < 0) || (N > 3000); for (i = 0; i < N, i++) {

printf (“Insira um inteiro: “); scanf (“%d”, &V[i]); } } #include <stdio.h> #include <stdlib.h> int main () { int *V, N; do { printf (“N = ?”); scanf (“%d”, &N); } while (N < 0);

V = (int*) malloc (N*sizeof (int)); if (V == NULL)

return 1;

for (i = 0; i < N, i++) {

printf (“Insira um inteiro: “); scanf (“%d”, &V[i]);

}

free (V); }

(21)

Memória dinâmica 19

Quando se usa memória estática, é necessário reservar um grande bloco de memória, mesmo parte dela não ser necessária.

Quando se usa memória dinâmica, a memória apenas é reservada quando é necessária e na quantidade exata.

(22)

4.

Algoritmos recursivos

4.1.

Objetivo

Obter uma solução para um problema através da solução de outro com idêntica natureza, mas de menor dimensão. A dimensão do problema é sucessivamente reduzida até se atingir um caso especial cuja solução seja imediata. Este caso especial denomina-se

Caso Degenerado. Considere-se o seguinte exemplo:

Procurar (palavra, dicionário)

Se dicionário está na 1ª página Então Localizar palavra

Senão

Abrir próximo do meio

Determinar a metade que interessa Se palavra está na 1ª metade Então

Procurar (palavra, 1ª metade) Senão

Procurar (palavra, 2ª metade)

Na matemática, a função fatorial de um inteiro não negativo é usualmente definida pela fórmula seguinte:

n! = n x (n−1) x ... x 1

As reticências presentes nesta fórmula significam “continuar na mesma forma”. Para calcular o fatorial é necessária uma definição mais precisa, como a seguinte:

    > − × = = 0 n se )! 1 n ( n 0 n se 1 ! n Exemplo: 4! = 4 * 3! = 4 * (3 * 2!) = 4 * (3 * (2 * 1!)) = 4 * (3 * (2 * (1 * 0!))) = 4 * (3 * (2 * (1 * 1))) = 4 * (3 * (2 * 1)) = 4 * (3 * 2) = 4 * 6 = 24

(23)

Algoritmos recursivos 21

Estes cálculos ilustram a essência do modo como a recursão funciona. Um método geral para obter a resposta para um problema, é reduzir este a um ou mais problemas de natureza idêntica, mas de menor dimensão. O mesmo método é usado para estes subproblemas. A recursão continua até que a dimensão dos subproblemas seja reduzido um caso especial onde a solução seja obtida diretamente sem usar a recursão.

Por outras palavras, qualquer processo recursivo consiste de duas partes: 1. Um caso degenerado, que é tratado sem recursão;

2. Um método geral que reduz o problema a um ou mais problemas menores, fazendo com que o processo avance até atingir o caso degenerado.

Exemplo: Dado um inteiro positivo N, determinar o fatorial de N − N! = N x (N−1)! long fatorial (int N) {

if (N == 0) return 1;

return N * fatorial(N-1); }

Como se pode ver a partir deste exemplo, a definição recursiva e a solução recursiva podem ser ambas concisas e elegantes, mas os detalhes computacionais podem requerer muitos cálculos parciais antes do processo estar concluído.

É sempre importante validar as entradas. Mas deverá esse código ser incluído no corpo do subprograma, ou será melhor escrever um outro subprograma que valide as entradas e de seguida invocar o subprograma recursivo para fazer o trabalho útil?

4.2.

Estratégia (para a construção de soluções recursivas)

1. Definir o problema em termos de outro do mesmo tipo com menor dimensão. 2. Determinar a instância do problema que serve como caso degenerado/particular. 3. Estabelecer o modo como cada chamada recursiva diminui a dimensão do problema,

(24)

4.3.

Exercícios

1. Sucessão de Fibonacci: 1, 1, 2, 3. 5, 8, 13, 21, … f(n) = f(n-1) + f(n-2), n ≥ 3. int fibonacci (int N) {

if (N <= 2) return 1;

return fibonacci(N-1) + fibonacci(N-2); }

2. Determinar o máximo divisor comum entre dois números inteiros positivos. int mdc (int a, int b) {

if (b == 0) return a;

return mdc(b, a%b); }

3. Pesquisar um elemento num vetor, utilizando o algoritmo da Pesquisa Binária. int PesquisaBinaria (int Elem, int inf, int sup, int V[]) {

int k; if (sup < inf) return 0; k = (sup+inf-1)/2 + 1; if (Elem == V[k]) return k; if (Elem > V[k])

return PesquisaBinaria(Elem, k+1, sup, V); return PesquisaBinaria(Elem, inf, k-1, V);

}

5.

Estruturas de Dados

5.1.

Definição

Uma estrutura de dados é uma coleção de tipos de dados, composta por tipos não estruturados básicos, tipos estruturados ou uma mistura de ambos os tipos, e um conjunto de operações definidas sobre os tipos de dados.

(25)

Estruturas de Dados 23

Por outras palavras, uma estrutura de dados é composta por 3 partes: 1. Um conjunto de operações;

2. Uma estrutura de armazenamento especificando as classes de dados relacionados e as coleções de variáveis;

3. Um conjunto de algoritmos, uma para cada operação.

Cada algoritmo procura e modifica a estrutura de armazenamento para alcançar o resultado definido pela operação.

Um conjunto de variáveis inteiras e o conjunto de operações aritméticas simples (adição, subtração, multiplicação, divisão, negação, valor absoluto, etc.) sobre elas é um exemplo de uma estrutura de dados básica.

Existem 2 classes de estrutura de dados, que estão relacionadas com o tamanho do armazenamento exigido:

1. Estática (fixa): estrutura cujo tamanho e atribuição de memória associadas são fixadas aquando da compilação do programa;

2. Dinâmica: estrutura cujo tamanho aumenta ou diminuí consoante as necessidades durante a execução do programa, e em que a localização da memória a si associada poderá ser alterada.

5.2.

Estruturas Abstratas de Dados

Uma Estrutura Abstrata de Dados (EAD), muitas vezes também referida como tipo abstrato de dados (ADT - “Abstract Data Type”), é um conjunto de operações sobre uma coleção de dados armazenados. A definição funcional de uma EAD é independente da estrutura de dados escolhida para a sua representação. Ou seja, as operações a implementar devem ser aplicadas a qualquer tipo de estrutura de dados.

A utilização de EAD esconde os dados e dá ênfase às operações (ações) sobre os dados. A EAD esconde os dados como uma cápsula esconde o seu conteúdo. A EAD fornece ao utilizador interfaces através das funções que implementam as operações na EAD.

A representação interna dos dados, o armazenamento e a implementação das operações sobre os dados são escondidos dos utilizadores, os quais acedem e manipulam os dados. Por exemplo, nós usamos a EAD das operações aritméticas de inteiros +, −, *, etc. sem conhecer como o inteiro é representado internamente num sistema computacional.

Referências

Documentos relacionados

A aplicação do Atlas no caso do aço Sincron (ASTM A572 Grau 65) mostrou ser possível correlacionar microestruturas simuladas (Atlas) como microestrutura reais (soldas) e, a

Diante do exposto, a Hipótese de pesquisa deste estudo, de que o Índice de Qualidade da Governança Corporativa (IQGC) é value relevant para a avaliação de empresas no

Assim, considerando esse contexto, a forma, a autenticidade, a qualidade e o volume de informações divulgadas em relatórios socioambientais de empresas de mineração, e o propósito

A Diretora de Pós-Graduação, Professora Doutora Tereza Cristina Monteiro Mafra e o Coordenador Geral de Pós-Graduação da Faculdade de Direito Milton Campos,

Da mesma forma que os métodos de custeio, as práticas de orçamento tam- bém são relativamente tradicionais, com a utilização do orçamento anual para controle de custos,

autoria de pensamento, compreendendo o(a) aprendente em movimento, em busca de significados e sentidos para as suas práticas. Para entender melhor os processos de

OBSERVAÇÃO: Em qualquer modo de acerto, se nenhum botão de pressão for accionado durante dois minutos, todas as alterações serão guardadas e o acerto regressará automaticamente

Missão e Atribuições Investigação e Desenvolvimento Referência e Avaliação Externa da Qualidade Observação em Saúde Formação e Difusão da Cultura Científica