Baixe o app para aproveitar ainda mais
Prévia do material em texto
Índice i Índice Í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 ii Índice Í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 A Estrutura Abstrata de Dados “Lista” 1 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 2 A Estrutura Abstrata de Dados “Lista” Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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. Listas - armazenamento sequencial usando memória estática 3 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 4 Listas - armazenamento sequencial usando memória estática Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 50 ... 98 99 9 14 18 ... 27 35 ... N = 100; for (k = 50; k < N; k++) scanf(“%d”, &Lista[k]); 0 1 2 ... 48 49 50 ... 98 99 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 Listas - armazenamento sequencial usando memória estática 5 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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. 6 Listas - armazenamento sequencial usando memória estática Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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: Listas - armazenamento sequencial usando memória estática 7 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 comum 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) 8 Listas - armazenamento sequencial usando memória estática Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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; // a lista está vazia return 0; // 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; // a lista L está cheia return 0; // 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. Listas - armazenamento sequencial usando memória estática 9 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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]; // avançar todos os elementos da lista uma posição L[0] = X; // 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. 10 Listas - armazenamento sequencial usando memória estática Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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]; // avançar uma posição os elementos de L depois de pos L[pos] = X; // 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 Listas - armazenamento sequencial usando memória estática 11 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 12 Listas - armazenamento sequencial usando memória estática Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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”); } Listas - armazenamento sequencial usando memória estática 13 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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). 14 Listas - armazenamento sequencial usando memória dinâmica Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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. Listas - armazenamento sequencial usando memória dinâmica 15 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 ... 98 99 9 14 18 ... 27 35 51 ... 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 ... 98 99 100 101 9 14 18 ... 27 35 51 ... 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 todosos elementos seguintes 16 Listas - armazenamento sequencial usando memória dinâmica Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 Listas - armazenamento sequencial usando memória dinâmica 17 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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) 18 Listas - armazenamento sequencial usando memória dinâmica Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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). Listas - armazenamento sequencial usando memória dinâmica 19 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial int ListaVazia1 (ELEMENTO L[]) { if (L == NULL) return 1; // lista vazia return 0; // lista não vazia } int ListaVazia2 (int N) { if (N == 0) return 1; // lista vazia return 0; // 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 20 Listas - armazenamento sequencial usando memória dinâmica Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial *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; // impossível alocar memória para um elemento *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) return L; // operação com êxito – novo elemento inserido } Ordem complexidade (pior caso): O(0). Prova: C = 0 eA = 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). Listas - armazenamento sequencial usando memória dinâmica 21 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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. 22 Listas - armazenamento sequencial usando memória dinâmica Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 Listas - armazenamento sequencial usando memória dinâmica 23 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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]; // recuar uma posição os elementos desde k *N = *N – 1; // 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); 24 Listas - armazenamento sequencial usando memória dinâmica Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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. Ordenação duma lista 25 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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. 26 Ordenação duma lista Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 = )1N( 2 1)1N( −⋅ +− = 2 )1N(N −⋅ = 2 NN2 − 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: C + A = 2 NN2 − + 4(N − 1) + 2 NN2 − = 2 2 NN2 − + 4.N - 4 = N2 + 3.N - 4 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 Ordenação duma lista 27 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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); } 28 Ordenação duma lista Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 = )1N( 2 1)1N( −⋅ +− = 2 )1N(N −⋅ = 2 NN2 − 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 = 2 NN2 − + (N-1) + 4. 2 NN2 − = 5. 2 NN2 − + N-1 = 2 2N3N5 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 Ordenação duma lista 29 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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); } } 30 Pesquisa dum elemento numa lista Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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. Pesquisa dum elemento numa lista 31 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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) 32 Pesquisa dum elemento numa lista Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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. Pesquisa dum elemento numa lista 33 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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 } 34 Pesquisa dum elemento numa lista Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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].ChaveEntã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 Pesquisa dum elemento numa lista 35 Cap. 2 - Estrutura de Dados sequencial com armazenamento sequencial 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).
Compartilhar