capitulo 1 - introducao - di.ubi.ptcbarrico/disciplinas/programacaoalgoritmos/downloads... ·...

25
Índice i Índice Í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.10. Restrições ao uso de variáveis ponteiro ............................................. 8

Upload: hathien

Post on 28-Nov-2018

219 views

Category:

Documents


0 download

TRANSCRIPT

Índice i

Índice

Í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.10. Restrições ao uso de variáveis ponteiro ............................................. 8

ii Índice

Índice

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

5.2. Estruturas Abstratas de Dados ......................................................... 23

Estruturas na linguagem C 1

Cap. 1 - Introdução

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.

2 Estruturas na linguagem C

Cap. 1 - Introdução

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

variável_estrutura.campo[índice]

Estruturas na linguagem C 3

Cap. 1 - Introdução

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

4 Ponteiros/apontadores em C

Cap. 1 - Introdução

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.

Ponteiros/apontadores em C 5

Cap. 1 - Introdução

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

6 Ponteiros/apontadores em C

Cap. 1 - Introdução

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.

Ponteiros/apontadores em C 7

Cap. 1 - Introdução

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.

8 Ponteiros/apontadores em C

Cap. 1 - Introdução

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 *P pode 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

Memória dinâmica 9

Cap. 1 - Introdução

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]);

}

}

10 Memória dinâmica

Cap. 1 - Introdução

#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).

Memória dinâmica 11

Cap. 1 - Introdução

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

frequência a função sizeof e o operador cast.

12 Memória dinâmica

Cap. 1 - Introdução

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).

Memória dinâmica 13

Cap. 1 - Introdução

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.

14 Memória dinâmica

Cap. 1 - Introdução

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.

Memória dinâmica 15

Cap. 1 - Introdução

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));

16 Memória dinâmica

Cap. 1 - Introdução

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.

Memória dinâmica 17

Cap. 1 - Introdução

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.

18 Memória dinâmica

Cap. 1 - Introdução

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); }

Memória dinâmica 19

Cap. 1 - Introdução

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.

20 Algoritmos recursivos

Cap. 1 - Introdução

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:

>−×

=

=

0nse)!1n(n

0nse1!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

Algoritmos recursivos 21

Cap. 1 - Introdução

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,

assegurando que o caso degenerado é sempre atingido.

22 Estruturas de Dados

Cap. 1 - Introdução

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.

Estruturas de Dados 23

Cap. 1 - Introdução

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.

Exemplos de Estruturas Abstratas de Dados são: Listas, Pilhas, Filas e Árvores.