Índice
i
Índice
Capítulo 2 – Estrutura de Dados sequencial com
armazenamento sequencial
1. A Estrutura Abstrata de Dados “Lista” ................................................... 1
1.1. Definição .................................................................................. 1
1.2. Implementação de Listas utilizando armazenamento sequencial ................. 2
1.3. Implementação de Listas utilizando armazenamento não sequencial ............ 3
2. Listas - armazenamento sequencial usando memória estática ...................... 3
2.1. Introdução ................................................................................. 3
2.2. Declaração ................................................................................ 5
2.3. Criar um elemento ....................................................................... 6
2.4. Criar uma lista ............................................................................ 6
2.5. Lista vazia e lista cheia ................................................................. 7
2.6. Inserir um elemento numa lista ........................................................ 8
2.6.1. Inserir no início ..................................................................... 8
2.6.2. Inserir no fim ........................................................................ 9
2.6.3. Inserir por ordem .................................................................. 10
2.7. Remover um elemento duma lista .................................................... 10
2.8. Listar/mostrar um elemento ou os elementos duma lista ........................ 12
2.9. Destruir uma lista ....................................................................... 13
Índice
ii
Índice
3. Listas - armazenamento sequencial usando memória dinâmica ................... 14
3.1. Introdução ................................................................................ 14
3.2. Declaração................................................................................ 16
3.3. Criar um elemento ...................................................................... 16
3.4. Criar uma lista ........................................................................... 17
3.5. Lista vazia e lista cheia................................................................. 18
3.6. Inserir um elemento numa lista ....................................................... 19
3.6.1. Inserir no início .................................................................... 19
3.6.2. Inserir no fim ....................................................................... 20
3.6.3. Inserir por ordem .................................................................. 20
3.7. Remover um elemento duma lista .................................................... 21
3.8. Listar/mostrar um elemento ou os elementos duma lista ......................... 23
3.9. Destruir uma lista ....................................................................... 24
4. Ordenação duma lista ...................................................................... 25
4.1. Seleção Linear ........................................................................... 25
4.2. Bubblesort ................................................................................ 26
4.3. Quicksort ................................................................................. 28
4.4. Por Inserção .............................................................................. 30
5. Pesquisa dum elemento numa lista ..................................................... 30
5.1. Pesquisa exaustiva ...................................................................... 30
5.2. Pesquisa Sequencial ..................................................................... 31
5.3. Pesquisa Binária ......................................................................... 32
Índice
A Estrutura Abstrata de Dados “Lista”
1
Capítulo 2 – Estrutura de Dados sequencial com
armazenamento sequencial
1. A Estrutura Abstrata de Dados “Lista”
1.1. Definição
Uma EAD Lista é uma sequência linear de tipos de dados do mesmo tipo com as
seguintes propriedades:
1. Existe um primeiro elemento (a cabeça) com um único sucessor (o próximo);
2. Existe um último elemento (a cauda) sem sucessor;
3. Qualquer outro elemento tem um sucessor (próximo) e um antecessor (anterior);
sobre a qual se realiza as seguintes operações:
1. Criar um elemento
2. Criar uma lista
3. Verificar se uma lista está vazia (não tem elementos)
4. Procurar um elemento
5. Procurar o antecessor de um elemento
6. Remover/eliminar um elemento
7. Inserir um elemento atrás de um elemento específico
8. Inserir um elemento à frente de um elemento específico
9. Recuperar um elemento
10. Atualizar um elemento
11. Ordenar uma lista
12. Imprimir/listar uma lista
13. Determinar a quantidade de elementos da lista (tamanho/comprimento)
14. Eliminar/destruir uma lista
15. Verificar se existe espaço para outro elemento
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
2
A Estrutura Abstrata de Dados “Lista”
Os tipos de dados podem ser considerados muito abstratos. A regra principal na
estrutura da EAD Lista é utilizar ponteiros. Os elementos numa lista estão sempre ligados
pela sua relação sucessor-antecessor.
Uma EAD Lista pode ser implementada num espaço de armazenamento sequencial ou
num espaço de armazenamento não sequencial. No caso da implementação num espaço
sequencial podem-se usar array's, os quais podem ser declarados usando memória estática
ou memória dinâmica. No caso do armazenamento não sequencial será usado ponteiros e
memória dinâmica.
1.2. Implementação de Listas utilizando armazenamento sequencial
Ao
implementar
uma
estrutura
de
dados
num
espaço
de
armazenamento
sequencial/contínuo, todos os dados na estrutura são mantidos num array. Os array's
podem ser declarados com um tamanho que é fixado quando o programa é escrito, não
podendo ser alterado enquanto o programa está a ser executado, ou então usando memória
dinâmica em que o tamanho não é fixado quando o programa é escrito e vai sendo alterado
à medida que o programa vai sendo executado, consoante as necessidades de espaço de
armazenamento.
No primeiro caso, quando se está a escrever um programa, tem que se decidir qual é
o limite máximo de memória que vai ser necessário para os array's e estabelecer este
limite nas declarações. Se o programa for executado com um pequeno conjunto de dados,
então muito deste espaço nunca vai ser utilizado. Se pelo contrário, o programa for
executado com um grande conjunto de dados, então pode-se esgotar o espaço estabelecido
na declaração e encontrar uma situação de overflow. Isto pode ocorrer mesmo no caso da
memória do computador não estar totalmente usada, devido aos limites iniciais do array
serem muito pequenos.
No segundo caso, apenas é necessário declarar um apontador/ponteiro para uma
variável do mesmo tipo dos elementos da lista, sendo que a lista vai crescendo em
tamanho à medida das necessidades de inserção de elementos na lista. Esta estratégica
tem como grande problema a possibilidade de inexistência um bloco de memória contígua
necessário para receber todos os elementos da lista.
As dificuldades anteriores podem ser ultrapassadas usando outra forma de manter as
estruturas de dados na memória sem usar array's, como é o caso das listas ligadas.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória estática
3
No tipo de implementação de Listas usando array's, a definição de cada um dos seus
elementos consiste nos seguintes campos:
1. Dados;
2. Chave (opcional) − campo pelo qual a lista está ordenada;
3. Localização (índice) na lista.
Numa lista L:
− o elemento de índice 0 (primeira posição – L[0]) não tem antecessor;
− o elemento de índice N-1 (última posição – L[N-1]) não tem sucessor;
− o elemento da lista com índice k tem:
- o elemento de índice k-1 como seu antecessor,
- o elemento de índice k+1 como seu sucessor.
1.3. Implementação de Listas utilizando armazenamento não sequencial
Uma Lista implementada num espaço de armazenamento não sequencial, a memória
de cada elemento é reservada individualmente, usando ponteiros e alocação de memória, e
o seu tamanho é atualizado à medida que vão sendo inseridos ou removidos elementos da
Lista. Neste tipo de implementação enquadram-se os conceitos de Lista ligada, usando
ligações simples, duplas ou circulares.
2. Listas - armazenamento sequencial usando memória estática
2.1. Introdução
Na implementação de Listas utilizando armazenamento sequencial em memória
estática, todos os elementos da Lista são mantidos num array (estático), cujo tamanho
máximo é fixado quando o programa é escrito e não pode ser alterado enquanto o
programa está a ser executado. Quando se está a escrever um programa, tem que se
decidir qual é o limite máximo de memória que vai ser necessário para os array's e
estabelecer este limite nas declarações (tamanho máximo), o que implica que o número
de elementos da lista (tamanho efetivo) não pode exceder aquele valor. Se o programa
for executado com um pequeno conjunto de dados (tamanho efetivo muito inferior ao
tamanho máximo), então muito deste espaço nunca vai ser utilizado. Se pelo contrário, o
programa for executado com um grande conjunto de dados, então pode-se esgotar o
espaço estabelecido na declaração (tamanho efetivo superior ao tamanho máximo) e
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
4
Listas - armazenamento sequencial usando memória estática
encontrar uma situação de overflow. Isto pode ocorrer mesmo no caso da memória do
computador não estar totalmente usada, devido ao limite inicial do array (tamanho
máximo) sere muito baixo.
É importante fazer uma distinção entre estes dois conceitos (tamanho efetivo e
tamanho máximo). Numa lista de N (tamanho efetivo) elementos, pode ser inserido ou
eliminado um elemento, donde, se N = 3 então a lista contém somente 3 elementos
(tamanho efetivo igual a 3), se N = 19 então a lista contém 19 elementos (tamanho efetivo
igual a 3) − o tamanho da lista é variável. De qualquer forma, o valor de N (tamanho
efetivo) não pode ser superior ao valor do tamanho máximo da lista; caso isto não
aconteça, está-se perante uma situação de overflow.
Uma lista é, por definição, uma estrutura dinâmica de dados porque o seu tamanho
pode variar. Um array estático é uma estrutura de dados fixa porque o seu tamanho é
fixado quando o programa é compilado. Os conceitos de lista e array estático estão
relacionados uma vez que uma lista de tamanho variável pode ser implementada usando
um array com tamanho fixo (com algumas posições do array não sendo utilizadas).
Existem diferentes formas de implementar uma lista e não se deve confundir decisões
de implementação com decisões de escolha e especificação da estrutura de dados.
Exemplo: uma lista com no máximo 100 elementos (valores inteiros).
#define TamMax 100;
int Lista[TamMax], N, k;
N = 50;
for (k = 0; k < N; k++)
scanf(“%d”, &Lista[k]);
0
1
2
...
48
49
9
14
18
...
27
35
50
...
98
99
98
99
...
N = 100;
for (k = 50; k < N; k++)
scanf(“%d”, &Lista[k]);
0
1
2
...
48
49
50
...
9
14
18
...
27
35
51
...
176 185
N = 102;
for (k = 100; k < N; k++)
scanf(“%d”, &Lista[k]);
// ERRO: Não existem os elementos Lista[100] e Lista[101] (o maior índice é 99) − overflow
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória estática
5
Características fundamentais:
Tamanho máximo: constante (= TamMax) ⇒ ineficiência
Tamanho efetivo: necessário atualizar e testar eventual transbordo (> TamMax)
Acesso: direto
Reordenamento ⇒ trocas (ou vetor auxiliar)
Inserção (e remoção) de elementos ⇒ deslocamento de todos os elementos seguintes
2.2. Declaração
Os elementos de uma lista podem ser declarados (definidos) como um tipo qualquer
simples (por exemplo, inteiro, real ou carácter) ou estruturado (por exemplo, registo).
Como se disse, uma lista pode ser definida como um vetor (array). Portanto, antes de se
definir uma lista tem que se definir os elementos da lista.
Considere-se, por exemplo, as seguintes definições em linguagem C:
#define TamMax 100;
typedef struct {
char Nome[50], Morada[50];
int
Idade, BI;
char Sexo[1]; // Masculino(M/m) ou Feminino(F/f)
} ELEMENTO;
ELEMENTO Lista[TamMax];
Nesta lista, cada um dos elementos consiste no seguinte:
Dados − informação associada (Nome, Morada, Idade, BI e Sexo)
Chave − campo que servirá de base para ordenar a lista (por exemplo, o Nome)
Localização − é o índice que lhe está associado (entre 0 e TamMax-1).
Para controlar o tamanho da lista, utiliza-se uma variável global do tipo inteiro.
Considere-se, por exemplo, a seguinte declaração em linguagem C:
int N;
{ número de elementos (tamanho efetivo) da lista }
Nos capítulos seguintes, sempre que se referir à ordenação da lista, é suposto ser por
ordem crescente do campo Chave, se nada se disser em contrário.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
6
Listas - armazenamento sequencial usando memória estática
2.3. Criar um elemento
Quando se pretende inserir um novo elemento numa lista, este deve ser criado antes;
isto é, atribuir valores a todos os campos da estrutura que suporta a informação associada
a cada elemento da lista. Um possível algoritmo para criar um elemento é o seguinte:
Dados de entrada: um elemento E (sem informação)
Dados de saída: o elemento E (preenchido com a informação desejada)
Algoritmo:
Para cada campo do elemento E fazer
Pedir informação
Introduzir informação
Uma função que traduz o algoritmo é a que se segue, a qual recebe um elemento E
sem informação e devolve o elemento E com a informação desejada introduzida.
void CriarElemento (ELEMENTO *E) {
printf(“Insira o nome : \n“);
fflush(stdin);
// para esvaziar o buffer do teclado
gets(E->Nome);
printf(“Insira a morada : \n“);
fflush(stdin);
gets(E->Morada);
printf(“Insira a idade : \n“);
scanf(“%d”, &(E->Idade));
printf(“Insira o BI : \n“);
scanf(“%d”, &(E->BI));
printf(“Insira o sexo (M/m; F/f) : \n“);
fflush(stdin);
gets(E->Sexo);
}
Ordem complexidade (pior caso): O(0). Prova: total de operações = 13.
2.4. Criar uma lista
Quando se pretende criar uma lista, isto deve ser realizado com a introdução de um
elemento (que será o primeiro). Um possível algoritmo para criar uma lista é o seguinte:
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória estática
7
Dados de entrada: o tamanho da lista (que é 0)
Dados de saída: a lista L com um só elemento e o seu tamanho (que é 1)
Algoritmo:
Criar um novo elemento
Inserir o novo elemento na posição 1 (índice 0) da lista L (L[0])
Atualizar o tamanho da lista, atribuindo o valor 1 a N
Uma função que traduz o algoritmo é a que se segue, a qual recebe o tamanho da
lista (N = 0) e devolve a lista L com um elemento e o seu tamanho a valer 1.
void CriarLista (ELEMENTO L[], int *N) {
ELEMENTO E;
CriarElemento(&E);
L[0] = E;
*N = 1;
}
Ordem complexidade (pior caso): O(0). Prova: total de operações = 13 (CriarElemento) + 2.
2.5. Lista vazia e lista cheia
Uma lista está vazia quando não tem qualquer elemento e, está cheia quando não
pode receber mais elementos. Para se verificar se a lista está vazia ou cheia, basta analisar
o valor da variável associada ao tamanho da Lista (TAM), da seguinte forma:
N=0
⇒ lista vazia
N = TamMax
⇒ lista cheia
Um possível algoritmo para verificar se uma lista está vazia é o seguinte:
Dados de entrada: uma lista L e o tamanho da lista (N)
Dados de saída: 1 (se a lista L está vazia) e 0 (se a lista L não está vazia)
Algoritmo:
Se N = 0 Então
Devolver 1 (a lista L está vazia)
Senão
Devolver 0 (a lista L não está vazia)
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
8
Listas - armazenamento sequencial usando memória estática
Uma função que traduz o algoritmo é a que se segue, a qual recebe o tamanho de
uma lista (N) e, devolve 0 ou 1 consoante a lista L está vazia ou não.
int ListaVazia (int N) {
if (N == 0)
return 1;
return 0;
// a lista está vazia
// a lista não está vazia
}
Ordem complexidade (pior caso): O(0). Prova: C = 0 e A = 1.
Um possível algoritmo para verificar se uma lista está cheia é o seguinte:
Dados de entrada: uma lista L e o tamanho da lista (N)
Dados de saída: 1 (se a lista L está cheia) e 0 (se a lista L não está cheia)
Algoritmo:
Se N = TamMax
Então devolver 1 (a lista L está cheia)
Senão devolver 0 (a lista L não está cheia)
Uma função que traduz o algoritmo é a que se segue, a qual recebe uma lista (L) e o
seu tamanho (N) e, devolve 0 ou 1 consoante a lista L está cheia ou não.
int ListaCheia (ELEMENTO L[], int N) {
if (N == TamMax)
return 1;
return 0;
// a lista L está cheia
// a lista L não está cheia
}
Ordem complexidade (pior caso): O(0). Prova: C = 0 e A = 1.
2.6. Inserir um elemento numa lista
Na resolução deste problema tem de ser analisadas três situações distintas, consoante
a forma como se pretende inserir o elemento: no início, no fim ou com a lista ordenada. No
entanto, esta operação só deve ser realizada caso a lista não esteja cheia.
2.6.1. Inserir no início
Neste caso tem que se fazer avançar uma posição a todos os elementos para que a
posição 1 (índice 0) fique liberta, para que esta possa receber o novo elemento. Um
possível algoritmo é o que se segue.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória estática
9
Dados de entrada: o elemento X, a lista L e o tamanho da lista (N)
Dados de saída: a lista L e o seu tamanho (N) atualizados
Algoritmo:
Avançar uma posição todos os elementos da lista L
Inserir X na posição 1 (índice 0)
Atualizar o tamanho da lista, incrementando um valor a N
Uma função que traduz o algoritmo é a que se segue, que recebe o elemento a inserir
(X), uma lista (L) e o tamanho da lista (*N) e, devolve a lista e o seu tamanho atualizados.
void InserirInicio (ELEMENTO X, ELEMENTO L[], int *N) {
int k;
*N = *N + 1;
// atualizar o tamanho da lista, incrementando um valor
for (k = *N-1; k > 0; k--)
L[k] = L[k-1];
L[0] = X;
// avançar todos os elementos da lista uma posição
// colocar o elemento X na 1ª posição (índice 0)
}
Ordem complexidade (pior caso): O(N). Prova: C = 0 e A = (N−1) + 2 = N + 1; C + A = N + 1.
2.6.2. Inserir no fim
Aqui apenas é necessário introduzir o novo elemento na posição seguinte à do último
elemento da lista, passando a ser este novo elemento o último da lista. Um possível
algoritmo é o que se segue.
Dados de entrada: o elemento X, a lista L e o tamanho da lista (N)
Dados de saída: a lista L e o seu tamanho (N) atualizados
Algoritmo:
Inserir X na posição N+1 (uma posição à frente do último elemento da lista)
Atualizar o tamanho da lista, incrementando um valor a N
Uma função que traduz o algoritmo é a que se segue, que recebe o elemento a inserir
(X), uma lista (L) e o seu tamanho (*N) e, devolve a lista L e o seu tamanho atualizados.
void InserirFim (ELEMENTO X, ELEMENTO L[], int *N) {
*N = *N + 1;
// atualizar o tamanho da lista, incrementando um valor
L[*N-1] = X;
// colocar o elemento X na última posição (*N-1)
}
Ordem complexidade (pior caso): O(0). Prova: C = 0 e A = 2.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
10
Listas - armazenamento sequencial usando memória estática
2.6.3. Inserir por ordem
Este é o caso mais utilizado, começando-se por pesquisar a posição onde inserir o
elemento, para depois se fazer avançar uma posição todos os elementos que se encontram
depois da posição pesquisada, para que esta posição fique liberta para receber o novo
valor. Um possível algoritmo é o que se segue.
Dados de entrada: o elemento X, a lista L e o tamanho da lista (N)
Dados de saída: a lista L e o seu tamanho atualizados
Algoritmo: (lista ordenada crescentemente)
Determinar a posição k onde inserir o elemento X
Avançar uma posição todos os elementos da lista L que estão depois da posição k
Inserir X na posição k
Atualizar o tamanho da lista, incrementando N em um valor
A função que traduz o algoritmo é a que se segue, a qual recebe o elemento X a
inserir, a lista L e o tamanho da lista (*N), e devolve a lista e o seu tamanho atualizados.
void InserirOrdem (ELEMENTO X, ELEMENTO L[], int *N) {
int k, pos = 0;
while (pos < *N && L[pos].BI < X.BI)
pos = pos + 1; // o elemento X deve ser inserido na posição pos da lista L
*N = *N + 1;
// atualizar o tamanho da lista, incrementando um valor
for (k = *N-1; k > pos; k--)
L[k] = L[k-1];
L[pos] = X;
// avançar uma posição os elementos de L depois de pos
// colocar o elemento X na posição pos
}
Ordem complexidade (pior caso): O(N). Prova: C = N e A = 1 + k + (N−k) + 2 = 3 + N;
C + A = N + 3 + N = 3 + 2 x N.
2.7. Remover um elemento duma lista
Este problema consiste em remover (eliminar) um dado elemento de uma lista, a qual
pode ou não estar ordenada. No entanto, apesar destas duas situações serem diferentes,
em termos algorítmicos essa diferença encontra-se apenas na forma de pesquisar a posição
do elemento a remover. Para tal, basta apenas aplicar um dos métodos existentes para o
efeito (ver Pesquisa exaustiva − pág. 30), o que mais se adequar à lista: ordenada ou não
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória estática
11
ordenada. No entanto, esta operação só deve ser realizada caso a lista não esteja vazia.
Desta forma, um possível algoritmo é o que se segue.
Dados de entrada: o elemento X, uma lista L e o tamanho da lista (N)
Dados de saída: a lista L e o seu tamanho (N) atualizados
Algoritmo:
k ← posição do elemento X na lista L (utilizar um dos métodos de pesquisa)
Recuar uma posição todos os elementos posicionados depois da posição k
Atualizar o tamanho da lista, decrementando N em um valor
Uma função que traduz o algoritmo é a que se segue, a qual recebe o elemento a
remover (X), uma lista ordenada (L) e o tamanho da lista (*N) e, devolve a lista e o seu
tamanho atualizados.
int Remover (ELEMENTO X, ELEMENTO L[], int *N) {
int k, pos;
pos = PesquisaExaustiva(X, L, *N);
if (pos == -1)
// o elemento a remover, X, não pertence à lista L
return 0;
// para indicar que o elemento X não foi removido
for (k = pos; k < *N-1; k++)
L[k] = L[k+1];
*N = *N – 1;
// atualizar o tamanho da lista, decrementando um valor
return 1;
// para indicar que o elemento X foi removido
}
No entanto, a forma mais comum de tratar este problema não é fornecer o elemento
a remover, mas sim apenas a posição daquele elemento, sendo o trabalho de pesquisa da
posição do referido elemento, assim como a verificação se aquele elemento pertence ou
não à lista, feito antes da remoção. Isto é, antes de se remover um elemento duma lista,
verifica-se se aquele elemento pertence à lista e, caso pertença, em que posição se
encontra, o que se pode fazer recorrendo a um dos métodos de pesquisa existentes (ver
Pesquisa dum elemento numa lista, pág. 30). Um possível algoritmo é o que se segue.
Dados de entrada: a posição (Pos) do elemento a remover, a lista L e o seu tamanho (N)
Dados de saída: a lista L e o seu tamanho (N) atualizados
Algoritmo:
Recuar uma posição todos os elementos que se encontram em posição após Pos
Atualizar o tamanho da lista, decrementando N em um valor
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
12
Listas - armazenamento sequencial usando memória estática
Uma função que traduz o algoritmo é a que se segue, a qual recebe a posição do
elemento a remover (Pos), uma lista (L) e o tamanho da lista (N) e, devolve a lista L e o
seu tamanho atualizados.
void Remover (int Pos, ELEMENTO L[], int *N) {
int k;
for (k = Pos; k < *N-1; k++)
L[k] = L[k+1];
// recuar uma posição os elementos desde k
*N = *N – 1;
// atualizar o tamanho da lista, decrementando a *N um valor
}
Ordem complexidade (pior caso): O(N). Prova: C = 0 e A = (N - 1) + 1 = N; C + A = N.
2.8. Listar/mostrar um elemento ou os elementos duma lista
Uma das operações muito úteis é aquela que permite mostrar um ou todos os
elementos de uma lista. Para mostrar apenas um elemento, um possível algoritmo é o
seguinte:
Dados de entrada: um elemento E
Dados de saída: nada (apenas mostra no écran os dados do elemento dado)
Algoritmo:
Para cada campo do elemento E fazer
Mostrar informação
Uma função que traduz o algoritmo é a que se segue, a qual recebe um elemento E e
mostra no écran a informação que lhe está associada.
void MostrarElemento (ELEMENTO E) {
printf(“Nome: “);
puts(E.Nome);
printf(“Morada: “);
puts(E.Morada);
printf(“Idade: %d\n”, E.Idade);
printf(“BI: %d\n”, E.BI);
printf(“Sexo: “);
puts(E.Sexo);
printf(“\n”);
}
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória estática
13
Para mostrar todos os elementos de uma lista, um possível algoritmo é o seguinte:
Dados de entrada: uma lista L e o seu tamanho N
Dados de saída: nada (apenas mostra no écran os dados dos elementos da lista L)
Algoritmo:
Para cada elemento k da lista L fazer
Mostrar o elemento da posição/índice k
Uma função que traduz o algoritmo é a que se segue, a qual recebe uma lista L e o
seu tamanho N e mostra no écran a informação que está associada a cada elemento de L.
void MostrarLista (ELEMENTO L[], int N) {
int k;
for (k = 0; k < N; k++) {
printf(“L[%d]: \n”, k);
MostrarElemento(L[k]);
printf(“\n”);
}
}
2.9. Destruir uma lista
Quando se pretende destruir uma lista, basta atribuir ao seu tamanho o valor nulo
(N = 0). Um possível algoritmo para criar uma lista é o seguinte:
Dados de entrada: o tamanho da lista (N)
Dados de saída: o valor do tamanho da lista a nulo (N = 0)
Algoritmo:
Atualizar ao tamanho da lista o valor 0 (N = 0)
Uma função que traduz o algoritmo é a que se segue, a qual recebe o tamanho da
lista (N = 0) e devolve a lista L com um elemento e o seu tamanho a valer 1.
void DestruirLista (int *N) {
*N = 0;
}
Ordem complexidade (pior caso): O(0).
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
14
Listas - armazenamento sequencial usando memória dinâmica
3. Listas - armazenamento sequencial usando memória dinâmica
3.1. Introdução
No caso da utilização de armazenamento sequencial com memória dinâmica, todos os
elementos da Lista são mantidos num array (array dinâmico), cujo tamanho não é fixado
quando o programa é escrito e vai sendo alterado à medida que o programa vai sendo
executado, consoante as alterações que sofrerá. Quando se está a escrever um programa,
apenas terá que se definir uma variável do tipo apontador para um elemento do tipo de
dados da Lista, assim como uma variável para guardar o seu tamanho. Ou seja, no início
apenas se declara um ponteiro que receberá o endereço do primeiro elemento do array (ou
Lista), mas que no início não tem elementos (tamanho nulo). Desta forma, a memória
necessária para armazenar cada elemento do array será reservada à medida que um novo
elemento é inserido na Lista e libertada à medida que os elementos são removidos, sendo
que os elementos são inseridos sequencialmente na memória. No entanto, e no que
respeita às restantes operações a efetuar sobre um array dinâmico (ou Lista), estas são
efetuadas exatamente da mesma forma como é feita sobre um array estático.
Numa lista cada um dos elementos consiste no seguinte:
1. Dados;
2. Chave (opcional) − campo pelo qual a lista está ordenada;
3. Localização (índice) na lista.
Numa lista L:
− O elemento de índice 0 (primeira posição – L[0]) não tem antecessor;
− O elemento de índice N-1 (última posição – L[N-1]) não tem sucessor;
− O elemento da lista com índice k tem:
- o elemento de índice k-1 como seu antecessor,
- o elemento de índice k+1 como seu sucessor.
Numa lista de N elementos, pode ser inserido ou eliminado um elemento, donde, se
N = 3 então a lista contém somente 3 elementos, se N = 19 então a lista contém 19
elementos − o tamanho da lista é variável.
Uma lista é, por definição, uma estrutura dinâmica de dados porque o seu tamanho
pode variar. Um array dinâmico é também uma estrutura de dados variável porque o seu
tamanho é atualizado à medida que o programa vai sendo executado.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória dinâmica
15
Os conceitos de lista e array dinâmico estão relacionados uma vez que uma lista de
tamanho variável pode ser implementada usando um array com tamanho variável,
aumentando ou diminuindo de tamanho à medida que novos elementos são inseridos ou
elementos são removidos.
Existem diferentes formas de implementar uma lista e não se deve confundir decisões
de implementação com decisões de escolha e especificação da estrutura de dados.
Exemplo: uma lista com 50 elementos (valores inteiros) numa primeira fase, que
passa para 100 elementos numa segunda fase.
int *Lista, N, k;
N = 50;
Lista = (int *) malloc (N*sizeof(int));
for (k = 0; k < N; k++)
scanf(“%d”, &Lista[k]);
0
1
2
...
48
49
9
14
18
...
27
35
N = 100;
Lista = (int *) realloc (Lista, N*sizeof(int));
for (k = 50; k < N; k++)
scanf(“%d”, &Lista[k]);
0
1
2
...
48
49
50
...
9
14
18
...
27
35
51
...
98
99
176 185
N = 102;
Lista = (int *) realloc (Lista, N*sizeof(int));
for (k = 100; k < N; k++)
scanf(“%d”, &Lista[k]);
0
1
2
...
48
49
50
...
9
14
18
...
27
35
51
...
98
99
100
101
176 185 245 267
Características fundamentais:
Tamanho máximo: variável ⇒ eficiência
Tamanho atual: necessário atualizar (testar disponibilidade de memória antes de
aumentar o tamanho − para inserir novos elementos).
Acesso: direto
Reordenamento ⇒ trocas (ou vetor auxiliar)
Inserção/Remoção de elementos ⇒ deslocamento de todos os elementos seguintes
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
16
Listas - armazenamento sequencial usando memória dinâmica
3.2. Declaração
Os elementos de uma lista podem ser definidos como um tipo qualquer simples (por
exemplo, inteiro, real ou carácter) ou estruturado (por exemplo, registo). Como se disse,
uma lista pode ser definida como um vetor (array). Portanto, antes de se definir uma lista
tem que se definir os elementos da lista.
Considere-se, por exemplo, as seguintes definições em linguagem C:
typedef struct {
char Nome[50], Morada[50];
int
Idade, BI;
char Sexo[1]; // Masculino(M/m) ou Feminino(F/f)
} ELEMENTO;
ELEMENTO *Lista;
int N;
Nesta lista, cada um dos elementos consiste no seguinte:
1. Dados − informação associada (Nome, Morada, Idade, BI e Sexo)
2. Chave − campo que servirá de base para ordenar a lista (por exemplo, o Nome)
3. N − número de elementos (tamanho) da Lista
4. Localização − é o índice que lhe está associado (entre 0 e N-1).
Nos capítulos seguintes, sempre que se referir à ordenação da lista, é suposto ser por
ordem crescente do campo Chave, se nada se disser em contrário.
3.3. Criar um elemento
Quando se pretende inserir um novo elemento numa lista, este deve ser criado antes;
isto é, atribuir valores a todos os campos da estrutura que suporta a informação associada
a cada elemento da lista. Um possível algoritmo para criar um elemento é o seguinte:
Dados de entrada: nada
Dados de saída: retorna o endereço de um elemento E (criado e preenchido com a
informação desejada localmente, usando alocação de memória)
Algoritmo:
Para cada campo do elemento E fazer
Pedir informação
Introduzir informação
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória dinâmica
17
Uma função que traduz o algoritmo é a que se segue (a função apresentada no
capítulo anterior para o mesmo efeito também serve), a qual recebe um elemento E sem
informação e devolve o elemento E com a informação desejada introduzida.
ELEMENTO *CriarElemento {
ELEMENTO *E;
E = (ELEMENTO *) malloc(sizeof(ELEMENTO));
if (E == NULL)
return NULL;
printf(“Insira o nome : \n“);
fflush(stdin);
gets(E->Nome);
printf(“Insira a morada : \n“);
fflush(stdin);
gets(E->Morada);
printf(“Insira a idade : \n“);
scanf(“%d”, &(E->Idade));
printf(“Insira o BI : \n“);
scanf(“%d”, &(E->BI));
printf(“Insira o sexo (M/m; F/f) : \n“);
fflush(stdin);
gets(E->Sexo);
return E;
}
Ordem complexidade (pior caso): O(0).
3.4. Criar uma lista
Quando se pretende criar uma lista, isto deve ser realizado com a introdução de um
elemento (que será o primeiro). Um possível algoritmo para criar uma lista é o seguinte:
Dados de entrada: o tamanho da lista (que é o)
Dados de saída: a lista L com um só elemento e o seu tamanho (que é 1)
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
18
Listas - armazenamento sequencial usando memória dinâmica
Algoritmo:
Criar um novo elemento
Inserir o novo elemento na posição 1 (índice 0) da lista L (L[0])
Atualizar o tamanho da lista, atribuindo o valor 1 a N
Uma função que traduz o algoritmo é a que se segue, a qual recebe o tamanho da
lista (N = 0) e devolve a lista L com um elemento e o seu tamanho a valer 1.
ELEMENTO *CriarLista (int *N) {
ELEMENTO *L, *E;
E = CriarElemento();
L = (ELEMENTO *) malloc(sizeof(ELEMENTO));
if (L == NULL)
return NULL;
L[0] = *E;
*N = 1;
free(E);
return L;
}
Ordem complexidade (pior caso): O(0).
3.5. Lista vazia e lista cheia
Uma lista está vazia quando não tem qualquer elemento e, está cheia quando não
existe memória que permita acrescentar mais elementos à lista L.
Para se verificar se uma lista está vazia, basta analisar o valor da variável N (se é
nulo) ou o endereço contido em L (se for NULL, L está vazia), da seguinte forma:
if (N == 0) ou if (L == NULL)
Uma lista está “cheia” se a alocação de memória para mais um elemento não for
conseguida; ou seja:
V = (ELEMENTO *) realloc ((N+1) * sizeof(ELEMENTO))
if (V == NULL)
return 1; // realocação de memória não conseguida (lista cheia)
Uma função para verificar se uma lista está vazia, pode ser um dos dois que se
seguem: o primeiro recebe uma lista (L) e o segundo recebe o tamanho de uma lista (N), e
ambos devolvem o valor 1 (lista vazia) ou 0 (lista não vazia).
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória dinâmica
19
int ListaVazia1 (ELEMENTO L[]) {
if (L == NULL)
return 1;
return 0;
// lista vazia
// lista não vazia
}
int ListaVazia2 (int N) {
if (N == 0)
return 1;
return 0;
// lista vazia
// lista não vazia
}
3.6. Inserir um elemento numa lista
Na resolução deste problema tem de ser analisadas três situações distintas, consoante
a forma como se pretende inserir o elemento: no início, no fim ou com a lista ordenada.
3.6.1. Inserir no início
Começa-se por avançar uma posição a todos os elementos da lista, para que a 1ª
posição fique livre para receber o novo elemento. Um possível algoritmo é o que se segue.
Dados de entrada: o elemento X, a lista L e o tamanho da lista (N)
Dados de saída: a lista L e o seu tamanho (N) atualizados
Algoritmo:
Realocar memória para mais um elemento
Avançar uma posição todos os elementos da lista L
Inserir X na posição 1 (índice 0)
Atualizar o tamanho da lista, incrementando um valor a N
Uma função que traduz o algoritmo é a que se segue, que recebe o elemento a inserir
(X), uma lista (L) e o seu tamanho (*N) e, devolve a lista L e o seu tamanho atualizados.
ELEMENTO *InserirInicio (ELEMENTO X, ELEMENTO L[], int *N) {
int k;
L = (ELEMENTO *) realloc(L, (*N+1) * sizeof(ELEMENTO));
if (L == NULL)
return NULL;
// impossível alocar memória para um novo elemento
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
20
Listas - armazenamento sequencial usando memória dinâmica
*N = *N+1;
// atualizar o tamanho da lista, incrementando um valor
for (k = *N-1; k > 0; k--)
L[k] = L[k-1];
// avançar todos os elementos da lista uma posição
L[0] = X;
// colocar o elemento X na 1ª posição (índice 0)
return L;
// operação com êxito – novo elemento inserido
}
Ordem complexidade (pior caso): O(N). Prova: C = 0 e A = (N−1) + 2 = N + 1; C + A = N + 1.
3.6.2. Inserir no fim
Neste caso, apenas é necessário introduzir o novo elemento na última posição da lista
(nova posição). Um possível algoritmo é o que se segue.
Dados de entrada: o elemento X, a lista L e o tamanho da lista (N)
Dados de saída: a lista L e o seu tamanho (N) atualizados
Algoritmo:
Inserir X na posição N+1 (uma posição à frente do último elemento da lista)
Atualizar o tamanho da lista, incrementando-o num valor
Uma função que traduz o algoritmo é a que se segue, que recebe o elemento a inserir
(X), uma lista (L) e o seu tamanho (*N) e, devolve a lista L e o seu tamanho atualizados.
ELEMENTO *InserirFim (ELEMENTO X, ELEMENTO L[], int *N) {
L = (ELEMENTO *) realloc(L, (*N+1) * sizeof(ELEMENTO));
if (L == NULL)
return NULL;
*N = *N + 1;
// impossível alocar memória para um elemento
// atualizar o tamanho da lista, incrementando um valor
L[*N-1] = X;
// colocar o elemento X na última posição (*N-1)
return L;
// operação com êxito – novo elemento inserido
}
Ordem complexidade (pior caso): O(0). Prova: C = 0 e A = 2.
3.6.3. Inserir por ordem
É o caso mais utilizado. Começa por pesquisar a posição pos onde inserir o novo
elemento; depois, todos os elementos que se posicionem depois de pos avançam uma
posição, ficando a posição pos livre para receber o novo valor. Um possível algoritmo é o
que se segue (considerando a ordenação pelo campo BI).
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória dinâmica
21
Dados de entrada: o elemento X, a lista L e o tamanho da lista (N)
Dados de saída: a lista L e o seu tamanho atualizados
Algoritmo: (lista ordenada crescentemente)
Determinar a posição k onde inserir o elemento X
Avançar uma posição todos os elementos da lista L que estão depois da posição k
Inserir X na posição k
Atualizar o tamanho da lista, incrementando-o num valor
A função que traduz o algoritmo é a que se segue, a qual recebe o elemento X a
inserir, a lista L e o tamanho da lista (*N), e devolve a lista e o seu tamanho atualizados.
ELEMENTO *InserirOrdem (ELEMENTO X, ELEMENTO L[], int *N) {
int k, pos;
L = (ELEMENTO *) realloc(L, (*N+1) * sizeof(ELEMENTO));
if (L == NULL)
return NULL;
// operação sem êxito – impossível inserir novo elemento
pos = 0;
while ((pos < *N) && (L[pos].BI > X.BI))
pos = pos + 1; // o elemento X deve ser inserido na posição pos da lista L
*N = *N + 1;
// atualizar o tamanho da lista, incrementando um valor
for (k = *N-1; k > pos; k--)
L[k] = L[k-1];
// avançar uma posição os elementos de L depois de pos
L[pos] = X;
// colocar o elemento X na posição pos
return L;
// operação com êxito – novo elemento inserido
}
Ordem complexidade (pior caso): O(N). Prova: C = N e A = 1 + k + (N−k) + 2 = 3 + N;
C + A = N + 3 + N = 3 + 2 x N.
3.7. Remover um elemento duma lista
Este problema consiste em remover (eliminar) um dado elemento de uma lista, a qual
pode ou não estar ordenada. No entanto, apesar destas duas situações serem diferentes,
em termos algorítmicos essa diferença encontra-se apenas na forma de pesquisar a posição
do elemento a remover. Para tal, basta apenas aplicar um dos métodos existentes para o
efeito (ver Pesquisa exaustiva − pág. 30), o que mais se adequar à lista: ordenada ou não
ordenada. Desta forma, um possível algoritmo é o que se segue.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
22
Listas - armazenamento sequencial usando memória dinâmica
Dados de entrada: o elemento X, uma lista L e o tamanho da lista (N)
Dados de saída: a lista L e o seu tamanho (N) atualizados
Algoritmo:
k ← posição do elemento X na lista L (utilizar um dos métodos de pesquisa)
Recuar uma posição todos os elementos que estão depois do elemento da posição k
Atualizar o tamanho da lista, decrementando N em um valor
Uma função que traduz o algoritmo é a que se segue, a qual recebe o elemento a
remover (X), uma lista ordenada (L) e o tamanho da lista (*N) e, devolve a lista e o seu
tamanho atualizados.
ELEMENTO *Remover (ELEMENTO X, ELEMENTO L[], int *N) {
int k, pos, *aux;
pos = PesquisaExaustiva(X, L, *N);
if (pos != -1) { // o elemento a remover, X, pertence à lista L
for (k = pos; k < *N-1; k++)
L[k] = L[k+1];
*N = *N – 1;
// atualizar o tamanho da lista, decrementando um valor
L = (ELEMENTO *) realloc(L, (*N) * sizeof(ELEMENTO));
//atualizar lista
}
return L;
}
No entanto, a forma mais comum de tratar este problema não é fornecer o elemento
a remover, mas sim apenas a posição daquele elemento, sendo o trabalho de pesquisa da
posição do referido elemento, assim como a verificação se aquele elemento pertence ou
não à lista, feito antes da remoção. Isto é, antes de se remover um elemento duma lista,
verifica-se se aquele elemento pertence à lista e, caso pertença, em que posição se
encontra, o que se pode fazer recorrendo a um dos métodos de pesquisa existentes (ver
Ordenação duma lista, pág. 25). Assim sendo, um possível algoritmo é o que se segue.
Dados de entrada: a posição (Pos) do elemento a remover, a lista L e o seu tamanho (N)
Dados de saída: a lista L e o seu tamanho (N) atualizados
Algoritmo:
Recuar uma posição todos os elementos que se encontram em posição após Pos
Atualizar o tamanho da lista, decrementando N em um valor
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Listas - armazenamento sequencial usando memória dinâmica
23
Uma função que traduz o algoritmo é a que se segue, a qual recebe a posição do
elemento a remover (Pos), uma lista (L) e o tamanho da lista (N) e, devolve atualizados a
lista L e o seu tamanho.
ELEMENTO *Remover (int Pos, ELEMENTO L[], int *N) {
int k;
for (k = Pos; k < *N-1; k++)
L[k] = L[k+1];
*N = *N – 1;
// recuar uma posição os elementos desde k
// atualizar o tamanho da lista, decrementando a N um valor
L = (ELEMENTO *) realloc(L, (*N) * sizeof(ELEMENTO));
// atualizar lista
return L;
}
Ordem complexidade (pior caso): O(N). Prova: C = 0 e A = (N - 1) + 1 = N; C + A = N.
3.8. Listar/mostrar um elemento ou os elementos duma lista
Uma das operações muito úteis é aquela que permite mostrar um ou todos os
elementos de uma lista.
Para mostrar apenas um elemento, um possível algoritmo é o seguinte:
Dados de entrada: um elemento E
Dados de saída: nada (apenas mostra no écran os dados do elemento dado)
Algoritmo:
Para cada campo do elemento E fazer
Mostrar informação
Uma função que traduz o algoritmo é a que se segue, a qual recebe um elemento E e
mostra no écran a informação que lhe está associada.
void MostrarElemento (ELEMENTO E) {
printf(“Nome: “);
puts(E.Nome);
printf(“Morada: “);
puts(E.Morada);
printf(“Idade: %d\n”, E.Idade);
printf(“BI: %d\n”, E.BI);
printf(“Sexo: “);
puts(E.Sexo);
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
24
Listas - armazenamento sequencial usando memória dinâmica
printf(“\n”);
}
Ordem complexidade (pior caso): O(0). Prova: total de operações = 9.
Para mostrar todos os elementos de uma lista, um possível algoritmo é o seguinte:
Dados de entrada: uma lista L e o seu tamanho N
Dados de saída: nada (apenas mostra no écran os dados dos elementos da lista L)
Algoritmo:
Para cada elemento k da lista L fazer
Mostrar o elemento da posição/índice k
Uma função que traduz o algoritmo é a que se segue, a qual recebe uma lista L e o
seu tamanho N e mostra no écran a informação que está associada a cada elemento de L.
void MostrarLista (ELEMENTO L[], int N) {
int k;
for (k = 0; k < N; k++) {
printf(“L[%d]: \n”, k);
MostrarElemento(L[k]);
printf(“\n”);
}
}
Ordem complexidade (pior caso): O(N). Prova: total de operações = N x (1 + 9 + 1) = 11xN.
3.9. Destruir uma lista
Quando se pretende destruir uma lista, basta realocar memória para 0 elementos e
atribuir ao seu tamanho o valor nulo (N = 0). Um possível algoritmo para criar uma lista é o
seguinte:
Dados de entrada: a lista L e o seu tamanho (N)
Dados de saída: a lista L sem elementos e o seu tamanho a valer 0 (N = 0)
Algoritmo:
Realocar memória para 0 elementos
Atualizar ao tamanho da lista o valor 0 (N = 0)
Uma função que traduz o algoritmo é a que se segue, a qual recebe a lista L a destruir
e o seu tamanho (N), devolve a lista L vazia e o seu tamanho a valer 0.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Ordenação duma lista
25
ELEMENTO *DestruirLista (ELEMENTO *L, int *N) {
L = (ELEMENTO *) realloc(0*sizeof(ELEMENTO));
*N = 0;
L = NULL;
return L;
}
Ordem complexidade (pior caso): O(0).
4. Ordenação duma lista
Este problema consiste em ordenar uma lista que se encontra desordenada. Existem
vários métodos para conseguir atingir tal objetivo, dos quais se destacam os seguintes:
Seleção Linear, Bubblesort, Quicksort e Inserção. Os algoritmos associados aos dois
primeiros são versões iterativas e os associados aos dois restantes são versões recursivas.
No estudo destes métodos vai-se admitir, tal como nos anteriores, que se pretende ordenar
a lista crescentemente, relativamente ao campo Chave (que pode ser, por exemplo, o
campo BI).
4.1. Seleção Linear
Este método consiste em determinar o menor elemento e colocá-lo na posição 1
(índice 0), depois determinar o segundo menor elemento e colocá-lo na posição 2 (índice
1), e assim sucessivamente até colocar o maior elemento na posição N (índice N-1). Um
possível algoritmo é o que se segue.
Dados de entrada: uma lista L e o tamanho da lista (N)
Dados de saída: a lista L ordenada (alguns elementos estão em posições diferentes)
Algoritmo:
Para cada posição da lista L (k = 0, …, N-1) Fazer
menor ← posição do k-ésimo menor elemento da lista L
Trocar o elemento da posição menor com o da posição k
Uma função que traduz o algoritmo é a que se segue, que recebe uma lista não
ordenada (L) e o tamanho da lista (N) e, devolve a lista L (apesar do tamanho ser o mesmo,
alguns dos elementos mudaram de posição) atualizada.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
26
Ordenação duma lista
void OrdenarSeleccaoLinear (ELEMENTO L[], int N) {
int k, j, menor;
ELEMENTO aux;
for (k = 0; k < N; k++) {
menor = k;
// inicialmente o menor elemento está na posição i
for (j = k+1; j < N; j++) // percorrer os elementos desde a posição i+1
if (L[menor].BI > L[k].BI)
menor = j; // atualizar menor, pois encontrou-se um elemento menor
aux = L[menor];
// trocar os elementos das posições menor com o de i
L[menor] = L[k];
L[k] = aux;
}
}
Ordem de complexidade (pior caso): O(N2).
Prova:
k = 0; j = 1,… , N-1 ⇒ N-1 comparações
k = 1; j = 2,… , N-1 ⇒ N-2 comparações
...
k = N-2; j = N-1,… , N-1 ⇒ N-(N-1) = 1 comparação
Logo, o número de comparações é o seguinte:
C = (N-1) + (N-2) + … + 1 =
N ⋅ (N − 1)
(N − 1) + 1
N2 − N
⋅ (N − 1) =
=
2
2
2
Por outro lado, o número de atribuições é o seguinte:
A = 4.(N-1) + C
{ for k + for j }
Concluindo, temos que o número total de operações é o seguinte:
N2 − N
N2 − N
N2 − N
C+A=
+ 4(N − 1) +
=2
+ 4.N - 4 = N2 + 3.N - 4
2
2
2
Ou seja, o algoritmo é da ordem de complexidade de N2.
4.2. Bubblesort
Este método consiste em fazer trocas sucessivas entre dois elementos seguidos
(vizinhos), de forma que o maior fique depois do menor. Este processo (iteração) é
repetido percorrendo todos os elementos da lista as vezes que for necessário, até que não
se faça qualquer troca, o que acontece quando a lista está ordenada. Note-se que após a
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Ordenação duma lista
27
iteração k se concluir, os k-últimos elementos ficam ordenados (após a conclusão da 1ª
iteração, o maior elemento fica na N-ésima (última) posição, e assim sucessivamente);
portanto, cada iteração apenas vai analisar até uma determinada posição. Um possível
algoritmo é o que se segue.
Dados de entrada: uma lista L e o tamanho da lista (N)
Dados de saída: a lista L modificada (apesar do tamanho e dos elementos se manter,
alguns elementos estão em posições diferentes)
Algoritmo:
limite ← N
// inicialmente vai-se analisar até à posição N
Fazer
trocas ← 0
Para cada posição da lista L (k = 1, …, limite−1) Fazer
Se L[k+1].Chave < L[k].Chave Então
Trocar o elemento da posição k+1 com o da posição k
trocas ← trocas + 1
limite ← limite – 1 // o elemento que está na posição limite está ordenado
Enquanto (trocas > 0) // termina quando o teste “Se…Então…” for sempre falso
Uma função que traduz o algoritmo é a que se segue, a qual recebe uma lista não
ordenada (L) e o tamanho da lista (N) e, devolve atualizado a lista L (apesar do tamanho
ser o mesmo, alguns dos elementos mudaram de posição).
void OrdenarBubblesort (ELEMENTO L[], int N) {
int k, limite = N-1, trocas; // inicialmente vai-se analisar até à posição N-1
ELEMENTO aux;
do {
trocas = 0;
for (k = 0; k < limite; k++)
if (L [k+1].BI < L[k].BI) {
aux = L[k+1];
L[k+1] = L[k];
L[k] = aux;
trocas = trocas + 1;
}
limite = limite − 1;
// o elemento da posição limite está na posição correta
} while (trocas = 0);
}
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
28
Ordenação duma lista
Ordem complexidade (pior caso − lista ordenada pela ordem inversa): O(N2).
Prova:
1ª iteração: k = 0,…, N-2 ⇒ N-1 comparações
2ª iteração: k = 0,…, N-3 ⇒ N-2 comparações
...
k-ésima iteração: k = 0,…, N-k ⇒ k comparações
...
N-ésima iteração (última): k = 0,…, 0 ⇒ 1 comparação
Logo, o número de comparações é o seguinte:
C = (N-1) + (N-2) + … + 1 =
N ⋅ (N − 1)
(N − 1) + 1
N2 − N
=
⋅ (N − 1) =
2
2
2
Por outro lado, o número de atribuições é o seguinte:
A = (N-1) + 4.C { do…while + for k }
Concluindo, temos que o número total de operações é o seguinte:
C+A=
5 N2 − 3 N − 2
N2 − N
N2 − N
N2 − N
+ (N-1) + 4.
= 5.
+ N-1 =
2
2
2
2
Ou seja, o algoritmo é da ordem de complexidade de N2.
4.3. Quicksort
Este método consiste em considerar um elemento qualquer da lista, o pivot, que
poderá ser o primeiro da lista, e deslocá-lo para a sua posição na lista ordenada, colocando
todos os elementos menores do que o pivot à sua esquerda e os maiores à sua direita (se
ordenação por ordem crescente). Este processo é aplicado recursivamente às sublistas que
ainda não estejam ordenadas. Um possível algoritmo é o que se segue.
Dados de entrada: uma lista L e as posições do início e do fim da lista
Dados de saída: a lista L ordenada
Algoritmo:
pivot ← elemento que está na posição inicio da lista
Fazer:
Procurar um elemento maior do que o pivot (posição k)
Procurar um elemento à direita de k que seja menor do que o pivot
Trocar os dois elementos encontrados antes, entre si
Enquanto for possível fazer alguma troca
Trocar o pivot com o elemento menor do que ele que se encontra mais afastado
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Ordenação duma lista
29
Se existe alguma sublista à esquerda do pivot que não está ordenada Então
Aplicar recursivamente o método àquela sublista
Se existe alguma sublista à direita do pivot que não está ordenada Então
Aplicar recursivamente o método àquela sublista
Uma função que traduz o algoritmo é a que se segue, que recebe uma lista não
ordenada (L) e o tamanho da lista (N) e, devolve atualizado a lista L (apesar do tamanho
ser o mesmo, alguns dos elementos mudaram de posição).
void Trocar (int *a, int *b) {
int aux;
aux = *a;
*a = *b;
*b = aux;
}
int DeterminarPivot (int V[], int inicio, int fim) {
int i, k = inicio;
// k = posição do pivot V[inicio]
for (i = inicio+1; i <= fim; i++)
if (V[i].BI < V[inicio].BI) {
k++;
Trocar(&V[i], &V[k]);
}
Trocar(&V[inicio], &V[k]);
return k;
}
void OrdenarQuicksort (int V[], int inicio, int fim) {
int k;
// k = posição do pivot V[inicio]
if (inicio < fim) {
k = DeterminarPivot(V, inicio, fim);
OrdenarQuicksort(V, inicio, k-1);
OrdenarQuicksort(V, k+1, fim);
}
}
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
30
Pesquisa dum elemento numa lista
4.4. Por Inserção
Este método consiste em tomar um dos elementos da lista, ordenar a sublista
composta pelos restantes elementos e depois inserir o elemento tomada antes na sublista
ordenada. Este processo é executado recursivamente e utilizando o algoritmo para inserir
um elemento numa lista ordenada estudado antes (ver Inserir um elemento numa lista −
pág. 8). Um possível algoritmo é o que se segue.
Dados de entrada: uma lista L e o tamanho da lista (N)
Dados de saída: a lista L ordenada (alguns elementos estão em posições diferentes)
Algoritmo:
Se N > 1 Então
Ordenar a sublista de L com tamanho N-1 (do índice 0 ao índice N-2)
Inserir o elemento no índice N-1, L[N-1], na sublista ordenada no passo anterior
Uma função que traduza o algoritmo é a que se segue, que recebe uma lista não
ordenada (L) e o tamanho da lista (N) e, devolve atualizado a lista L (apesar do tamanho
ser o mesmo, alguns dos elementos mudaram de posição).
void OrdenarPorInsercao (ELEMENTO L[], int N) {
int k;
if (N > 1) {
k = N-1;
OrdenarPorInsercao(L, k);
InserirOrdem(L[N-1], L, k);
}
}
5. Pesquisa dum elemento numa lista
Dada uma lista, pretende-se determinar se um dado elemento pertença à lista; se
sim, deve-se indicar a posição na lista. A lista pode estar desordenada ou ordenada.
5.1. Pesquisa exaustiva
Este método aplica-se quando a lista não se encontra ordenada, pois a pesquisa do
elemento terá que ser feita exaustivamente (percorrendo e analisando todos os elementos
da lista). Um possível algoritmo para este caso é o que se segue.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Pesquisa dum elemento numa lista
31
Dados de entrada: o elemento X, a lista L não ordenada e o tamanho da lista (N)
Dados de saída: a posição de X na lista L (se X ∈ L)
Algoritmo:
Para cada elemento de L, L[k] com k = 0, …, N-1, Fazer
Se X.Chave = L[k].Chave Então
Terminar pesquisa (L[k] tem como chave o mesmo valor de X)
Senão
Avançar mais uma posição da lista L
Se X ∈ L Então
Devolver k (posição do elemento de L com a mesma Chave de X)
Senão
Devolver -1 (não foi encontrado qualquer elemento com a mesma Chave de X)
Uma função que traduz o algoritmo é a que se segue, a qual devolve a posição do
elemento X (se X ∈ L) ou -1 (se X ∉ L).
int PesquisaExaustiva (ELEMENTO X, ELEMENTO L[], int N) {
int k = 0;
while ((k < N) && (L[k].BI != X.BI))
k = k + 1;
if (k < N)
return k;
return -1;
}
Ordem complexidade (pior caso): O(N).
5.2. Pesquisa Sequencial
Este método aplica-se a listas ordenadas, em que a pesquisa do elemento é feita a
partir do início da lista e sequencialmente, terminando quando o elemento for encontrado
ou quando o valor da Chave do elemento a pesquisar for superior ao da Chave do elemento
a analisar. Um possível algoritmo é o que se segue.
Dados de entrada: o elemento X, a lista L e o tamanho da lista L (N)
Dados de saída: a posição de X na lista L (se X ∈ L) ou zero (se X ∉ L)
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
32
Pesquisa dum elemento numa lista
Algoritmo:
Para cada elemento de L, L[k] com k = 0, …, N-1, Fazer
Se X.Chave = L[k].Chave Então
Terminar pesquisa (L[k] tem como chave o valor X)
Senão
Se X.Chave > L[k].Chave Então
Terminar pesquisa (nenhum elemento da lista L tem a Chave de X)
Senão
Avançar mais uma posição da lista L
Se X ∈ L Então
Devolver k (posição do elemento de L com a mesma Chave de X)
Senão
Devolver -1 (não foi encontrado qualquer elemento com a mesma Chave de X)
Uma função que traduz o algoritmo é a que se segue, a qual devolve a posição do
elemento X (se X ∈ L) ou -1 (se X ∉ L).
int PesquisaSequencial (ELEMENTO X, ELEMENTO L[], int N) {
int k = 0;
while ((k < N) && (L[k].BI <= X.BI))
k = k + 1;
if (k < N && (L[k].BI == X.BI)
return k;
return -1;
}
Ordem complexidade (pior caso): O(N). Prova: C1 = N, C2 = N e A = 1; C + A = 2 N + 1
5.3. Pesquisa Binária
Neste método a lista deverá estar ordenada, sendo que a pesquisa do elemento na
lista é feita através da análise de sublistas. Isto é, pega-se no elemento que se encontra ao
meio da lista e verifica-se se o elemento a pesquisar está para a direita ou para a esquerda
deste elemento; se está à esquerda, vai-se pesquisar apenas a sublista à esquerda deste
elemento, caso contrário, pesquisa-se apenas a sublista à direita. O processo termina
quando o elemento for encontrado ou quando a sublista onde se vai pesquisar o elemento
não tem elementos, o que significa que o elemento a pesquisar não pertence à lista.
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Pesquisa dum elemento numa lista
33
A implementação deste método pode ser feita de duas formas distintas ou versões:
iterativa e recursiva. Um possível algoritmo para a versão iterativa é o que se segue.
Dados de entrada: o elemento X, a lista L e o tamanho da lista L (N)
Dados de saída: a posição de X na lista L (se X ∈ L) ou -1 (se X ∉ L)
Algoritmo:
esq ← 1
dir ← N
Enquanto (esq ≤ dir) Fazer { esq ≤ dir ⇒ L ≠ ∅ }
meio ← posição do elemento que está ao meio da lista − (esq + dir) / 2
Se X.Chave = L[meio].Chave Então
X encontra-se na posição meio
Terminar pesquisa
Senão
Se X.Chave < L[meio].Chave Então
dir ← meio - 1
Senão
esq ← meio + 1
Se (esq > dir) Então
X ∉ L (nenhum elemento de L tem a mesma Chave de X)
Uma função que traduz o algoritmo é a que se segue, a qual devolve a posição do
elemento X (se X ∈ L) ou -1 (se X ∉ L).
int PesquisaBinaria (ELEMENTO X, ELEMENTO L[], int N) {
int esq = 0, dir = N-1, meio;
while (esq <= dir) {
meio = (esq + dir) / 2;
if (X.BI == L[meio].BI)
return meio; //elemento na posição meio de L com mesmo Nome de X
else
if (X.BI < L[meio].BI)
dir = meio − 1;
else
esq = meio + 1;
}
return -1; // nenhum elemento da lista L tem a mesma Chave de X
}
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
34
Pesquisa dum elemento numa lista
Ordem complexidade (pior caso − X ∉ L e X < L[1]): O(log2 N).
Prova:
1ª comparação: [1..N] = [1..N/20]
2ª comparação: [1..N/2] = [1..N/21]
3ª Comparação:[1..(N/2)/2] = [1..N/4] = [1..N/22]
...
k-ésima comparação: [1..N/2k−1]
Na última comparação: [1..1]. Ou seja, N/2k−1 = 1.
Logo, 2k−1 = N ⇒ k - 1 = log2 N ⇔ k = log2 N + 1
Assim sendo, o número de comparações é o seguinte: C = log2 N + 1.
O número de atribuições está associado ao número de comparações: por cada
comparação são efetuadas 2 atribuições (meio e dir/esq). Logo, A = 2.(log2 N + 1).
Concluindo,
C + A = (log2 N + 1) + (2.(log2 N + 1)) = 3.log2 N + 3.
Ou seja, o algoritmo é da ordem de complexidade de log2 N.
Um possível algoritmo para a versão recursiva é o que se segue.
Dados de entrada: o elemento X, a lista L e as posições dos elementos mais à esquerda
(esq) e mais à direita (dir) na lista L
Dados de saída: a posição de X na lista L (se X ∈ L) ou zero (se X ∉ L)
Algoritmo:
Se esq > dir Então
{ esq > dir ⇒ L = ∅ }
X ∉ L  pesquisa de X numa lista vazia
STOP  terminar pesquisa
meio ← posição do elemento que está ao meio da lista − (esq + dir) / 2
Se X.Chave = L[meio].Chave Então
X encontra-se na posição meio
Senão
Se X.Chave < L[meio].Chave Então
Pesquisar X na lista L entre as posições esq e meio−1
Senão
Pesquisar X na lista L entre as posições meio+1 e dir
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Pesquisa dum elemento numa lista
35
Uma função que traduz o algoritmo é a que se segue, a qual devolve a posição do
elemento X (se X ∈ L) ou -1 (se X ∉ L).
int PesquisaBinariaRec (ELEMENTO X, ELEMENTO L[], int esq, dir) {
int i, meio;
if (esq > dir)
return -1; // pesquisar X numa lista vazia ⇒ X ∉ L
meio = (esq + dir) / 2;
if (X.BI == L[meio].BI)
return meio;
else
if (X.BI < L[meio].BI)
return PesquisaBinariaRec(X, L, esq, meio-1);
else
return PesquisaBinariaRec(X, L, meio+1, dir);
}
Ordem complexidade (pior caso): O(log2 N) (tal como na versão iterativa, fazendo a mesma
análise).
Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial
Download

Cap. 2