Você está na página 1de 298

Universidade Católica de Angola

Algoritmos e complexidade

Carlos Ramos António


Capítulo 1 - Ponteiros

1.Variáveis estáticas e dinâmicas


2.Ponteiros
3.Passagem de Parâmetros
4.Aritmética de Ponteiros
5.Ponteiros e Vectores
6.Vectores e String's
7.Ponteiros para funções
8.Ponteiros para estruturas
9.Exercícios
Capítulo 1 - Ponteiros
Variáveis estáticas e dinâmicas
Como as variáveis dinâmicas só existem
quando o programa estiver a ser executado,
não é possível atribuir um nome durante a
fase de desenvolvimento desse programa. A
única forma de referenciar essa variável é
utilizar um ponteiro.
Capítulo 1 - Ponteiros
Ponteiro
Um ponteiro (pointer) é uma variável que
armazena um endereço de memória de um
objecto qualquer. Entendemos por objecto
uma variável elementar, uma variável
estruturada, uma função e um outro
ponteiro.
Capítulo 1 - Ponteiros
Ponteiro
Como um ponteiro é uma variável, ele
precisa de ser declarado. A sua declaração é
descrita pela seguinte sintaxe:

<tipo> <oper> <nome>;


Capítulo 1 - Ponteiros
Ponteiro
<tipo> <oper> <nome>;

onde
<tipo> representa qualquer tipo de dados
que a linguagem C suporta;
<oper> representa o operador do tipo
ponteiro que pode ser * ou &;
<nome> denota uma variável;
Capítulo 1 - Ponteiros
Ponteiro
<tipo> <oper> <nome>;

onde
<tipo> representa qualquer tipo de dados
que a linguagem C suporta;
<oper> representa o operador do tipo
ponteiro que pode ser * ou &;
<nome> denota uma variável;
Capítulo 1 - Ponteiros
Ponteiro
O operador unário *, denominado por
operador de acesso indirecto é um ponteiro
que devolve o conteúdo do endereço de
memória do seu operando. Por exemplo,
nas declarações:
int *p;
float *f;
Capítulo 1 - Ponteiros
Ponteiro
as variáveis p e f são ponteiros. O primeiro
armazena o conteúdo do endereço de
memória de objetos do tipo inteiro,
enquanto o segundo armazena o conteúdo
de memória de objectos do tipo real.
Capítulo 1 - Ponteiros
Ponteiro
Um ponteiro só deve armazenar o conteúdo
de um endereço de memória se esse
conteúdo e o ponteiro tiverem o mesmo
tipo de dados. Se essa restrição não for
observada, teremos resultados
imprevisíveis.
Capítulo 1 - Ponteiros
Ponteiro
O operador unário &, denominado por
operador endereço é um ponteiro que
devolve o endereço de memória do seu
operando. Por exemplo, no segmento de
código:
float x = -1.0;
float *ptr;
ptr = &x;
Capítulo 1 - Ponteiros
Ponteiro
Nas duas primeiras linhas declaramos uma
variável x do tipo float que recebe o valor
-1.0 e um ponteiro p do mesmo tipo. Como
temos uma compatibilidade de tipos, na
terceira linha o ponteiro ptr aponta para o
endereço da variável x e, dizemos que ptr
aponta para x.
Capítulo 1 - Ponteiros
Passagem de parâmetros
Por defeito, na linguagem C os parâmetros
são passados por valor. Neste tipo de
passagem, os parâmetros da função e os
parâmetros da chamada estão armazenados
em posições de memória diferentes. Logo,
nenhuma alteração feita aos parâmetros da
função afectam os valores dos parâmetros
da chamada.
Capítulo 1 - Ponteiros
Passagem de parâmetros
Uma das principais vantagens dessa forma
de passagem é que as funções ficam
impedidas de aceder aos conteúdos das
variáveis declaradas em outras funções.
Capítulo 1 - Ponteiros
Passagem de parâmetros
Para que uma função possa alterar os
valores dos parâmetros da chamada é
necessário que eles sejam passados por
referência. Nesse caso, os parâmetros da
chamada e os parâmetros da função
compartilham as mesmas posições de
memória.
Capítulo 1 - Ponteiros
Passagem de parâmetros
Para que uma função possa alterar os
valores dos parâmetros da chamada é
necessário que eles sejam passados por
referência. Nesse caso, os parâmetros da
chamada e os parâmetros da função
compartilham as mesmas posições de
memória.
Capítulo 1 - Ponteiros
Passagem de parâmetros
Para clarificar esse conceito vejamos um
exemplo. A função permutar() que
descrevemos a seguir, troca os conteúdos
dos dois parâmetros passados por
referência.
Capítulo 1 - Ponteiros
Passagem de parâmetros
Observe que na declaração da função os
nomes dos parâmetros são precedidos por
um asterístico, isso quer dizer que a função
recebe como parâmetro os conteúdos dos
endereços representados por esses
ponteiros.
Capítulo 1 - Ponteiros
Passagem de parâmetros
#include <stdio.h>
...
void permutar ( int *p, int *q )
{
int x;
x = *p;
*p = *q;
*q = x;
}
Capítulo 1 - Ponteiros
Passagem de parâmetros
Na chamada dessa função os parâmetros
são precedidos pelo operador endereço &,
isso quer dizer que o compilador vai
transferir para a função os endereços de
memória onde os parâmetros estão
armazenados.
Capítulo 1 - Ponteiros
Passagem de parâmetros
int main()
{
int a = 3, b = 7;
permutar (&a, &b ); /* Chamada da Função */
printf("%d %d", a, b);
return 0;
}
Capítulo 1 - Ponteiros
Aritmética de Ponteiros
A adição e à subtracção são as únicas
operações aritméticas que podem ser
realizadas com ponteiros. Para entender o
que ocorre com a aritmética de ponteiros,
vamos considerar que pt1 é um ponteiro
que aponta para o endereço 2000 e que
esse ponteiro é do tipo inteiro. O valor do
incremento: pt1++
Capítulo 1 - Ponteiros
Aritmética de Ponteiros
Depende da arquitectura do computador.
Suponhamos sem perda da generalidade
que o computador possui uma arquitectura
de 64 bits. Para essa arquitectura, o tipo
caracter é armazenado em um byte, o
inteiro em quatro bytes, o float em quatro
bytes e o double em 8 bytes.
Capítulo 1 - Ponteiros
Aritmética de Ponteiros
A operação anterior deverá apontar para a
posição de memória do próximo elemento
do tipo inteiro. O próximo elemento está no
endereço 2004. O mesmo acontece com a
operação de decremento, para esse caso, o
ponteiro apontaria para o endereço 1996.
Capítulo 1 - Ponteiros
Aritmética de Ponteiros
Também podemos realizar a operação de
diferença de ponteiros. Dados dois
ponteiros do mesmo tipo, a diferença de
ponteiros permite determinar quantos
elementos existem entre eles. Por exemplo,
o comprimento de uma string, pode ser
obtido pela diferença entre o carácter ‘\0’ e
o endereço do primeiro elemento.
Capítulo 1 - Ponteiros
Aritmética de Ponteiros
int strlen ( char *s)
{
char *ptr = s;
while ( *s != ‘\0’)
s++;
return (s - ptr); // diferença entre os endereços
}
Capítulo 1 - Ponteiros
Ponteiros e Vetores
Quando declaramos um vetor, o compilador
reserva automaticamente um bloco de
bytes consecutivos de memória, para
armazenar os seus elementos e, trata o
nome do vetor, como um ponteiro que faz
referencia ao primeiro elemento. Por
exemplo, na declaração:
int x[4];
Capítulo 1 - Ponteiros
Ponteiros e Vetores
Se o primeiro elemento estiver armazenado
no endereço 104, o segundo estará
endereço 108, o terceiro no endereço 112 e
o ultimo no endereço 116. Para além disso,
x é um ponteiro que aponta para o
endereço 104.
Capítulo 1 - Ponteiros
Ponteiros e Vetores
Como o compilador trata o nome de um
vetor como um ponteiro, então, o acesso a
qualquer elemento pode ser feito pela
notação vetorial ou pela notação com de
ponteiro.
Capítulo 1 - Ponteiros
Ponteiros e Vetores
Na notação vetorial, o i-ésimo elemento é
acedido pela variável indexada x[i],
enquanto na notação por ponteiro esse
elemento é acedido pelo ponteiro *(x+i).
Isto que dizer que:
x[0] = *(x + 0);
x[1] = *(x + 1);
x[i] = *(x + i);
Capítulo 1 - Ponteiros
Ponteiros e Vetores
Então, o comando de atribuição múltiplo:

x[1] = x[2] + x[3];

é equivalente à:

*(x+1) = *(x+2) + *(x+3);


Capítulo 1 - Ponteiros
Ponteiros e Vetores
E a função
int somaElementos ( int A[ ], int ultPos)
{
int i, soma = 0;
for ( i = 0; i <= ultPos; i++)
soma += A[i];
return soma;
}
Capítulo 1 - Ponteiros
Ponteiros e Vetores
É equivalente à:
int somaElementos ( int A[ ], int ultPos)
{
int i, soma = 0; *pta = A;
for ( i = 0; i <= ultPos; i++)
soma += *(pta+i);
return soma;
}
Capítulo 1 - Ponteiros
Ponteiros e Strings
As cadeias de caracteres são vetores. Como
a notação por ponteiros é mais eficiente do
que a notação vetorial para aceder aos
elementos de um vetor, devemos utilizá-la
para desenvolver as nossas funções. Por
exemplo, para a função "String Copy"
temos:
Capítulo 1 - Ponteiros
Ponteiros e Strings
Versão por ponteiro
void strcpy(char *s, char *t)
{
while ((*s = *t) != ´\0´)
{
s++;
t++;
}
}
Capítulo 1 - Ponteiros
Ponteiros e Strings
Versão vetorial
void strcpy(char *s, char *t)
{
int i = 0;

while((s[i] = t[i]) != '\0')


i++;
}
Capítulo 1 - Ponteiros
Ponteiros e Strings
pode ser optimizada para:

void strcpy(char *s, char *t)


{
while ((*s++ = *t++));
}
Capítulo 1 - Ponteiros
Ponteiros e Strings
Para a função “String Length”, temos:

int strlen ( char *s)


{
int i = 0;
while (*s++)
i++;
return i;
}
Capítulo 1 - Ponteiros
Ponteiros e Strings
Para a função “string Compare” que retorna
um número positivo se s > t; um número
negativo se s < t e zero se s = t, temos:
Capítulo 1 - Ponteiros
Ponteiros e Strings
int strcmp ( char *s, char *t)
{
while ( *s == *t && *s)
{
s++;
t++;
}
return (*s-*t);
}
Capítulo 1 - Ponteiros
Ponteiros e Strings
Os vetores de ponteiros são normalmente
utilizados para processar vetores de
cadeiras de caracteres. Um dos exemplos
muito comuns em programação, é a
impressão das mensagens de erro de uma
aplicação.
Capítulo 1 - Ponteiros
Ponteiros e Strings
Em vez de escrevermos comandos para
imprimir essas mensagens no interior das
funções que desenvolvemos, devemos
retornar um código de erro, e tratá-lo num
procedimento independente.
Capítulo 1 - Ponteiros
Ponteiros e Strings
Suponhamos sem perda da generalidade,
que pretendemos desenvolver um
programa para movimentar um rato num
labirinto e necessitamos de mostrar as
seguintes mensagens:
Capítulo 1 - Ponteiros
Ponteiros e Strings
Posição inicial inválida, Posição final não
encontrada e Posição final encontrada. O
procedimento que descrevemos a seguir
implementa o tratamento dos erros
enviados pelas funções que compõem esse
programa.
Capítulo 1 - Ponteiros
Ponteiros e Strings
void msgErro ( int codigo )
{
char * Erro[ ] = { "Posicao Inicial Inválida",
"Posicao Final Nao Encontrada",
"Posicao Final Encontrada" };
printf ( "\n "%s", Erro[codigo] );
}
Capítulo 1 - Ponteiros
Ponteiros e Strings
Observe que o vetor Erro contêm um
ponteiro para cada string. Se o
procedimento msgErro() receber como
parâmetro de entrada o número dois, ele irá
mostrar na tela a mensagem Posição Final
Encontrada.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Já vimos que funções podem receber
parâmetros do tipo ponteiro; de fato, isso é
feito sempre que usamos passagem por
referência.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Agora, veremos também que podemos criar
funções que devolvem ponteiros como
resposta ou, ainda, que podemos criar
ponteiros para apontar áreas de memória
onde encontra-se o código de uma função.
Capítulo 1 - Ponteiros
Ponteiros para Funções
C não permite que vetores sejam devolvidos
por uma função. Então, quando uma função
precisa devolver um vetor, ela devolve seu
endereço. Tipicamente, ponteiros são
devolvidos por funções que precisam
devolver strings.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Para declarar uma função que devolve um
ponteiro, basta colocar como prefixo um
asterisco no nome da função.
Capítulo 1 - Ponteiros
Ponteiros para Funções
char *dia_semana(int n)
{
char *d[] = {
"erro","domingo","segunda-feira",
"terça-feira","quarta-feira",
"quinta-feira","sexta-feira","sábado"
};
return d[1<=n && n<=7 ? n : 0];
}
Capítulo 1 - Ponteiros
Ponteiros para Funções
Quando chamada com um inteiro entre 1 a
7, a função devolve um ponteiro para uma
string contendo o dia da semana
correspondente por extenso. Para valores
fora do intervalo, ela devolve um ponteiro
para a string "erro".
Capítulo 1 - Ponteiros
Ponteiros para Funções
Contudo, a linguagem C permite que se crie
ponteiros que armazenam endereços de
memória que indicam o início do código de
uma função.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Um poderoso recurso oferecido em C é a
possibilidade de tomar o endereço de uma
função, armazená-lo numa variável ponteiro
e, depois, executá-la através desse ponteiro.
Para declarar um ponteiro de função,
usamos a seguinte sintaxe:
Capítulo 1 - Ponteiros
Ponteiros para Funções
t (*f)(t1, t2, …),
sendo f um ponteiro para uma função que
devolve uma resposta do tipo t e recebe
uma lista de parâmetros cujos tipos são t1,
t2, etc.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Nem todas as operações permitidas com
ponteiros de dados são permitidas com
ponteiros de código; por exemplo, não é
possível incrementar nem decrementar
ponteiros de função. Em C, um ponteiro de
código pode ser usado apenas para receber
o endereço de uma função e chamar a
função apontada.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Assim como ocorre no caso de vetores, o
nome de uma função representa o endereço
em que seu código se encontra armazenado
na memória.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Vimos que quando passamos um vetor
como parâmetro de uma função, o
compilador trata o nome desse vetor como
um ponteiro para o primeiro endereço de
memória.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Para as funções, o compilador trata essa
passagem da mesma maneira, ou seja, ele
associa um ponteiro ao endereço de
memória onde inicia o código da função. Por
exemplo, as funções:
Capítulo 1 - Ponteiros
Ponteiros para Funções
int soma (int a, int b)
{
return a+b;
}
int subtracao ( int a, int b);
{
return a – b;
}
Capítulo 1 - Ponteiros
Ponteiros para Funções
int mult (int a, int b)
{
return a*b;
}
int divisao ( int a, int b);
{
return a /b;
}
Capítulo 1 - Ponteiros
Ponteiros para Funções
Estas funções recebem como parâmetro
duas variáveis do tipo inteiro e retorna a
soma, subtracção , multiplicação e divisão
inteira, do conteúdo dessas variáveis.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Com a declaração:

int (*pf)(int, int );

Criamos como ponteiro qualquer função


que recebe como parâmetro duas variáveis
do tipo inteiro e retorna um valor inteiro do
mesmo tipo.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Então podemos utilizar essa declaração,
para passar como parâmetro um ponteiro,
que irá receber qualquer função que tenha
esse cabeçalho. A função que descrevemos
a seguir, utiliza esse tipo de passagem de
parâmetros para mostrar na tela o resultado
dessa operação.
Capítulo 1 - Ponteiros
Ponteiros para Funções
void imprimeOper ( int (*pf)(int , int ) , int n,
int m )
{
printf (“\n %d “, (*pf) (n, m) );
}
Capítulo 1 - Ponteiros
Ponteiros para Funções
Provavelmente, uma das aplicações mais
interessante de ponteiros de funções seja
possibilitar que uma função seja passada a
outra como argumento. Isso nos permite
implementar rotinas cujo processamento
executado depende do tipo de entrada que
passamos a ela.
Capítulo 1 - Ponteiros
Ponteiros para Funções
Agora, estamos em condições de poder
escrever uma função principal para efectuar
as operações aritméticas no domínio inteiro.

main( )
{
imprimeOperacao (soma,3,5);
imprimeOperacao (subtração,5,9);
imprimeOperacao (multiplicacao,10,5);
}
Capítulo 1 - Ponteiros
Ponteiros para Funções
Ponteiros de código permitem que funções
sejam passadas como argumentos a outras
funções; um recurso essencial na
implementação de rotinas polimórficas em
C.
Capítulo 1 - Ponteiros
Ponteiros e Estruturas
Um ponteiro para uma estrutura não é
diferente de um ponteiro para um tipo de
dados elementar. Se p aponta para uma
estrutura que tem um campo m, então
podemos escrever (∗p).m para aceder a
esse campo. Por exemplo:
Capítulo 1 - Ponteiros
Ponteiros e Estruturas
Capítulo 1 - Ponteiros
Ponteiros e Estruturas
Como o operador ponto (.) tem maior
precedência que o operador de acesso
indireto (∗), temos de utilizar
obrigatoriamente parênteses para fazer
referencia a qualquer campo dessa
estrutura no comando printf().
Capítulo 1 - Ponteiros
Ponteiros e Estruturas
Para evitar essa notação pesada, a
linguagem C possui um operador específico
para aceder aos campos das estruturas,
denominado por operador seta. Com esse
operador, em vez de escrevermos
(∗ponteiro).membro passamos a escrever
ponteiro−>membro. Então, o programa
anterior deveria ser escrito como:
Capítulo 1 - Ponteiros
Ponteiros e Estruturas
Capítulo 1 - Ponteiros
Ponteiros e Estruturas

Vamos estudar em seguida, algumas


operações com estruturas. As funções
podem receber e retornar via valor de
retorno estruturas.
Capítulo 1 - Ponteiros
Ponteiros e Estruturas
Capítulo 1 - Ponteiros
Ponteiros e Estruturas

Observe que a função retorna uma cópia da


estrutura, que neste caso é a estrutura res.
Como a passagem é feita por valor o acesso
a qualquer campo é feito pelo operador
ponto.
Capítulo 1 - Ponteiros
Ponteiros e Estruturas

Mas as estruturas passadas por valor podem


trazer alguns problemas de eficiência. De
facto, a passagem de estruturas muito
grandes como parâmetros é ineficiente,
uma vez que é necessário efetuar cópia de
todos os campos.
Capítulo 1 - Ponteiros
Ponteiros e Estruturas

Por essa razão, utilizam-se normalmente


ponteiros para estruturas e, portanto, a
passagem passa a ser por referência. Nesse
sentido, o exemplo anterior teria a seguinte
código:
Capítulo 1 - Ponteiros
Ponteiros e Estruturas
void adicionarPonto (TPlano *p1, TPlano *p2, Tplano *res)
{
res->x = p1->x + p2->x;
res->y = p1->y + p2->y;
}
Capítulo 1 - Ponteiros
Exercícios
1.9.3-Desenvolva a função strchr (char *s,
char c) que retorna o endereço da primeira
ocorrência do caracter c na string s ou o
valor NULL caso esse caracter não seja
encontrado.
 
Capítulo 1 - Ponteiros
Exercícios

1.9.5-Desenvolva a função strsub (char


*s,i,n) denominada por “substring”, que
devolve uma substring de s que inicia na
posição i e tem no máximo n caracteres. A
string s não deve ser alterada.
Capítulo 1 - Ponteiros
Exercícios

1.9.7-Desenvolva a função strcountc (char


*s, char ch) denominada por “string Count
Char”, que retorna o número de ocorrências
do caracter ch na string s.
•Cap 2 – Gestão Dinâmica de Mem
Uma das aplicações mais interessantes de
ponteiros é a alocação dinâmica de
memória, que permite a um programa
requisitar memória adicional para o
armazenamento de dados durante sua
execução.
•Cap 2 – Gestão Dinâmica de Mem
Em geral, após um programa ter sido
carregado para a memória, parte dela
permanece livre. Então, usando a função
malloc(), o programa pode alocar um pedaço
dessa área livre e acede-lo através de um
ponteiro.
•Cap 2 – Gestão Dinâmica de Mem
A função malloc() exige como argumento o
tamanho, em bytes, da área de memória que
será alocada. Então, se o espaço livre é
suficiente para atender à requisição, a área é
alocada e seu endereço é devolvido; senão, a
função devolve um ponteiro especial,
representado pela constante NULL.
•Cap 2 – Gestão Dinâmica de Mem
Além disso, como malloc() não tem
conhecimento sobre o tipo dos dados que
serão armazenados na área alocada, ela
devolve um ponteiro do tipo void *.

Ponteiros de dados do tipo void* são


compatíveis de atribuição com ponteiros de
quaisquer outros tipos e, por isso, são
denominados ponteiros genéricos.
•Cap 2 – Gestão Dinâmica de Mem
Com ponteiros genéricos ou ponteiros do tipo void
*, não podemos efectuar operações de atribuição e
de leitura, porque o programa não consegue
armazenar dados nos blocos de memória que
foram alocadas. Para tornar essas operações
possíveis é necessário converter de forma explicita,
utilizando o operador cast, o endereço obtido pela
função malloc(), para um ponteiro com o tipo de
dados que iremos manipular.
•Cap 2 – Gestão Dinâmica de Mem
Por exemplo, para armazenar um número
inteiro na memória dinâmica, devemos
converter o ponteiro genérico para um
ponteiro do tipo inteiro.

void *ptr;
int *pnumero;
ptr = malloc(1000);
pnumero = (int *)ptr;
•Cap 2 – Gestão Dinâmica de Mem
•Cap 2 – Gestão Dinâmica de Mem
Outro ponto importante é que, como cada
tipo de dados pode requerer uma
quantidade de memória diferente,
dependendo da máquina que executa o
programa, devemos usar o operador sizeof
para especificamos a quantidade de
memória a ser alocada.
Para usar a função malloc(), devemos incluir
o arquivo stdlib.h ou alloc.h.
•Cap 2 – Gestão Dinâmica de Mem
•Cap 2 – Gestão Dinâmica de Mem
O programa cria um vetor cujo tamanho é
determinado pelo próprio usuário, em
tempo de execução. Como o vetor deve ter
capacidade para guardar n valores e cada um
deles ocupa 4 bytes, a área de memória
alocada deve ter 4n bytes de extensão;
justamente o valor da expressão n *sizeof(int).
•Cap 2 – Gestão Dinâmica de Mem
Quando um programa termina, todas as
áreas alocadas pela função malloc() são
automaticamente liberadas. Entretanto, se
for necessário liberar explicitamente uma
dessas áreas, podemos usar a função free().
Essa função exige como argumento um
ponteiro para a área que será liberada.
•Cap 2 – Gestão Dinâmica de Mem
•Cap 2 – Gestão Dinâmica de Mem
•Cap 2 – Gestão Dinâmica de Mem
Às vezes é necessário alterar, durante a
execução do programa, o tamanho de um
bloco de bytes que foi alocado por malloc.
Isso acontece, por exemplo, durante a leitura
de um arquivo que se revela maior que o
esperado. Nesse caso, podemos recorrer à
função realloc para redimensionar o bloco
de bytes.
•Cap 2 – Gestão Dinâmica de Mem
A função realloc recebe o endereço de um
bloco previamente alocado por malloc (ou
por realloc) e o número de bytes que o bloco
redimensionado deve ter. A função aloca o
novo bloco, copia para ele o conteúdo do
bloco original, e devolve o endereço do novo
bloco.
•Cap 2 – Gestão Dinâmica de Mem
Se o novo bloco for uma extensão do bloco
original, seu endereço é o mesmo do original
(e o conteúdo do original não precisa ser
copiado para o novo). Caso contrário, realloc
copia o conteúdo do bloco original para o
novo e libera o bloco original (invocando
free). A propósito, o tamanho do novo bloco
pode ser menor que o do bloco original.
•Cap 2 – Gestão Dinâmica de Mem
•Cap 2 – Gestão Dinâmica de Mem
Suponhamos, por exemplo, que alocamos
um vetor de 1000 inteiros e depois
decidimos que precisamos de duas vezes
mais espaço. Vejamos um caso concreto:
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de vetores:
Uma das aplicações mais interessantes na
gestão de memória dinâmica é a criação e a
manipulação de vetores.
Para criar um ponteiro para um vetor,
devemos seguir a seguinte sequencia de
passos:
1º- Calcular com a função sizeof() o número
de bytes para armazenar a estrutura;
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de vetores:
2º- Invocar a função malloc() para retornar
um apontador genérico que aponta para o
primeiro byte desse bloco de memória;
3º- Converter o ponteiro genérico retornado
pela função malloc(), para um ponteiro cujo
tipo de dados é compatível com o tipo do
vetor.
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de vetores:
Eis como um vetor com n elementos inteiros
pode ser alocado (e depois desalocado)
durante a execução de um programa:
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de vetores:
A expressão abaixo representa a alocação
dinâmica de vetores:
v = malloc (100 * sizeof (int));

Tem efeito análogo ao da alocação estática:


int v[100];
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de matrizes:
Matrizes bidimensionais são implementadas
como vetores de vetores. Uma matriz com m
linhas e n colunas é um vetor de m
elementos cada um dos quais é um vetor de
n elementos. O seguinte fragmento de
código faz a alocação dinâmica de uma tal
matriz:
•Cap 2 – Gestão Dinâmica de Mem
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de estrutura:
O procedimento para alocação dinâmica de
estruturas (struct) é análogo ao
procedimento para a alocação dinâmica de
vetores. Para mostrar essa analogia, veremos
a declaração de um ponto no plano
cartesiano.
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de estrutura:

Para criar um ponteiro para uma estrutura


devemos utilizar os mesmos passos que
utilizamos para criar um ponteiro para um
vetor. Veremos a seguir uma função que
atende a essa necessidade.
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de estrutura:
•Cap 2 – Gestão Dinâmica de Mem
Alocação dinâmica de estrutura:
O programa cria um vetor f cujos elementos
são ponteiros para estruturas do tipo
funcionário. Dentro do laço for, as
estruturas são alocadas dinamicamente e
seus endereços são guardados nesse vetor.
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
O espaço de endereços de um processo
em execução é dividido em várias
segmentos lógicos. Os mais importantes
são:
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
Text: contém o código do programa e
suas constantes. Este segmento é
alocado durante a criação do processo
(''exec'') e permanece do mesmo
tamanho durante toda a vida do
processo.
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
Data: este segmento é a memória de
trabalho do processo, aonde ficam
alocadas as variáveis globais e
estáticas. Tem tamanho fixo ao longo da
execução do processo.
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
Stack: contém a pilha de execução,
onde são armazenadas os parâmetros,
endereços de retorno e variáveis locais
de funções. Pode variar de tamanho
durante a execução do processo.
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
Heap: contém blocos de memória
alocadas dinamicamente, a pedido do
processo, durante sua execução. Varia
de tamanho durante a vida do processo.
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
Um programa em C suporta três tipos de
alocação de memória:
A alocação estática ocorre quando são
declaradas variáveis globais ou estáticas;
geralmente alocadas em Data.
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
A alocação automática ocorre quando são
declaradas variáveis locais e parâmetros de
funções. O espaço para a alocação dessas
variáveis é reservado quando a função é
invocada, e liberado quando a função
termina. Geralmente é usada a pilha (stack).
•Cap 2 – Gestão Dinâmica de Mem
Alocação de memória Resumo:
A alocação dinâmica, quando o processo
requisita explicitamente um bloco de
memória para armazenar dados; o controle
das áreas alocadas dinamicamente é
manual ou semi-automático: o programador
é responsável por liberar as áreas alocadas
dinamicamente. A alocação dinâmica
geralmente usa a área de heap.
•Cap 3 – Tipo Abstrato de Dados
A abstração é um dos conceitos mais
importantes na programação de
computadores. É uma técnica de
modelagem que nos permite concentrar nos
aspetos essenciais de um problema,
ignorando as características menos
importantes.
•Cap 3 – Tipo Abstrato de Dados
Abstração de Procedimentos
A maior parte dos elementos sintáticos de
uma linguagem de programação de alto
nível são abstrações. Estas abstrações
facilitam o desenvolvimento de programas
complexos e evitam que os programadores
tenham a necessidade de manipular bits e
endereços de memória.
•Cap 3 – Tipo Abstrato de Dados
Abstração de Procedimentos
A abstração de procedimento permite que o
programador crie funcionalidades que
escondem as sequências de instruções
necessárias à sua realização.
•Cap 3 – Tipo Abstrato de Dados
Abstração de Procedimentos
Numa abstração de um procedimento, o
programador deve ver esse procedimento como
uma caixa preta, que só mostra as seguintes
informações:
Entrada: O conjunto dos dados de entrada;
Saída: O conjunto dos dados produzidos;
Pós-Condições: Descrição resumida sobre a sua
finalidade.
Pré-Condições: Restrições necessárias para a
execução desse procedimento.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
A abstração de dados tem os mesmos
objetivos do que a abstração de
procedimentos, mas está voltada para as
estruturas de dados utilizadas nos
programas.
A abstração de dados tem por objetivo criar
novos tipos de dados e modelar o seu
comportamento. Esses novos tipos são
chamados de Tipos Abstratos de Dados
(TAD).
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
Os tipos abstractos de dados são de facto,
estruturas de dados que não foram previstas
no núcleo das linguagens de programação, e
são necessárias para resolver problemas
específicos. Essas estruturas estão divididas
em duas partes: os dados e as operações que
manipulam esses dados.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
Um exemplo que ilustra esta forma de
organização do software é a biblioteca stdio,
que entre outras funcionalidades, define o
Tipo Abstrato de Dados FILE. Se consultar o
arquivo 'stdio.h’, irá perceber que este
contém apenas as definições necessárias
para que uma aplicação desenvolvida por
terceiros, consiga utilizar as variáveis do tipo
FILE, e as respectivas operações.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
Os detalhes da implementação dessas
funções, ficam escondidos dos programas
que os programadores utilizadores
desenvolvem.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
O desenvolvimento de bibliotecas universais
como essa, baseia-se nos seguintes
princípios:
- Abstracção; Encapsulamento; Generalização.
A abstracção tem por finalidade separar os
conceitos essenciais do problema a ser
resolvido dos detalhes da programação.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
O encapsulamento tem por finalidade manter
dentro de cada módulo do sistema os
detalhes que só dizem respeito e esse
módulo. Cada módulo só 'exporta' as
definições necessárias dos serviços que ele
oferece. Essa é a base do conceito de
encapsulamento das linguagens orientadas
por objetos.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
A generalização tem por finalidade projectar
cada módulo de forma que este possa ser
utilizado pelo maior número de aplicações
possíveis.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
O desenvolvedor não tem a necessidade de
conhecer os detalhes da implementação das
bibliotecas de aplicação, ou seja, das linhas
de código da implementação do TDA.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
Para que o desenvolvimento de aplicações
seja consistente é necessário adoptar a
seguinte metodologia:

- Análise do Problema;
- Especificação do Problema.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
Vamos aplicar os conceitos vistos na secção
anterior para desenvolver um programa, que
irá realizar operações aritméticas de adição e
de multiplicação de números racionais, dados
dois números inteiros. Em primeiro lugar,
vamos extrair do problema a estrutura de
dados.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
typedf struct
{
int x;
int y;
} TRacional;
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
Em segundo, vamos extrair as operações
associadas a essa estrutura de dados.
•Ler um número inteiro;
•Criar um número racional;
•Calcular a soma de dois números racionais;
•Calcular a multiplicação de dois números racionais.
•Verificar se dois números racionais são iguais.
•Imprimir um número racional.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
Para finalizar, vamos detalhar para cada
operação, o tipo de dados que recebe, que
devolve, o objectivo da operação e as
condições necessárias para sua realização.
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
CriarRacional()
Entrada: Dois números inteiros;
Saída : Um número racional;
Pré-Condições: Denominador não pode ser
igual a zero;
Pós-Condições: Criar um número racional;
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
SomarRacional()
Entrada: Dois números racionais;
Saída : Um número racional;
Pré-Condições: Nenhuma;
Pós-Condições: Somar dois números
racionais;
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
MultiplicarRacional()
Entrada: Dois números racionais;
Saída : Um número racional;
Pré-Condições: Nenhuma;
Pós-Condições: Multiplicar de dois números
racionais;
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
TestarIgualdade()
Entrada: Dois números racionais;
Saída : Verdadeiro ou Falso
Pré-Condições: Nenhuma;
Pós-Condições: Verifica se dois números
racionais são iguais;
•Cap 3 – Tipo Abstrato de Dados
Tipo Abstracto de Dados
ImprimirRacional()
Entrada: um números racionais;
Saída : nada
Pré-Condições: Nenhuma;
Pós-Condições: Mostrar na tela um número
racional;
•Cap 3 – Tipo Abstrato de Dados
Implementação do Problema
Cada Tipo Abstracto de Dados deve ser
implementado por um único módulo e cada
módulo deve ser constituído por uma
estrutura de dados e um conjunto de
operações que a manipulam.
•Cap 3 – Tipo Abstrato de Dados
Implementação do Problema
Para garantir a universalidade dos módulos e
a coerência dos dados devolvidos, as
operações devem ser implementadas como
subprogramas com um sistema de controlo
de erros.
•Cap 3 – Tipo Abstrato de Dados
Implementação do Problema
Com este método, cada subprograma deverá
devolver um código de erro a notificar o
estado da operação, e com esse código, o
programador saberá se operação terminou
com sucesso ou não.
•Cap 3 – Tipo Abstrato de Dados
Implementação do Problema
Para esconder as linhas de código dos
subprogramas que compõem cada módulo,
cada TAD deve ser desenvolvido em dois
arquivos diferentes. O arquivo interface e o
arquivo com a implementação. Esses
arquivos serão objecto de estudo nas
próximas secções.
•Cap 3 – Tipo Abstrato de Dados
TDA (Recap)
Uma estrutura de dados é uma forma de
armazenar e organizar os dados de modo que
eles possam ser usados de forma eficiente.
Consiste em:
um conjunto de tipos de dados;
Definição de operações que podem ser
realizadas sobre este conjunto de dados.
•Cap 3 – Tipo Abstrato de Dados
TDA (Recap)
Exemplos:
•arranjos (vetores e matrizes);
•estruturas (struct);
•referências (ponteiros)

Um TAD é uma forma de definir um novo tipo


de dado juntamente com as operações que
manipulam esse novo tipo de dado.
•Cap 3 – Tipo Abstrato de Dados
TDA (Recap)
Separação entre conceito (definição do tipo)
e implementação das operações; Visibilidade
da estrutura interna do tipo fica limitada às
operações; Aplicações que usam o TAD são
denominadas clientes do tipo de dado;
Cliente tem acesso somente à forma abstrata
do TAD.
•Cap 3 – Tipo Abstrato de Dados
TDA (Recap)
O TAD estabelece o conceito de tipo de dado
separado da sua representação. Definido
como um modelo matemático por meio de
um par (v, o) em que: v é um conjunto de
valores o é um conjunto de operações sobre
esses valores.
Ex.: tipo real v = R
o = {+, −, ∗, /, =, , <=, >=}
•Cap 3 – Tipo Abstrato de Dados
Projeto de um TDA
Envolve a escolha de operações
adequadas para uma determinada
estrutura de dados, definindo seu
comportamento.
•Cap 3 – Tipo Abstrato de Dados
Projeto de um TAD
Dicas para definir um TAD:
Definir pequeno número de operações
conjunto de operações deve ser suficiente
para realizar as computações necessárias às
aplicações que utilizarem o TAD, cada
operação deve ter um propósito bem
definido, com comportamento constante e
coerente.
•Cap 3 – Tipo Abstrato de Dados
Exemplo de TAD: Representação de um
ponto

Ponto (x,y)
•Coordenada - x
•Coordenada - y
•Cap 3 – Tipo Abstrato de Dados
Exemplo de TAD: Representação de um
ponto

Par (v, o):


v - dupla formada por dois reais: Ponto(x,y)

o - operações aplicáveis sobre o tipo Ponto


•Cap 3 – Tipo Abstrato de Dados
Exemplo de TAD: Representação de um
ponto
Operações:
pto_cria: operação que cria um ponto,
alocando memória para as coordenadas x e y;
pto_libera: operação que libera a memória
alocada por um ponto;
pto_acessa: operação que devolve as
coordenadas de um ponto;
•Cap 3 – Tipo Abstrato de Dados
Exemplo de TAD: Representação de um
ponto
Operações:
pto_atribui: operação que atribui novos
valores às coordenadas de um ponto;
pto_distancia: operação que calcula a
distância entre dois pontos.
•Cap 3 – Tipo Abstrato de Dados
Modularização e Implementação do TAD
A convenção em linguagem C é preparar dois
arquivos para implementar uma TAD:
Arquivo .h: protótipos das funções, tipos de
ponteiro, e dados globalmente acessíveis.
Aqui é definida a interface visível pelo
usuário.
•Cap 3 – Tipo Abstrato de Dados
Modularização e Implementação do TAD
Arquivo .c: declaração do tipo de dados e
implementação das suas funções. Aqui é
definido tudo que ficará oculto do cliente da
TAD. Assim separamos o “conceito”
(definição do tipo) de sua “implementação”.
A esse processo de separação da definição do
TAD em dois arquivos damos o nome de
modularização.
•Cap 3 – Tipo Abstrato de Dados
Interface
Trata-se do Protótipo de função ou
declaração de uma função.
int fac(int n);
Através da utilização de protótipos de função
em arquivos de cabeçalho (normalmente, em
programas escritos na linguagem C, em
arquivos com a extensão “.h”) é possível
especificar interfaces para bibliotecas de
software.
•Cap 3 – Tipo Abstrato de Dados
Interface
Na interface também podemos especificar
tipos que são globais e portanto acessíveis
globalmente e também podemos especificar
ponteiros.
•Cap 3 – Tipo Abstrato de Dados
Exemplo Ponto

•Definir o arquivo “.h”


•Protótipos das funções
•Tipos de ponteiros
•Dados globalmente acessíveis
•Definir o arquivo “.c”
•Na condição de cliente, usar...
•Cap 3 – Tipo Abstrato de Dados
Arquivo .h (Definição da TAD e suas operações)
•Cap 3 – Tipo Abstrato de Dados
Arquivo .c (Implementação das operações)
•Cap 3 – Tipo Abstrato de Dados
Arquivo .c (Implementação das operações)
•Cap 3 – Tipo Abstrato de Dados
Arquivo .c (Para aplicação cliente)
•Cap 3 – Tipo Abstrato de Dados
Processo de compilação de um programa em c
•Cap 3 – Tipo Abstrato de Dados
Processo de compilação de um programa em c
1. Pré-processamento
Responsável por modificar o código-fonte
do programa lexicamente. É nesta etapa
que ocorre a suspensão de espaços
desnecessários e a inclusão de outros
códigos (através das diretivas de pré-
processamento).
•Cap 3 – Tipo Abstrato de Dados
Processo de compilação de um programa em c
1. Pré-processamento
Esta etapa gera um código chamado de
unidade de compilação.

O comando para fazer o pré-processamento


do programa é o gcc –E <nome_programa>.c
•Cap 3 – Tipo Abstrato de Dados
Processo de compilação de um programa em c
2. Compilação (cria um ficheiro .s)
É na compilação que o compilador faz a
análise sintática e semântica da unidade de
compilação. Não havendo erro de sintaxe e
de semântica o compilador gera o código
assembly correspondente.
O comando para fazer a compilação do
programa é o gcc –S <nome_programa>.c
•Cap 3 – Tipo Abstrato de Dados
Processo de compilação de um programa em c
3. Montagem (cria um ficheiro .o)
A etapa de montagem é responsável por
gerar o código-objeto. Ou seja, é nessa
etapa que os comandos assembly são
transformados em linguagem de máquina.
O comando para fazer a montagem do
programa é o gcc –c <nome_programa>.c
•Cap 3 – Tipo Abstrato de Dados
Processo de compilação de um programa em c
4. Ligação
Essa é a etapa final, onde ocorre a
combinação de todos os códigos-objetos
que compõem um programa C. O resultado
desta etapa é um código executável.
O comando para fazer a montagem do
programa é o gcc –C <nome_programa>.c
Fazer linkagem: gcc Ponto_Main.c Ponto.o
•Cap 4 – Listas sequenciais
Conceito

Uma Lista linear, ou tabela, é um conjunto


de n > 0 itens x1, x2,…,xn cuja propriedade
estrutural envolve somente as posições
relativas de cada elemento, e os elementos
estão ordenados em função das posições
relativas que ocupam.
•Cap 4 – Listas sequenciais
Conceito

Se n > 0, x1 é o primeiro, x2 o segundo e, xn


o último. Esses elementos possuem as
propriedades:
1- x1 é o início da lista (a cabeça) e possui
único sucessor (o próximo);
2- xn é o último elemento da lista (a cauda)
e não possui sucessor;
•Cap 4 – Listas sequenciais
Conceito

3-Qualquer outro elemento tem um


sucessor (próximo) e um antecessor
(anterior)
•Cap 4 – Listas sequenciais
Conceito
As operações mais frequentes nas listas
lineares são a busca, a inclusão e a remoção
de um determinado elemento. Essas
operações são as mais importantes, porque
estão presentes em todas as aplicações de
processamento de dados, e são as mais
utilizadas. Por esse facto, os algoritmos que
as manipulam devem ser muito eficientes.
•Cap 4 – Listas sequenciais
Conceito
Nas listas lineares não existe uma disciplina
de acesso ao conteúdo de qualquer
elemento, queremos com isso dizer, que
todas as operações sobre essa estrutura de
dados podem ser realizadas no início, no fim
ou no interior, por esse facto, diz-se que a
lista linear tem uma disciplina de acesso
aleatório, ou randómico.
•Cap 4 – Listas sequenciais
Conceito
Contudo, esta estrutura de dados pode ser
implementada de duas formas: em
organização sequencial ou em organização
não sequencial.
•Cap 4 – Listas sequenciais
Conceito
As listas lineares em organização sequencial,
ou listas sequenciais, caracterizam-se por
utilizar o vetor. A quantidade de memória
para armazenar a informação nessa
estrutura de dados e declarada em tempo
de compilação, ou seja, quando o
programador está a desenvolver o seu
código.
•Cap 4 – Listas sequenciais
Conceito
Todas as estruturas de dados que possuem
essa propriedade, são chamadas de
estrutura de dados sequenciais em memória
estática, e na ciência de computação são
usualmente denominadas por Vetores
Estáticos.
•Cap 4 – Listas sequenciais
Conceito
Mas a linguagem C permite que o
programador declare um vetor a quantidade
mínima de elementos que este irá
armazenar. A medida que a aplicação vai
sendo executada, esse vetor poderá crescer
ou decrescer, ou seja, solicitar porções de
memória ou devolver porções de memória
não utilizada a RAM.
•Cap 4 – Listas sequenciais
Conceito
Todas as listas sequenciais que possuem
essa propriedade, são chamadas de
estrutura de dados sequenciais em memória
dinâmica, e na ciência de computação
recebem o nome de Vetores Dinâmicos.
•Cap 4 – Listas sequenciais
Conceito
As listas lineares também podem ser
organizadas em ordem não sequencial. Estas
listas utilizam variáveis do tipo ponteiros é
são vulgarmente chamadas por Listas
Encadeadas.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
Como existem uma infinidade de operações
que manipulam listas lineares, vamos definir
um conjunto de operações fundamentais
que servem de suporte para qualquer
aplicação de gestão.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
Como existem uma infinidade de operações
que manipulam listas lineares, vamos definir
um conjunto de operações fundamentais
que servem de suporte para qualquer
aplicação de gestão.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
• Criar uma lista;
• Limpar a lista;
• Inserir um elemento na lista;
• Remover um elemento da lista;
• Imprimir os elementos de uma lista;
• Verificar se uma lista está vazia;
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
• Verificar se uma lista está cheia;
• Consultar a informação dada uma
posição;
• Consultar a localização de um elemento
dada uma chave.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
Em seguida, vamos detalhar cada operação.
Como cada operação deve ser
implementada como uma função, vamos
descrever o mecanismo de comunicação
com o exterior e as condições de excepção
para sua execução.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
criarLista()
Recebe: nenhuma;
Devolve: lista Vazia;
Pré-Condições: nenhuma.
Pós-Condições: inicializar a estrutura de
dados.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
limparLista()
Recebe: lista, última posição de inserção;
Devolve: lista vazia;
Pré-Condições: lista não pode estar vazia
Pós-Condições: colocar a estrutura de dados
no estado inicial.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
inserirElemento()
Recebe: lista, última posição de inserção,
posição de inserção e o valor a inserir;
Devolve: lista atualizada e o código de erro
da operação;
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
inserirElemento()
Pré-Condições: lista não pode estar cheia e a
posição relativa de inserção deve estar entre
0 e a última posição de inserção mais 1.
Pós-Condições: incluir uma informação no
início, fim ou meio da lista.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
removerElemento()
Recebe: lista, última posição de inserção,
posição de remoção;
Devolve: lista atualizada, valor removido e
código de erro da operação;
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
removerElemento()
Pré-Condições: lista não pode estar vazia e a
posição de remoção deve estar entre 0 e a
última posição do elemento inserido.
Pós-Condições: excluir uma informação em
qualquer posição da lista.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
imprimirElementos()
Recebe: lista e o número de elementos
inseridos;
Devolve: lista atualizada, valor removido e
código de erro da operação;
Pré-Condições: lista não pode estar vazia.
Pós-Condições: mostrar na tela todos os
elementos da lista.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
vaziaLista()
Recebe: número de elementos inseridos;
Devolve: verdadeiro ou falso
Pré-Condições: nenhuma.
Pós-Condições: verificar se a lista está vazia;
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
cheiaLista()
Recebe: número de elementos inseridos;
Devolve: verdadeiro ou falso;
Pré-Condições: nenhuma.
Pós-Condições: verificar se a lista está cheia.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
consultarElemento()
Recebe: lista e número de elementos
inseridos;
Devolve: informação do elemento e código
de erro da operação;
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
consultarElemento()
Pré-Condições: lista não pode estar vazia, a
posição do elemento a ser consultado deve
estar entre 0 e última posição de inserção.
Pós-Condições: extrair a informação de um
determinado elemento da lista.
•Cap 4 – Listas sequenciais
Análise do Tipo Abstrato de Dados
buscarElemento()
Recebe: lista e o elemento a pesquisar
Devolve: posição relativa do elemento que
contêm essa informação ou -1;
Pré-Condições: lista não pode estar vazia;
Pós-Condições: encontrar a primeira
ocorrência de uma determinada chave.
•Cap 4 – Listas sequenciais
Vetores estáticos
Para efeitos de implementação, vamos
considerar que cada elemento da lista
sequencial é composto por dois campos: a
chave que identifica cada registro de forma
única e um valor que não tem qualquer
valor para este estudo.
•Cap 4 – Listas sequenciais
Vetores estáticos
Associado a essa lista, temos uma variável
que dar-nos-á em qualquer instante o
número de elementos inseridos nessa
estrutura. Em termos gráficos.
•Cap 4 – Listas sequenciais
Vetores estáticos
Para implementar as operações sobre uma
lista, definiremos os seguintes códigos de
erro:
•Cap 4 – Listas sequenciais
Vetores estáticos
E utilizaremos a seguinte estrutura de
dados:
•Cap 4 – Listas sequenciais
Vetores estáticos (Especificação do Interface )
•Cap 4 – Listas sequenciais
Vetores estáticos (Especificação do Interface )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Agora vamos implementar no arquivo com
extensão .c, denominado por arquivo de
implementação, as estratégias utilizadas nos
algoritmos que descrevem o funcionamento
das funções, e a função que controla os
erros dessas operações.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Como estamos a implementar uma
biblioteca que fornecerá serviços a
programadores de aplicações, essas funções
deverão ser universais, ou seja, elas deverão
possui a propriedade de devolver a
informação correta para todas a amostra de
dados que for enviada como argumento.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Esta propriedade, faz com que o
programador cliente ou programador de
aplicações, se concentre no
desenvolvimento do esqueleto da aplicação,
deixando os detalhes, em especial a
consistência de sua aplicação, para as
funções dessa biblioteca.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Então, para que essas funções tenham essa
propriedade, elas deverão devolver um
código de erro quando terminaram de
executar uma função. Se esse código for
igual a zero a operação foi bem-sucedida,
caso contrário, ocorreu alguma anomalia
que deverá ser facilmente detetada e
tratada.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Veremos a seguir, os algoritmos que
implementam essas operações com está
metodologia de engenharia de software.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Veremos a seguir, os algoritmos que
implementam essas operações com está
metodologia de engenharia de software.

A operação de criar uma lista sequencial,


consiste em solicitar um espaço de memória
para armazenar essa estrutura
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Ao criar uma lista sequencial, estamos na
verdade a prepara-la para receber dados,
isso quer dizer que nesse instante essa lista
não possui nenhum elemento com
informação válida. Logo, a variável que
controla o número de elementos inseridos,
nElem, deverá receber o valor zero.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A operação para determinar se uma lista sequencial está
vazia, consiste em verificar se ela não possui elementos.
Pelo operador anterior, basta verificar se o número de
elementos inseridos é igual a zero.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A operação para determinar se uma lista sequencial está
cheia, consiste em verificar se todos os campos do vetor
estão preenchidos. Para isso, basta verificar se o número
de elementos inseridos é igual a dimensão do vetor.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )

A operação para consultar a informação de


um elemento numa lista sequencial dado
uma determinada posição é muito simples.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )

Mas devido a propriedade de compactação


dos elementos do vetor, e atendendo ao
facto que essa operação esta contida numa
biblioteca que deve retornar a resposta
certa para qualquer amostra de dados, ela
só deve ser executada se essa posição
(valor) estiver no intervalo que vai de 0 a
nElem-1.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )

Observe que não temos a necessidade de


verificar se a lista está vazia, porque se
estiver, a variável que controla o número de
elementos inseridos, nElem é igual a zeros,
e a função retorna a mensagem de índice
inválido.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A operação para consultar a informação de
um elemento dado uma determinada chave é
um pouco mais complexa. Ela consiste em
procurar pela primeira ocorrência dessa
chave.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Se a chave estiver contida na parte compacta
da lista, a função devolve como valor de
retorno a posição relativa desse elemento,
no caso contrário, retorna o valor -1. A
semelhança da operação anterior, não é
necessário verificar se a lista está vazia. Se o
tiver essa função retorna o valor -1.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Quando temos uma lista sequencial
ordenada, ou seja, as chaves estão
ordenadas numa determinada ordem, é
possível acelerar o processo de busca.
Suponhamos sem perda da generalidade,
que as chaves estão ordenadas em ordem
crescente.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Se a chave que procuramos não está contida
na lista, quando encontrarmos um elemento
na lista cujo conteúdo é menor do que o
valor que procuramos, devemos suspender
esse processo, optimizando desta forma o
algoritmo.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Contudo, essa estratégia não altera a
complexidade do algoritmo anterior. Para
obter um algoritmo com melhor
performance, devemos utilizar uma
estratégia denominada por a busca binária.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A busca binária é o algoritmo de pesquisa
mais sofisticado e mais eficiente que existe.
Contudo, o mesmo só pode ser utilizado
quando a listas estiverem ordenadas.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Suponhamos sem perda da generalidade,
que a lista está ordenada em ordem
crescente. A ideia desse algoritmo consiste
em fragmentar a lista em duas partes. A
busca começa por selecionar uma posição k
no interior da lista, de tal forma que k é o
ponto médio.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Se o elemento encontrado for menor do que
a chave que procuramos, devemos continuar
a pesquisa na parte da lista à direita de k.
Mas se o elemento encontrado for maior do
que a chave que procuramos, devemos
continuar a pesquisa na parte da lista à
esquerda de k.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Executamos esse processo de forma repetida
até encontrarmos o elemento que
procuramos ou gerarmos uma parte vazia e
com isso o processo termina.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A operação para inserir um elemento numa
posição qualquer de uma lista sequencial é
mais complexa. Ela só poderá ser executada
se a lista não estiver cheia e a posição de
inserção estiver contida no intervalo [0…
nElem].
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
O processo de inserção propriamente dito,
consiste em deslocar para à direita todos os
elementos que vão desde o número de
elementos inseridos menos uma unidade até
a posição de inserção. Inserir em seguida o
elemento nessa posição, e atualizar o índice
que faz referência a posição do último
elemento inserido.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A operação para imprimir todos os
elementos de uma lista sequencial é
extremamente simples, mas só faz sentido,
se a lista não estiver vazia.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A operação para remover um elemento de
uma lista sequencial dada uma posição
qualquer é um pouco complexa. Contudo,
ela só pode ser executada se a lista não
estiver vazia e a posição de remoção estiver
contida no intervalo [0..NElem-1].
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
O processo de remoção consiste em guardar
numa variável auxiliar o conteúdo do
elemento que se pretende remover, deslocar
para à esquerda todos os elementos que vão
desde a posição de remoção até a posição do
último elemento inserido, e actualizar em
seguida o índice que faz referência a posição
do último elemento inserido.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A operação para controlar os tipos de erro do
módulo é muito simples. Ela pode ser
descrita pelo seguinte procedimento.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Note que não incluímos nesse procedimento
o código para mostrar a mensagem quando
a operação correu com sucesso.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
A título comparativo, veremos como
implementar uma biblioteca análoga a que
estudamos, mas cujas funções não são
universais. Para essa biblioteca, essas
funções só devem realizar as tarefas para os
quais foram definidas e não devem
preocupar-se com a validação de todas as
condições de excepção.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
Por exemplo: a operação para inserir um
elemento numa posição qualquer de uma
lista sequencial, consiste apenas no processo
de inserção propriamente dito. Como as
funções dessa biblioteca são independentes,
essa função só vai inserir o elemento na
posição que foi passada como argumento, e
retornar ok se for necessário.
•Cap 4 – Listas sequenciais
Vetores estáticos (Implementação das Operações )
•Cap 4 – Listas sequenciais
Vetores Dinâmicos
Como todas as operações que estão definidas
no tipo abstracto de dados lista em memória
estática também estão definidos no tipo
abstracto de dados lista em memória
dinâmica, não iremos tecer qualquer
consideração sobre o conjunto de operações
essências para o funcionamento de uma
aplicação de gestão.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos
Apresentaremos a seguir, os códigos de erro
para o controlo dessas operações.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos
Utilizaremos como estrutura de dados um
vetor dinâmico com a seguinte declaração:
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
Estudaremos em seguida, alguns algoritmos
para manipulação das operações essenciais
vistas para os vetores estáticos.
A operação para criar um vetor dinâmico,
consiste em armazenar inicialmente uma
pequena porção de memória e
redimensionar o vetor quando esse espaço
for esgotado.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
Uma estratégia mais inteligente e muito
utilizada, consiste em deixar para a aplicação
cliente a definição da dimensão inicial do
vetor. Com essa estratégia, o utilizador pode
estimar a dimensão apropriada para correr o
seu programa, tendo como pressuposto a
quantidade de dados que irá processar.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
Sempre que durante a execução de um
programa tivermos a necessidade de
aumentar ou diminuir os recursos de
hardware para armazenar os dados do vetor,
esta função que descrevemos a seguir,
deverá ser invocada, evitando desse modo o
desperdício de memória.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
Agora vamos utilizar essa função para
implementar a operação de inserção de um
elemento numa posição qualquer de uma
lista sequencial em organização dinâmica.
Mas antes disso, devemos analisar as
seguintes situações:
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
Se houver espaço no vetor, esta operação de
inserção é análoga a realizada para a listas
sequenciais em organização estática.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
Se não houver espaço no vetor, é necessário
solicitar a memória principal mais recursos
de memória (blocos de memória). Ao
solicitar esses recursos, temos as seguintes
situações:
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
A memória do computador está
completamente esgotada e a função anterior
retorna um código a notificar tal facto, ou
existe espaço na memória e essa função
anterior é realizado com sucesso. Para este
caso, as linhas de código são análogas as
estudadas no capítulo anterior.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
Nessa função, podemos observar que
sempre que a lista estiver cheia, a função
solicita mais espaço de memória para
armazenar um registro e redimensiona o
vetor para permitir que essa operação seja
realizada com sucesso.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
A operação para remoção de um elemento
dada a sua posição, também merece alguma
atenção. Apesar de ser muito parecida com a
operação estudada para as listas sequenciais
em memória estática, é necessário ajustar o
tamanho da lista a medida que os elementos
forem sendo removidos.
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
•Cap 4 – Listas sequenciais
Vetores Dinâmicos (Implementação das Operações)
Observe mais uma vez que sempre que a
operação de remoção for realizada com
sucesso, um bloco de memória é retirado do
vetor e o seu espaço é redimensionado para
que não haja desperdício de memória.
Como as restantes operações são análogas as
vistas anteriormente, deixamos como
exercício.
•Cap 5 – Listas Encadeadas

A forma mais comum de implementar a


alocação dinâmica consiste no encadea-
mento, onde os elementos são ligados
entre si por ponteiros.
•Cap 5 – Listas Encadeadas

A ordem dos elementos de uma lista, é


determinada por uma informação contida
no próprio elemento, que informa o
endereço físico do próximo elemento,
tornando deste modo, lógica a
continuidade de uma lista encadeada.
•Cap 5 – Listas Encadeadas
Conceito
Uma lista encadeada é um conjunto
finito de elementos que estão
armazenados em posições aleactórias de
memória.
•Cap 5 – Listas Encadeadas
Conceito
Os elementos dessa lista, denominados
por átomos possuem dois campos: info
que contém à informação propriamente
dita e prox que é o campo de ligação
que contém um ponteiro que faz
referencia ao próximo átomo.
•Cap 5 – Listas Encadeadas
Conceito
O término dessa lista é representado
pelo caracter especial, que na linguagem
C recebe o nome de NULL e indica um
endereço nulo.
•Cap 5 – Listas Encadeadas
Conceito
Uma lista encadeada deve possuir
obrigatoriamente um ponteiro, que
denotamos por plista, que aponta para o
primeiro átomo da lista;
•Cap 5 – Listas Encadeadas
Conceito
um encadeamento entre os átomos que
permite percorrer a estrutura, e um
ponteiro nulo no campo prox do último
átomo que permite detetar o seu término.
Em termos gráficos:
•Cap 5 – Listas Encadeadas
Conceito
Como a lista encadeada é uma estrutura
em alocação dinâmica, na sua
declaração, o sistema operativo não
reserva com antecedência um espaço
em memória.
•Cap 5 – Listas Encadeadas
Conceito
Esse espaço vai sendo reservado à
medida que a aplicação vai inserindo
átomos, e libertado à medida que a
aplicação vai removendo os átomos
previamente inseridos.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Como todas as operações que existem
no TAD lista em organização sequencial
também existem no TAD lista em
organização dinâmica, não iremos tecer
qualquer consideração sobre o conjunto
de operações essências para o
funcionamento de uma aplicação de
gestão.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Apresentaremos a seguir, os códigos de
erro para o controlo dessas operações.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Utilizaremos como estrutura de dados
uma lista encadeada com os seguintes
ponteiros: um ponteiro denominado por
primeiro, que aponta para o primeiro
átomo da lista; um encadeamento entre
os átomos que permite percorrer a
estrutura;
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


um ponteiro nulo no campo prox do
último átomo e um ponteiro ultimo que
aponta para o último átomo da lista. Em
termos gráficos:
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Mostraremos a seguir, os algoritmos para
manipular as operações essenciais para
qualquer aplicação de gestão.
A operação para criar uma lista
encadeada, consiste em armazenar nos
ponteiros que fazem referencia ao
primeiro e ao último átomo um endereço
nulo.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Ao associarmos um ponteiro ao inicio e
fim da lista criamos uma lista encadeada.
Como um ponteiro nulo não contêm
nenhum endereço de memória válido,
então
criamos uma lista vazia, logo, estamos
em condições de implementar o próximo
operador.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


A operação para imprimir todos os
átomos de uma lista encadeada consiste
em percorre-la do primeiro ao último
átomo. Para cada átomo imprimir o seu
conteúdo.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Vejamos em seguida o mesmo problema
numa outra perspetiva. De facto, imprimir
todos os átomos de uma lista encadeada
consiste fragmenta-la em dois subproble-
mas do mesmo tipo.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Uma lista encadeada unitária, cuja
solução é trivial, e uma lista encadeada
de menor dimensão. Então, a resolução
desse problema, passa por uma solução
recursiva por indução finita fraca
(decrementar para conquistar) cuja
solução apresentamos em seguida.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


A operação para encontrar uma
determinada informação dado uma chave
é muito simples, e é implementada pela
seguinte função.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


A operação para inserir um átomo no fim
de uma lista consiste em obter dinamica-
mente um espaço para alocar um novo
átomo. Se essa operação não for
realizada com sucesso devolver o
correspondente código de erro.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


No caso contrário, preencher o novo
átomo com a informação e o ponteiro de
fim de lista. Em seguida, ligar o novo
átomo ao fim da lista. Para efetuar essa
ligação, basta colocar como sucessor do
último átomo o novo átomo.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Mas está estratégia apenas funciona se a
lista possui pelo menos um elemento. Se
a lista estiver vazia, temos de associar o
endereço do novo átomo aos ponteiros
primeiro e último.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


A operação para remover um átomo de
uma posição qualquer de uma lista,
torna-se simples, quando dominarmos a
técnica de manipulação do ponteiro que
faz referência ao próximo átomo. O
algoritmo para implementar esta
operação é complexo.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Em termos gerais consiste em verificar
se a lista está vazia. Se essa condição
for verdadeira, devolver o
correspondente código de erro. No caso
contrário, movimentar um ponteiro
auxiliar até ao antecessor desse átomo.
Ligar em seguida, esse átomo ao
sucessor do seu sucessor.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Agora, o nosso problema consiste em
saber como implementar a instrução:
ligar esse átomo ao sucessor do seu
sucessor.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Suponhamos que pant é um ponteiro
que faz referência ao antecessor do
átomo que pretendemos remover e pdel
é um ponteiro que faz referencia ao
átomo que pretendemos remover
(contêm a chave). Ligar o átomo ao
sucessor do sucessor, consiste em
executar o comando: pant->prox = pdel->prox;
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


Contudo se a chave que pretendemos
remover encontra-se no primeiro ou
último átomo da lista teremos de tratar
essa operação como um caso especial.
•Cap 5 – Listas Encadeadas

Listas Encadeadas Simples


•Cap 5 – Listas Encadeadas

Exercícios
1. Desenvolva uma função iterativa e uma recursiva que devolva o
número de átomos de uma lista encadeada simples.

2. Desenvolva uma função iterativa que recebe como argumento uma


lista ligada simples, uma determinada informação e uma posição. Insira
um novo átomo com essa informação na posição escolhida.

3. Desenvolva uma função iterativa e uma função recursiva que recebe


como argumento uma lista ligada simples ordenada e uma determinada
chave. Verifique se essa chave encontra-se na lista.

Você também pode gostar