Baixe o app para aproveitar ainda mais
Prévia do material em texto
1 ED – Notas de Aula 01 Capítulo 1 – Introdução Ao concluir este capítulo, você deverá ser capaz de: - Conhecer as formas mais básicas de organizar os dados na memória de um computador, de forma que essa organização reflita bem o problema que está sendo tratado e torne mais efici- entes os algoritmos que manipulem esses dados. 2 Introdução Na resolução de um problema através de um programa, a primeira providência é con- ceber um algoritmo adequado. [Um algoritmo é um processo sistemático para a resolução de um problema, consistindo de uma seqüência de passos, ordenada e sem ambigüidades.] A eficiência de um algoritmo está intimamente relacionada com a disposição, na me- mória, dos dados que são tratados pelo programa. Por exemplo, se freqüentemente enfrenta- mos o problema de descobrir o número do telefone de nossos conhecidos, é conveniente dis- por de uma relação de números organizada em uma agenda. Se a organização for feita pela ordem alfabética, a agenda de fato ajuda. Se, porém, organizássemos nossa agenda pela ordem de altura das pessoas, com raras exceções, a agenda se tornaria difícil de manusear. As estruturas de dados são formas de distribuir e relacionar os dados disponíveis, de modo a tornar mais eficientes os algoritmos que manipulam esses dados. Vejamos alguns exemplos práticos Situação 1 problema: Manipular um conjunto de fichas em um fichário. solução: Organizar as fichas em ordem alfabética. operações possíveis: Inserir ou retirar uma ficha, procurar uma ficha, etc. estrutura de dados: LISTA – seqüência de elementos dispostos em alguma ordem. 3 Situação 2 problema: Organizar as pessoas que querem ser atendidas em um guichê. solução: Colocar as pessoas em fila. operações possíveis: Atender uma pessoa no guichê, entrar uma pessoa no final da fila. Não é permitido “furar” a fila, ou seja, entrar uma pessoa entre outras que já estão presentes. estrutura de dados: FILA – seqüência de elementos dispostos em ordem, mas com uma regra para a entrada e saída dos elementos: o primeiro elemento que chega também é o pri- meiro que sai da estrutura. 4 Situação 3 problema: Organizar um conjunto de pratos que estão sendo lavados, um a um, em um res- taurante. solução: Colocar os pratos empilhados. operações possíveis: Colocar um prato limpo no alto da pilha, retirar um prato do alto da pilha. Não é permitido tirar um prato qualquer do meio da pilha. estrutura de dados: PILHA - seqüência de elementos dispostos em ordem, mas com uma regra para a entrada e saída dos elementos: o último que chega é o primeiro a sair da estrutura. 5 Situação 4 problema: Conseguir um modo de visualizar o conjunto de pessoas que trabalham em uma empresa, tendo em conta a função de cada pessoa. solução: Construir um organograma da empresa. operações possíveis: Inserir ou retirar certas funções, localizar uma pessoa, etc. estrutura de dados: ÁRVORE – estrutura de dados que caracteriza uma relação de hierar- quia entre os elementos: uma pessoa não pode pertencer a dois departamentos diferentes, cada diretoria tem os seus próprios departamentos, etc. 6 Situação 5 problema: Estabelecer um trajeto para percorrer todas as capitais de um país. solução: Utilizar um mapa que indique as rodovias existentes, e estabelecer uma ordem pos- sível para percorrer todas as cidades operações possíveis: Encontrar um modo de percorrer todas as cidades, determinar o cami- nho mais curto para ir de uma cidade a outra, etc. estrutura de dados: GRAFO – estrutura que organiza vários elementos, estabelecendo rela- ções entre eles, dois a dois 7 Os exemplos vistos correspondem exatamente aos tipos básicos de estruturas de dados utilizadas em computação: listas, filas, pilhas, árvores e grafos. As estruturas de dados são abstratas, isto é, devem ser imaginadas como uma forma de organizar e relacionar dados na memória do sistema, de modo a permitir certas operações so- bre esses dados, independentemente do modo como essas estruturas são implementadas. Elas podem ser implementadas de diversos modos, dependendo da capacidade da máquina e dos recursos oferecidos pela linguagem que se utiliza. Neste curso, vamos implementá-las sempre utilizando ponteiros, de modo que o ge- renciamento da memória seja dinâmico. Isto significa que as estruturas podem ser criadas, ampliadas, reduzidas ou removidas da memória em tempo de execução. Desta forma otimiza- se o uso da memória disponível, em função do conjunto de dados que o programa está proces- sando. 1 ED – esquema para tutorial 02 Capítulo 2 – Listas [página de abertura] Ao concluir este capítulo, você deverá ser capaz de: - compreender o que são as listas - identificar os tipos mais importantes de listas - conhecer as operações básicas que se podem efetuar com as listas - tomar contato com algumas aplicações de listas 2.1 Conceitos 2.2 Implementação 2.3 Tipos Básicos 2.4 Algoritmos 2.5 Aplicações 2 2.1 Conceitos Uma lista é uma seqüência ordenada de elementos de mesmo tipo. Por exemplo, um conjunto de fichas de clientes de uma loja, organizadas pela ordem alfabética dos nomes dos clientes. Neste fichário é possível introduzir uma nova ficha ou retirar uma velha, alterar os dados de um cliente, etc. As operações mais básicas que se podem realizar sobre uma lista são: - construção da lista: montagem da lista na memória do computador, através da leitura dos dados de um arquivo. - percurso por todos os elementos da lista: percorrer todos os elementos da lista, possivel- mente para obter algum dado ou fazer alguma alteração em todos os elementos da lista. - procura de um elemento: procura de um elemento na lista, possivelmente para fazer alguma alteração nos dados desse elemento. - inserção de um elemento novo: inserção do novo elemento na lista, tendo em conta a posição que ele deve ocupar para manter a lista ordenada. - remoção de um elemento da lista: retirar um determinado elemento da lista, sendo necessá- rio para isso, primeiramente encontrar o elemento desejado. - destruição da lista toda: remover todos os elementos da lista, deixando-a vazia. Outras operações podem ser encontradas no módulo de Aplicações 3 3.2 Implementação Como já foi dito, as estruturas de dados são abstratas e portanto podem ser implementadas de diversos modos. [estruturas abstratas devem ser imaginadas como uma forma de organizar e relacionar dados na memória do sistema, de modo a permitir certas operações sobre esses dados, independentemente do modo como essas estruturas são implementadas] Para implementar as listas vamos utilizar a técnica de encadeamento por ponteiros. Cada elemento de uma lista é um registro que contém, entre outros campos, um campo de tipo ponteiro, que aponta para outro elemento da lista. struct objeto { tipodado dado; struct objeto *prox; }; A título de exemplo, vejamos como se poderia construir uma lista contendo a seqüên- cia de caracteres 'x', 'y', 'z'. Esta função está dividida em três blocos, cada um deles responsá- vel por acrescentar um dos elementos na lista. struct elemento { char x; struct elemento *prox; }; struct elemento *pinicio, *p1; main () { pinicio = NULL; p1 = malloc (sizeof (struct elemento)); p1->x = 'x'; p1->prox = pinicio; pinicio = p1; p1 = malloc (sizeof(struct elemento)); p1->x = 'y'; p1->prox = pinicio; pinicio = p1; p1 = malloc (sizeof (struct elemento)); p1->x = 'z'; p1->prox = pinicio; pinicio = p1; } Se os três blocos forem unificados em apenas um, dentro de um laço comandado pela leitura de um arquivo, temos a possibilidade de construir a lista com qualquer número de ele- Tira Teima 4 mentos. O número de elementos da lista será o número de elementos presentes no arquivo de entrada. O programa ficaria do seguinte modo: struct elemento { char x; struct elemento *prox; } ; struct elemento *pinicio, *p1; main () { FILE *arq; int c; arq = fopen ("t100.txt", "r"); pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->x = c; p1->prox = pinicio; pinicio = p1; } } Tira Teima 5 3.3 Tipos Básicos de Listas Há muitos modos de se implementar uma lista através de ponteiros, dependendo da disponibilidade de memória, eficiência desejada para os algoritmos, etc. Há dois critérios mais básicos para classificar as listas: abertas ou fechadas, com encadeamento simples ou encadeamento duplo, dando origem assim a quatro padrões básicos de listas: lista aberta com encadeamento simples [ ] lista aberta com encadeamento duplo [ ] lista fechada com encadeamento simples [ ] lista fechada com encadeamento duplo [ ] Nas listas abertas, o último elemento tem seu campo prox valendo NULL (se o enca- deamento for duplo, o mesmo acontece com o campo ant do primeiro elemento). Nas listas fechadas, o campo prox do último elemento aponta para o primeiro elemento (se o encadea- mento for duplo, o campo ant do primeiro elemento aponta para o último elemento). 6 Nas listas com encadeamento simples, cada elemento tem apenas um campo do tipo ponteiro, apontando para o elemento seguinte na lista. Nas listas com encadeamento duplo, cada elemento tem dois ponteiros: o prox, apontando para o elemento seguinte, e o ant, apon- tando para o elemento anterior. Outros padrões e modos de implementação de listas podem ser encontrados no tópico de Aplicações. 7 3.4 Algoritmos para Listas Neste tópico veremos os algoritmos correspondentes às operações mais básicas [1] que podem ser efetuadas com as listas, aplicadas aos quatro tipos mais comuns [2]de listas. As operações de construção serão desmembradas em duas: construção com inserção do ele- mento no início da lista e construção com inserção no final da lista. 1 [construção da lista, percurso na lista, procura de um elemento em uma lista ordenada, inserção de um elemento em uma lista ordenada, remoção de um elemento da lista, remoção de todos os ele- mentos da lista] 2 [ aberta com encadeamento simples, aberta com encadeamento duplo, fechada com encadea- mento simples, fechada com encadeamento duplo] É interessante notar que, em vários algoritmos, há a necessidade de se analisar separa- damente a situação genérica (em geral, o elemento que interessa está no meio da lista) e as situações particulares (que costumam ser as seguintes: lista vazia, elemento no começo da lista, elemento no final da lista, lista com apenas um elemento). Sempre que possível, convém que os casos particulares sejam tratados no mesmo trecho de programa que o caso geral, tor- nando o programa mais sintético. Dependendo do tipo de lista escolhida, certas operações se tornam mais simples, pois os casos particulares podem ser tratados juntamente com o caso geral. Em todas as funções que utilizam listas de encadeamento simples, o tipo de cada ele- mento será: struct elemento { char dado; struct elemento *prox; } ; Em todas as funções que utilizam listas de encadeamento duplo, o tipo de cada ele- mento será: struct elemento { char dado; struct elemento *prox, *ant; } ; ***** acrescentar uma observação a respeito da passagem de parâmetros do tipo “endereço para ponteiro” (ver esquema 4, árvores) 3.4.1 Listas Abertas com Encadeamento Simples a) Construção com inserção dos elementos no início da lista Neste caso, cada dado lido do arquivo é inserido no começo da lista. Dessa forma, a ordem dos elementos na lista fica invertida em relação à ordem original do arquivo. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista. void construir1(struct elemento **pinicio) { FILE *arq; 8 struct elemento *p1; char c; arq = fopen ("t1.txt", "r"); *pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; p1->prox = *pinicio; *pinicio = p1; } } fclose (arq); return; } b) Construção com inserção dos elementos no final da lista Neste caso, cada dado lido do arquivo é inserido no final da lista. Dessa forma, a or- dem dos elementos na lista fica idêntica à ordem original do arquivo. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista. void construir2(struct elemento **pinicio) { FILE *arq; struct elemento *p1, *p2; char c; arq = fopen ("t5.txt", "r"); *pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; p1->prox = NULL; if (*pinicio == NULL) *pinicio = p1; else p2->prox = p1; p2 = p1; } } fclose (arq); return; } c) Percurso por todos os elementos da lista Esta operação consiste em percorrer todos os elementos da lista, realizando alguma ação em cada um. Neste exemplo a ação consiste em escrever o dado na tela. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista. void percorrer(struct elemento *pinicio) { struct elemento *p1; if (pinicio == NULL) printf ("lista vazia \n"); else { p1 = pinicio; while (p1 != NULL) { printf("elemento: %c \n", p1->dado); p1 = p1->prox; } } return; } Tira Teima Tira Teima Tira Teima 9 d) Procura de um dado na lista Esta operação consiste na procura de um determinado dado na lista. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista, e o dado a ser procurado, denominado aqui de chave, e retorna um ponteiro apontando para o elemento que contem o dado. Se o dado procurado não estiver presente na lista, a função retorna o valor NULL. void *procurar(struct elemento *pinicio, char chave) { struct elemento *p1; p1 = pinicio; while ((p1 != NULL) && (p1->dado != chave)) p1 = p1->prox; return p1; } e) Inserção de um dado em uma lista ordenada Esta operação consiste em inserir um novo dado na lista, no lugar que lhe corresponda. No exemplo a lista está ordenada em ordem crescente. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista, e o dado a ser inserido, denominado aqui de dadonovo. void inserir(struct elemento **pinicio, char dadonovo){ struct elemento *p1, *p2; p1 = malloc (sizeof (struct elemento)); p1->dado = dadonovo; if (*pinicio == NULL) *pinicio = p1; else if ((*pinicio)->dado > dadonovo) { p1->prox = *pinicio; *pinicio = p1; } else { p2 = *pinicio; while ((p2->prox != NULL) && (p2->prox->dado < dadonovo)) p2 = p2->prox; p1->prox = p2->prox; p2->prox = p1; } return;} f) Remoção de um dado de uma lista Esta operação consiste em remover um determinado dado da lista. Se o dado não esti- ver presente, a função não faz nada. Se o dado estiver presente várias vezes na lista, todos os elementos correspondentes serão removidos. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista, e o dado a ser removido, aqui denominado chave. void remover(struct elemento **pinicio, char chave){ struct elemento *p1, *p2; int achou; if (*pinicio == NULL) printf("lista vazia \n"); else { p1 = *pinicio; achou = 0; Tira Teima Tira Teima Tira Teima 10 while (p1 != NULL) { if (p1->dado == chave) { if (*pinicio == p1){ *pinicio = (*pinicio)->prox; p2 = *pinicio; } else p2->prox = p1->prox; free (p1); p1 = p2; achou =1; } else { p2 = p1; p1 = p1->prox; } } if (!achou) printf ("elemento nao presente \n"); } return; } g) Remoção de toda a lista Esta operação consiste em desfazer totalmente uma lista, removendo todos os elemen- tos, um a um, começando pelo início da lista. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista. void destruir(struct elemento **pinicio){ struct elemento *p1; while (*pinicio != NULL) { p1 = *pinicio; *pinicio = (*pinicio)->prox; free (p1); } return; } 3.4.2 Listas Abertas com Encadeamento Duplo a) Construção com inserção dos elementos no início da lista Neste caso, cada dado lido do arquivo é inserido no começo da lista. Dessa forma, a ordem dos elementos na lista fica invertida em relação à ordem original do arquivo. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista. void construir1(struct elemento **pinicio) { FILE *arq; struct elemento *p1; char c; arq = fopen ("t1.txt", "r"); *pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; p1->prox = *pinicio; if (*pinicio != NULL) (*pinicio)->ant = p1; p1->ant = NULL; *pinicio = p1; } } fclose (arq); Tira Teima Tira Teima 11 return; } b) Construção com inserção dos elementos no final da lista Neste caso, cada dado lido do arquivo é inserido no final da lista. Dessa forma, a or- dem dos elementos na lista fica idêntica à ordem original do arquivo. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista. void construir2(struct elemento **pinicio) { FILE *arq; struct elemento *p1, *p2; char c; arq = fopen ("t5.txt", "r"); *pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; p1->prox = NULL; if (*pinicio == NULL) { *pinicio = p1; p1->ant = NULL; } else { p2->prox = p1; p1->ant = p2; } p2 = p1; } } fclose (arq); return; } c) Percurso por todos os elementos da lista Esta operação consiste em percorrer todos os elementos da lista, realizando alguma ação em cada um. Neste exemplo a ação consiste em escrever o dado na tela. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista. void percorrer(struct elemento *pinicio) { struct elemento *p1; if (pinicio == NULL) printf ("lista vazia \n"); else { p1 = pinicio; while (p1 != NULL) { printf("elemento: %c \n", p1->dado); p1 = p1->prox; } p1 = pinicio; while (p1->prox != NULL) p1 = p1->prox; while (p1 != NULL) { printf("elemento (para tras): %c \n", p1->dado); p1 = p1->ant; } } return; } d) Procura de um dado na lista Tira Teima Tira Teima 12 Esta operação consiste na procura de um determinado dado na lista. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista, e o dado a ser procurado, denominado aqui de chave, e retorna um ponteiro apontando para o elemento que contem o dado. Se o dado procurado não estiver presente na lista, a função retorna o valor NULL. void *procurar(struct elemento *pinicio, char chave) { struct elemento *p1; p1 = pinicio; while ((p1 != NULL) && (p1->dado != chave)) p1 = p1->prox; return p1; } e) Inserção de um dado em uma lista ordenada Esta operação consiste em inserir um novo dado na lista, no lugar que lhe corresponda. No exemplo a lista está ordenada em ordem crescente. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista, e o dado a ser inserido, denominado aqui de dadonovo. void inserir(struct elemento **pinicio, char dadonovo){ struct elemento *p1, *p2; p1 = malloc (sizeof (struct elemento)); p1->dado = dadonovo; if (*pinicio == NULL) { *pinicio = p1; p1->prox = NULL; p1->ant = NULL; } else if ((*pinicio)->dado > dadonovo) { p1->prox = *pinicio; p1->ant = NULL; (*pinicio)->ant = p1; *pinicio = p1; } else { p2 = *pinicio; while ((p2->prox != NULL) && (p2->prox->dado < dadonovo)) p2 = p2->prox; p1->prox = p2->prox; p1->ant = p2; if (p2->prox != NULL) p2->prox->ant = p1; p2->prox = p1; } return; } f) Remoção de um dado de uma lista Esta operação consiste em remover um determinado dado da lista. Se o dado não esti- ver presente, a função não faz nada. Se o dado estiver presente várias vezes na lista, todos os elementos correspondentes serão removidos. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista, e o dado a ser removido, aqui denominado chave. void remover(struct elemento **pinicio, char chave){ struct elemento *p1, *p2; int achou; if (*pinicio == NULL) printf("lista vazia \n"); else { Tira Teima Tira Teima Tira Teima 13 p1 = *pinicio; achou = 0; while (p1 != NULL) { if (p1->dado == chave) { if (*pinicio == p1) *pinicio = (*pinicio)->prox; p2 = p1->prox; if (p1->prox != NULL) p1->prox->ant = p1->ant; if (p1->ant != NULL) p1->ant->prox = p1->prox; free (p1); p1 = p2; achou =1; } else p1 = p1->prox; } if (!achou) printf ("elemento nao presente \n"); } return; } g) Remoção de toda a lista Esta operação consiste em desfazer totalmente uma lista, removendo todos os elemen- tos, um a um, começando pelo início da lista. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista. void removertudo(struct elemento **pinicio){ struct elemento *p1; while (*pinicio != NULL) { p1 = *pinicio; *pinicio = (*pinicio)->prox; free (p1); } return; } 3.4.3 Listas Fechadas com Encadeamento Simples a) Construção com inserção dos elementos no início da lista Neste caso, cada dado lido do arquivo é inserido no começo da lista. Dessa forma, a ordem dos elementos na lista fica invertida em relação à ordem original do arquivo. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista.void construir1(struct elemento **pinicio) { FILE *arq; struct elemento *p1, *p2; char c; arq = fopen ("t1.txt", "r"); *pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; if (*pinicio == NULL) { p1->prox = p1; *pinicio = p1; } else { p1->prox = *pinicio; p2 = *pinicio; while (p2->prox != *pinicio) Tira Teima Tira Teima 14 p2 = p2->prox; p2->prox = p1; *pinicio = p1; } } } fclose (arq); return; } b) Construção com inserção dos elementos no final da lista Neste caso, cada dado lido do arquivo é inserido no final da lista. Dessa forma, a or- dem dos elementos na lista fica idêntica à ordem original do arquivo. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista. void construir2(struct elemento **pinicio) { FILE *arq; struct elemento *p1, *p2; char c; arq = fopen ("t5.txt", "r"); *pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; if (*pinicio == NULL) *pinicio = p1; else p2->prox = p1; p2 = p1; p1->prox = *pinicio; } } fclose (arq); return; } c) Percurso por todos os elementos da lista Esta operação consiste em percorrer todos os elementos da lista, realizando alguma ação em cada um. Neste exemplo a ação consiste em escrever o dado na tela. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista. void percorrer(struct elemento *pinicio) { struct elemento *p1; if (pinicio == NULL) printf ("lista vazia \n"); else { p1 = pinicio; do { printf("elemento: %c \n", p1->dado); p1 = p1->prox; } while (p1 != pinicio); } return; } d) Procura de um dado na lista Esta operação consiste na procura de um determinado dado na lista. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista, e o dado a ser procurado, Tira Teima Tira Teima 15 denominado aqui de chave, e retorna um ponteiro apontando para o elemento que contem o dado. Se o dado procurado não estiver presente na lista, a função retorna o valor NULL. void *procurar(struct elemento *pinicio, char chave) { struct elemento *p1; if (pinicio == NULL) p1 = NULL; else { p1 = pinicio; do p1 = p1->prox; while ((p1 != pinicio) && (p1->dado != chave)); if ((p1 == pinicio) && (p1->dado != chave)) p1 = NULL; } return p1; } e) Inserção de um dado em uma lista ordenada Esta operação consiste em inserir um novo dado na lista, no lugar que lhe corresponda. No exemplo a lista está ordenada em ordem crescente. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista, e o dado a ser inserido, denominado aqui de dadonovo. void inserir(struct elemento **pinicio, char dadonovo){ struct elemento *p1, *p2; p1 = malloc (sizeof (struct elemento)); p1->dado = dadonovo; if (*pinicio == NULL) { p1->prox = p1; *pinicio = p1; } else if ((*pinicio)->dado > dadonovo) { p1->prox = *pinicio; p2 = *pinicio; while (p2->prox != *pinicio) p2 = p2->prox; p2->prox = p1; *pinicio = p1; } else { p2 = *pinicio; while ((p2->prox != *pinicio) && (p2->prox->dado < dadonovo)) p2 = p2->prox; p1->prox = p2->prox; p2->prox = p1; } return; } f) Remoção de um dado de uma lista Esta operação consiste em remover um determinado dado da lista. Se o dado não esti- ver presente, a função não faz nada. Se o dado estiver presente várias vezes na lista, todos os elementos correspondentes serão removidos. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista, e o dado a ser removido, aqui denominado chave. void remover(struct elemento **pinicio, char chave){ struct elemento *p1, *p2; int achou; if (*pinicio == NULL) Tira Teima Tira Teima Tira Teima 16 printf("lista vazia \n"); else { achou = 0; while ((*pinicio != NULL) && ((*pinicio)->dado == chave)) { if ((*pinicio)->prox == *pinicio) { free (*pinicio); *pinicio = NULL; achou = 1; } else { p1 = *pinicio; *pinicio = (*pinicio)->prox; p2 = *pinicio; while (p2->prox != p1) p2 = p2->prox; p2->prox = p1->prox; free (p1); p1 = p2; achou = 1; } } if ((*pinicio != NULL) && ((*pinicio)->prox != NULL)) { p1 = (*pinicio)->prox; while (p1 != *pinicio) if (p1->dado == chave) { p2 = *pinicio; while (p2->prox != p1) p2 = p2->prox; p2->prox = p1->prox; free (p1); p1 = p2->prox; achou = 1; } else p1 = p1->prox; } } return; } g) Remoção de toda a lista Esta operação consiste em desfazer totalmente uma lista, removendo todos os elemen- tos, um a um, começando pelo início da lista. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista. void removertudo(struct elemento **pinicio){ struct elemento *p1; if (*pinicio != NULL) { p1 = (*pinicio)->prox; while (p1 != *pinicio) { (*pinicio)->prox = p1->prox; free (p1); p1 = (*pinicio)->prox; } free (*pinicio); *pinicio = NULL; } return; } 3.4.4 Listas Fechadas com Encadeamento Duplo a) Construção com inserção dos elementos no início da lista Tira Teima 17 Neste caso, cada dado lido do arquivo é inserido no começo da lista. Dessa forma, a ordem dos elementos na lista fica invertida em relação à ordem original do arquivo. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista. void construir1(struct elemento **pinicio) { FILE *arq; struct elemento *p1; char c; arq = fopen ("t5.txt", "r"); *pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; if (*pinicio == NULL) { p1->prox = p1; p1->ant = p1; *pinicio = p1; } else { p1->prox = *pinicio; p1->ant = (*pinicio)->ant; (*pinicio)->ant->prox = p1; (*pinicio)->ant = p1; *pinicio = p1; } } } fclose (arq); return; } b) Construção com inserção dos elementos no final da lista Neste caso, cada dado lido do arquivo é inserido no final da lista. Dessa forma, a or- dem dos elementos na lista fica idêntica à ordem original do arquivo. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista. void construir2(struct elemento **pinicio) { FILE *arq; struct elemento *p1, *p2; char c; arq = fopen ("t5.txt", "r"); *pinicio = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; if (*pinicio == NULL) { p1->prox = p1; p1->ant = p1; *pinicio = p1; } else { p1->prox = *pinicio; p1->ant = p2;p2->prox = p1; (*pinicio)->ant = p1; } p2 = p1; } } fclose (arq); return; } c) Percurso por todos os elementos da lista Tira Teima Tira Teima 18 Esta operação consiste em percorrer todos os elementos da lista, realizando alguma ação em cada um. Neste exemplo a ação consiste em escrever o dado na tela. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista. void percorrer(struct elemento *pinicio) { void percorrer(struct elemento *pinicio) { struct elemento *p1; if (pinicio == NULL) printf ("lista vazia \n"); else { p1 = pinicio; do { printf("elemento: %c \n", p1->dado); p1 = p1->prox; } while (p1 != pinicio); do { p1 = p1->ant; printf("elemento (para tras): %c \n", p1->dado); } while (p1 != pinicio); } return; } d) Procura de um dado na lista Esta operação consiste na procura de um determinado dado na lista. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista, e o dado a ser procurado, denominado aqui de chave, e retorna um ponteiro apontando para o elemento que contem o dado. Se o dado procurado não estiver presente na lista, a função retorna o valor NULL. void *procurar(struct elemento *pinicio, char chave) { struct elemento *p1; if (pinicio == NULL) p1 = NULL; else { p1 = pinicio; do p1 = p1->prox; while ((p1 != pinicio) && (p1->dado != chave)); if ((p1 == pinicio) && (p1->dado != chave)) p1 = NULL; } return p1; } e) Inserção de um dado em uma lista ordenada Esta operação consiste em inserir um novo dado na lista, no lugar que lhe corresponda. No exemplo a lista está ordenada em ordem crescente. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista, e o dado a ser inserido, denominado aqui de dadonovo. void inserir(struct elemento **pinicio, char dadonovo){ struct elemento *p1, *p2; p1 = malloc (sizeof (struct elemento)); p1->dado = dadonovo; if (*pinicio == NULL) { p1->prox = p1; p1->ant = p1; *pinicio = p1; } else Tira Teima Tira Teima Tira Teima 19 if ((*pinicio)->dado > dadonovo) { p1->prox = *pinicio; p1->ant = (*pinicio)->ant; (*pinicio)->ant->prox = p1; (*pinicio)->ant = p1; *pinicio = p1; } else { p2 = *pinicio; while ((p2->prox != *pinicio) && (p2->prox->dado < dadonovo)) p2 = p2->prox; p1->prox = p2->prox; p1->ant = p2; p2->prox->ant = p1; p2->prox = p1; } return; } f) Remoção de um dado de uma lista Esta operação consiste em remover um determinado dado da lista. Se o dado não esti- ver presente, a função não faz nada. Se o dado estiver presente várias vezes na lista, todos os elementos correspondentes serão removidos. A função recebe como parâmetro o endereço do ponteiro pinicio, que aponta para o início da lista, e o dado a ser removido, aqui denominado chave. void remover(struct elemento **pinicio, char chave){ struct elemento *p1, *p2; int achou; if (*pinicio == NULL) printf("lista vazia \n"); else { achou = 0; while ((*pinicio != NULL) && ((*pinicio)->dado == chave)) { if ((*pinicio)->prox == *pinicio) { free (*pinicio); *pinicio = NULL; achou = 1; } else { p1 = *pinicio; *pinicio = (*pinicio)->prox; p2 = *pinicio; while (p2->prox != p1) p2 = p2->prox; p2->prox = p1->prox; p1->prox->ant = p2; free (p1); p1 = p2; achou = 1; } } if ((*pinicio != NULL) && ((*pinicio)->prox != NULL)) { p1 = (*pinicio)->prox; while (p1 != *pinicio) if (p1->dado == chave) { p2 = *pinicio; while (p2->prox != p1) p2 = p2->prox; p2->prox = p1->prox; p1->prox->ant = p2; free (p1); p1 = p2->prox; achou = 1; } else p1 = p1->prox; } } return; Tira Teima 20 } g) Remoção de toda a lista Esta operação consiste em desfazer totalmente uma lista, removendo todos os elemen- tos, um a um, começando pelo início da lista. A função recebe como parâmetro o ponteiro pinicio, que aponta para o início da lista. void removertudo(struct elemento **pinicio){ struct elemento *p1; if (*pinicio != NULL) { p1 = (*pinicio)->prox; while (p1 != *pinicio) { (*pinicio)->prox = p1->prox; free (p1); p1 = (*pinicio)->prox; } free (*pinicio); *pinicio = NULL; } return; } Tira Teima 21 3.5 Aplicações Neste tópico serão apresentadas algumas aplicações de listas. Algumas dessas aplica- ções são apenas algoritmos aplicados sobre as listas já estudadas nos tópicos anteriores, como é o caso da ordenação. Outras aplicações são constituídas por novas formas de construir lis- tas, como é o caso das listas com descritor e das listas ligadas a listas. 3.5.1 Ordenação dos dados em uma lista sem alterar a estrutura da lista São apresentados dois exemplos. No primeiro, trocam-se os conteúdos dos elementos na lista , sem alterar a ordem dos elementos da lista. No segundo trocam-se as posições dos elementos na lista, através da realocação dos ponteiros. Em ambos utiliza-se o algoritmo de Seleção Direta. [O algoritmo de Seleção Direta funciona do seguinte modo: percorre-se o conjunto de elementos, do primeiro ao último, selecionando-se o menor entre eles, e colocando-o na primeira posição. Em se- guida percorre-se o conjunto formado pelos elementos entre a segunda posição e a última, selecio- nando-se o menor deles, e colocando-o na segunda posição. Assim, sucessivamente, selecionam-se os menores elementos dos conjuntos restantes, até que esse conjunto fique vazio, e todos os ele- mentos estejam ordenados.] Exemplo 1 Neste caso, trocam-se os conteúdos dos elementos na lista, sem alterar a ordem dos elementos na lista. Se os dados de cada elemento da lista não ocupam muita memória, este algoritmo é mais adequado. No exemplo, a lista é aberta com encadeamento simples. No caso o ponteiro p1 ocupa sempre a posição em que será colocado o menor elemento do conjunto ainda desor- denado. O ponteiro p2 percorre esse conjunto, à procura do menor elemento, e p3 aponta sempre para o menor elemento encontrado, até que a varredura termine. Em seguida são tro- cados os dados dos elementos apontados por p1 e p3. void ordenarSelecaoDireta(struct elemento *pinicio) { struct elemento *p1, *p2, *p3; char aux; if (pinicio != NULL) { p1 = pinicio; while (p1->prox != NULL) { p3 = p1; p2 = p1->prox; while (p2 != NULL) { if (p3->dado > p2->dado) p3 = p2; p2 = p2->prox; } if (p1 != p3) { aux = p3->dado; p3->dado = p1->dado; p1->dado = aux; } p1 = p1->prox; } } return; } Exemplo 2 Neste caso trocam-se as posições dos elementos na lista, através da realocação dos ponteiros. Este algoritmo é mais adequado na situação em que os dados da lista ocupam muito Tira Teima 22 espaço, tornando computacionalmente a realocação dos ponteiros mais barata que a troca dos dados. Neste exemplo a lista é fechada com encadeamento duplo. void *ordenarAlteraPonteiros(struct elemento *pinicio) { struct elemento*p1, *p2, *p3, *paux; if (pinicio != NULL) { p1 = pinicio; while (p1->prox != NULL) { p2 = p1->prox; while (p2 != NULL) { if (p1->dado > p2->dado) { if (p1->prox == p2) { p1->prox = p2->prox; p2->ant = p1->ant; if (!(p2->prox == NULL)) p2->prox->ant = p1; if (!(p1->ant == NULL)) p1->ant->prox = p2; p2->prox = p1; p1->ant = p2; if (pinicio == p1) pinicio = p2; paux = p1; p1 = p2; p2 = paux; } else { p2->ant->prox = p1; p1->prox->ant = p2; if (!(p2->prox == NULL)) p2->prox->ant = p1; if (!(p1->ant == NULL)) p1->ant->prox = p2; paux = p1->ant; p1->ant = p2->ant; p2->ant = paux; paux = p1->prox; p1->prox = p2->prox; p2->prox = paux; if (pinicio == p1) pinicio = p2; paux = p1; p1 = p2; p2 = paux; } } p2 = p2->prox; } p1 = p1->prox; } } return pinicio; } 3.5.2 Listas com descritor As listas com descritor consistem em listas com um elemento especial, que não guarda dados como os outros elementos, mas apenas facilita o acesso à lista. Alguns algoritmos ficam simplificados através desse recurso, que pode ser aplicado a qualquer dos tipos de listas vistos anteriormente. Apresentamos aqui dois exemplos. No primeiro, o nó descritor é do mesmo tipo que os outros nós da lista, mas não contém dados (esse nó também é conhecido como "nó bobo"). A simplificação que se consegue em alguns algoritmos decorre da facilidade no tratamento de elementos da lista colocados em posições especiais, como no começo ou no final da lista. Tira Teima 23 No segundo exemplo, o nó descritor é completamente diferente dos outros nós da lista, podendo eventualmente conter outras informações sobre a lista. No caso, o elemento descritor foi construído com dois ponteiros de acesso à lista: um apontando para o começo e outro para o final da lista. Desse modo, ficam simplificados os algoritmos que atuam no final da lista. Exemplo 1 O nó descritor é do mesmo tipo que os outros nós da lista, que é aberta e com encade- amento simples. A função do exemplo faz a inserção de um novo elemento no início da lista. Tenha em conta que neste caso, uma lista vazia é formada pelo “nó bobo”; portanto pinicio não é NULL. void *inserir(struct elemento *pinicio, char dadonovo){ struct elemento *p1, *p2; p1 = malloc (sizeof (struct elemento)); p1->dado = dadonovo; p1->prox = NULL; if (pinicio->prox == NULL) pinicio->prox = p1; else { p2 = pinicio; while ((p2->prox != NULL) && (p2->prox->dado < dadonovo)) p2 = p2->prox; p1->prox = p2->prox; p2->prox = p1; } return pinicio; } Exemplo 2 O nó descritor neste caso é um elemento diferente dos outros elementos da lista. Ele contém apenas dois ponteiros, um apontando para o primeiro elemento da lista e o outro apon- tando para o último. A lista é aberta e com encadeamento simples. A função faz a construção da lista inserindo os elementos no final. [struct descritor { struct elemento *acessoinicio, *acessofim; } ; struct descritor *pdescr; ] void *construir2() { FILE *arq; struct elemento *p1, *p2; struct descritor *pdescritor; char c; pdescritor = malloc (sizeof (struct descritor)); pdescritor->acessoinicio = NULL; pdescritor->acessofim = NULL; arq = fopen ("t1.txt", "r"); while ((c = getc (arq)) != EOF) { if (c != '\n'){ p1 = malloc (sizeof (struct elemento)); p1->dado = c; if (pdescritor->acessoinicio == NULL) pdescritor->acessoinicio = p1; else p2->prox = p1; p2 = p1; } } Tira Teima Tira Teima 24 if (pdescritor->acessoinicio != NULL) { p1->prox = NULL; pdescritor->acessofim = p1; } fclose (arq); return pdescritor; } 3.5.3 Listas ligadas a listas Há situações em que, em cada elemento de uma lista, um dos campos é um ponteiro que dá acesso a outra lista. A estrutura assim formada já não é uma lista, mas um conjunto de listas ligadas a outra lista. Neste exemplo, a primeira lista é formada por elementos que identificam centrais tele- fônicas, através de letras (A, B, C ...). A cada central telefônica está ligada uma lista formada por elementos que identificam números de telefone (110, 111, ...). O algoritmo usado como exemplo faz a montagem de toda a estrutura a partir de um arquivo text. O arquivo contém, em cada linha, a letra identificadora da central, seguida por um número inteiro que indica quantos telefones existem nessa central, e em seguida os números dos telefones correspondentes. Note que a quantidade de telefones em cada central é variável (podendo ser também nula). O acesso a toda a estrutura é feito através de um único ponteiro (pacesso), que aponta para o início da lista de centrais. O acesso às listas de telefones é feito, para cada central, através do ponteiro que aponta para a lista de telefones correspondente a essa central. --------------------------------------- [ conteúdo das hotwords no programa:] struct elem_fone { int fone; struct elem_fone *proxf; } ; struct elem_central { char central; struct elem_central *proxc; struct elem_fone *pfone; } ; struct elem_central *pac; ----------------------------------------------- void *construirMalha(struct elem_central *pacesso) { FILE *arq; struct elem_central *pc1, *pc2; struct elem_fone *pf1, *pf2; char c; int i,n,m; arq = fopen ("t3.txt", "r"); pacesso = NULL; while ((c = getc (arq)) != EOF) { if (c != '\n'){ pc1 = malloc (sizeof (struct elem_central)); pc1->central = c; Tira Teima 25 pc1->pfone = NULL; pc1->proxc = NULL; if (pacesso == NULL) pacesso = pc1; else pc2->proxc = pc1; pc2 = pc1; fscanf (arq, "%d ", &n); for (i=1; i !=n+1; i++) { fscanf (arq, "%d ", &m); pf1 = malloc (sizeof (struct elem_fone)); pf1->fone = m; pf1->proxf = NULL; if (pc1->pfone == NULL) pc1->pfone = pf1; else pf2->proxf = pf1; pf2 = pf1; } } } fclose (arq); return pacesso; } 26 [página de conclusão] Neste capítulo estudamos as listas, que são as estruturas de dados mais simples deste curso. No entanto, se forem bem compreendidas, seu conhecimento poderá resolver inúmeros problemas computacionais que exigem gerenciamento dinâmico de memória, além de prepa- rar o aluno para aprender com mais facilidade as outras estruturas que serão apresentadas a seguir. 1 ED – esquema para tutorial 03 Capítulo 3 – Pilhas e Filas [página de abertura] Ao concluir este capítulo, você deverá ser capaz de: - compreender o que são as pilhas e filas - conhecer as operações básicas que se podem efetuar com as pilhas e com as filas - tomar contato com algumas aplicações de pilhas e de filas 2 4.1 Introdução Para certas aplicações, tornam-se úteis estruturas de dados constituídas por conjuntos de elementos organizados, não pelos valores dos dados, mas em função de uma determinado critério que regulamenta a entrada e a saída dos dados na estrutura. Os critérios mais usados para regular a entrada e a saídados dados são: - lifo – last in, first out – dentre os elementos presentes na estrutura, o primeiro a sair dela será o último que nela entrou (como acontece com uma pilha de pratos: sempre se retira o de cima, que foi o último a entrar na pilha, caso contrário pode-se provocar um acidente). - fifo – first in, first out – dentre os elementos presentes na estrutura, o primeiro a sair dela será o primeiro que nela entrou (como acontece com uma fila de pessoas em um guichê: será atendida em primeiro lugar aquela que está há mais tempo na fila) {repetir aqui a figura da fila de pessoas, do módulo de introdução} O critério LIFO dá origem à estrutura de dados denominada Pilha, e o critério FIFO dá origem à estrutura Fila. As operações básicas que se podem realizar sobre uma pilha são: - inicializar a pilha - verificar se a pilha está vazia - retornar o elemento que está no topo da pilha - inserir um elemento na pilha - retirar um elemento da pilha As operações básicas que se podem realizar sobre uma fila são: - inicializar a fila - verificar se a fila está vazia - retornar o elemento que está na primeira posição da fila - inserir um elemento na fila - retirar um elemento da fila 3 4.2 Implementação Como já foi dito, as estruturas de dados são abstratas, e portanto podem ser imple- mentadas de diversos modos. [As estruturas abstratas devem ser imaginadas como uma forma de organizar e relacionar dados na memória do sistema, de modo a permitir certas operações sobre esses dados, independentemente do modo como essas estruturas são implementadas. ] Em nosso caso, faremos a implementação das pilhas e das filas através de listas aber- tas com encadeamento simples. O tipo dos elementos será, portanto, o mesmo para os dois casos, ficando a diferença apenas por conta das operações que se realizam em cada caso. É importante observar que, uma vez implementada a lista que representa uma pilha ou uma fila, o usuário da estrutura (pilha ou fila) apenas tem acesso às operações pré-definidas, não po- dendo executar outras operações que seriam possíveis em listas. Ou seja, o usuário da pilha ou da fila não precisa saber como a estrutura foi implementada, apenas precisa saber utilizar as funções que permitem as operações básicas. Para exemplificar a implementação das operações básicas utilizaremos as estruturas abaixo, nas quais o dado que se armazena é um caractere: struct tipopilha { char dado; struct tipopilha *prox; } ; struct tipofila { char dado; struct tipofila *prox; } ; 4.2.1 Operações Básicas em Pilhas a) Inicializar a Pilha Esta função consiste apenas em inicializar a pilha, ou seja, definir uma pilha vazia, que fica apta a ser utilizada. Recebe como parâmetro o endereço para o ponteiro pilha, que é um ponteiro que dá acesso à pilha. void InicializaPilha (struct tipopilha **pilha) { *pilha = NULL; return ; } b) Verificar se a Pilha está vazia Esta função recebe como parâmetro o ponteiro pilha, que dá acesso à pilha, e retorna 1 se a pilha estiver vazia, e 0 em caso contrário. int PilhaVazia (struct tipopilha *pilha) { if (pilha == NULL) Tira Teima 4 return 1; else return 0; } c) Retornar o elemento que está no topo da pilha Esta função recebe como parâmetro o ponteiro pilha, que dá acesso à pilha, e retorna o elemento que estiver no topo, sem retirá-lo da pilha. Esta operação só pode ser usada quando se tem certeza de que a pilha não é vazia, o que se pode testar com a função PilhaVazia. char TopoPilha(struct tipopilha *pilha) { return pilha->dado; } d) Inserir um elemento na Pilha Esta função recebe como parâmetros o valor dadonovo, a ser inserido, e o endereço do ponteiro pilha, que dá acesso à pilha. O resultado da operação é a inserção do dadonovo no topo da pilha. void InserePilha(struct tipopilha **pilha, char dadonovo) { struct tipopilha *p1; p1 = malloc (sizeof (struct tipopilha)); p1->dado = dadonovo; p1->prox = *pilha; *pilha = p1; return; } e) Retirar um elemento da Pilha Esta função recebe como parâmetro o endereço do ponteiro pilha, que dá acesso à pi- lha, e retorna o elemento que está no topo da pilha, retirando-o dela. Esta operação só pode ser usada quando se tem certeza de que a pilha não é vazia, o que se pode testar com a função PilhaVazia. char RetiraPilha(struct tipopilha **pilha) { struct tipopilha *p1; char car; p1 = *pilha; *pilha = p1->prox; car = p1->dado; free (p1); return car; } 4.2.2 Operações Básicas em Filas a) Inicializar a Fila Tira Teima Tira Teima Tira Teima Tira Teima 5 Esta função consiste apenas em inicializar a fila, ou seja, definir uma fila vazia, que fica apta a ser utilizada. Recebe como parâmetro o endereço para o ponteiro fila, que é um ponteiro que dá acesso à fila. void InicializaFila (struct tipofila **fila) { *fila = NULL; return; } b) Verificar se a Fila está vazia Esta função recebe como parâmetro o ponteiro fila, que dá acesso à fila, e retorna 1 se a fila estiver vazia, e 0 em caso contrário. int FilaVazia (struct tipofila *fila) { if (fila == NULL) return 1; else return 0; } c) Retornar o elemento que está na primeira posição da fila Esta função recebe como parâmetro o ponteiro fila, que dá acesso à fila, e retorna o elemento que estiver na primeira posição, sem retirá-lo da fila. Esta operação só pode ser usa- da quando se tem certeza de que a fila não é vazia, o que se pode testar com a função FilaVa- zia. char FrenteFila(struct tipofila *fila) { return fila->dado; } d) Inserir um elemento na Fila Esta função recebe como parâmetros o valor dadonovo, a ser inserido, e o endereço do ponteiro fila, que dá acesso à fila. O resultado da operação é a inserção do dadonovo no fim da fila. void InsereFila(struct tipofila **fila, char dadonovo) { struct tipofila *f1, *f2; f1 = malloc (sizeof (struct tipofila)); f1->dado = dadonovo; f1->prox = NULL; if (*fila == NULL) *fila = f1; else { f2 = *fila; while (f2->prox != NULL) f2 = f2->prox; f2->prox = f1; } return; } e) Retirar um elemento da Fila Tira Teima Tira Teima Tira Teima Tira Teima 6 Esta função recebe como parâmetro o endereço do ponteiro fila, que dá acesso à fila, e retorna o elemento que está na primeira posição da fila, retirando-o dela. Esta operação só pode ser usada quando se tem certeza de que a fila não é vazia, o que se pode testar com a função FilaVazia. char RetiraFila(struct tipofila **fila) { struct tipofila *f1; char car; f1 = *fila; *fila = f1->prox; car = f1->dado; free (f1); return car; } Tira Teima 7 4.3 Aplicações Serão apresentadas agora algumas aplicações de pilhas e de filas. A partir deste ponto, as pilhas e as filas, por serem estruturas abstratas, serão manipuladas sempre através das ope- rações vistas anteriormente (InsereFila, RetiraPilha, etc...) Desta forma o usuário não precisa saber qual foi a implementação utilizada em cada caso, e não tem outro acesso às estruturas utilizadas, que não sejam as operações pré-definidas. Veremos as seguintes aplicações: - manipulação de uma seqüência de caracteres - avaliação de expressão totalmente parentetizada - avaliação de expressão na forma pós-fixada - conversão de expressão in-fixada para pós-fixada - simulação de recursividade - pilhas ou listas de uso geral 4.3.1 Manipulação de uma seqüência de caracteres É dada uma seqüência de caracteres formada por letras e algarismosalternados, come- çando por uma letra. A função deve construir outra seqüência que contenha todas as letras da seqüência dada, na mesma ordem e posições originais, e todos os algarismos da seqüência dada, nas posições originais, mas em ordem invertida. Como exemplo são dadas abaixo uma seqüência de entrada e outra de saída correspondente: Entrada: A 5 F 8 B 4 C 9 D 7 Saída: A 7 F 9 B 4 C 8 D 5 A função ManipulaCaracteres supõe que tenham sido definidos anteriormente os tipos tipofila e tipopilha. A seqüência original é lida do arquivo arq e a seqüência de saída é escri- ta na tela. [struct tipopilha { char dado; struct tipopilha *prox; };] [struct tipofila { char dado; struct tipofila *prox; };] void ManipulaCaracteres () { struct tipofila *f1; struct tipopilha *p1; char x; FILE *arq; InicializaFila (&f1); Tira Teima 8 InicializaPilha (&p1); arq = fopen ("t2.txt", "r"); while (!feof(arq)){ x = getc(arq); if ((x!='\n')&& (x!=EOF)) InsereFila (&f1, x); x = getc(arq); if ((x!='\n')&& (x!=EOF)) InserePilha (&p1, x); } fclose (arq); while ((!FilaVazia(f1)) && (!PilhaVazia(p1))) { printf ("%c", RetiraFila (&f1)); printf ("%c", RetiraPilha(&p1)); } } 4.3.2 Avaliação de expressão totalmente parentetizada É dada uma expressão numérica em que todas as operações são cercadas por parênte- sis, independentemente de sua prioridade, como por exemplo: ( 2 * ( 5 - 1 ) ) Supõem-se definidos anteriormente os tipos tipopilha e tipopilha2. A expressão é lida do arquivo arq. A função AvaliaExpressaoParentetizada utiliza a função ResolveOperacao, que retira da pilha de operadores o último operador e, em seguida, realiza a operação desejada e volta a inserir o resultado na pilha de números. void AvaliaExpressaoParentetizada () { struct tipopilha *p1, *p2; char car; int i; FILE *arq; //------------ void ResolveOperacao () { char op, c1, c2, caux; int x,y; if (PilhaVazia (p2)) { if (PilhaVazia (p1)) { caux = 0+'0'; InserePilha (&p1,0+'0'); } } else { op = RetiraPilha (&p2); c1 = RetiraPilha (&p1); x = c1 - '0'; c2 = RetiraPilha (&p1); y = c2 - '0'; switch (op) { case '+' : InserePilha (&p1, y+x+'0'); break; case '-' : InserePilha (&p1, y-x+'0'); break; case '*' : InserePilha (&p1, y*x+'0'); break; case '/' : InserePilha (&p1, y%x+'0'); break; } } } //------------ InicializaPilha (&p1); InicializaPilha (&p2); arq = fopen ("t3.txt", "r"); while ((car = getc (arq)) != EOF) { if (car != '\n') if ((car>='0')&&(car<='9')) InserePilha (&p1, car); else if (car == ')') Tira Teima 9 ResolveOperacao (); else if (car != '(') InserePilha (&p2, car); } fclose (arq); printf("%d \n", RetiraPilha (&p1)-'0'); } 4.3.3 Avaliação de expressão na forma pós-fixada A função deve fazer a avaliação de uma expressão dada em forma pós-fixada. A ex- pressão se encontra no arquivo arq. A função supõe que tenha sido definido o tipo tipopilha. A função AvaliaExpressaoPosfixada utiliza a função ResolveOperacao, que executa a opera- ção desejada entre dois operandos e retorna o resultado. [ Uma expressão algébrica é dita em forma pós-fixada, ou notação polonesa, quando o opera- dor é colocado depois dos dois operandos. Vejamos alguns exemplos: notação in-fixada notação pós-fixada A + B A B + (A + B) * C A B + C * (A * B + C) / D A B * C + D / Uma das vantagens da notação polonesa é prescindir do uso de parênteses. ] void AvaliaExpressaoPosFixada () { struct tipopilha *p1; char c, c1, c2; FILE *arq; int w; //------------ int ResolveOperacao (char c1, char c, char c2) { int x, y, z; x = c1 - '0'; y = c2 - '0'; switch (c) { case '+' : z = x+y; break; case '-' : z = x-y; break; case '*' : z = x*y; break; case '/' : z = x/y; break; } return z; } //------------ InicializaPilha (&p1); arq = fopen ("t6.txt", "r"); while ((c = getc (arq)) != EOF) { if (c != '\n'){ if ((c!='+')&&(c!='-')&&(c!='*')&&(c!='/')) InserePilha (&p1, c); else { c2 = RetiraPilha (&p1); c1 = RetiraPilha (&p1); w = ResolveOperacao (c1, c, c2); InserePilha (&p1, w+'0'); } } } printf ("resultado: %d \n", RetiraPilha (&p1)-'0'); fclose (arq); } Tira Teima 10 4.3.4 Conversão de expressão in-fixada para pós-fixada A função ConversaoParaPosFixada lê de um arquivo arq1 uma expressão algébrica em forma in-fixada, e escreve na tela a mesma expressão, na forma pós-fixada. A função utiliza os tipos tipopilha e tipofila. A fila f1 armazena os operandos e a pilha p1 armazena os operadores e o sinal ‘(‘. A função Prioridade retorna a prioridade de execução de uma operação. A variável aux serve apenas para eliminar o caractere ‘(‘ quando este for encontra- do no topo da pilha p1. [tabela de prioridades 1. exponenciação 2. multiplicação e divisão 3. soma e subtração 4. sinal '(' ] void ConversaoParaPosFixada () { struct tipopilha *p1; struct tipofila *f1; char c, aux; FILE *arq; //------------ int Prioridade (char c1) { switch (c1) { case '^' : return 1; break; case '*' : case '/' : return 2; break; case '+' : case '-' : return 3; break; case '(' : return 4; break; } } //------------ InicializaPilha (&p1); InicializaFila (&f1); arq = fopen ("t8.txt", "r"); while ((c = getc (arq)) != EOF) { if (c != '\n'){ if ((c!='^')&&(c!='+')&&(c!='-')&&(c!='*')&&(c!='/')&&(c!='(')&&(c!=')')) InsereFila (&f1, c); else if (c==')') { while (TopoPilha(p1) != '(') InsereFila (&f1, RetiraPilha (&p1)); aux = RetiraPilha (&p1); } else { if ((c!='(') && (!PilhaVazia (p1))){ while ((Prioridade (TopoPilha (p1)) <= Prioridade (c))&&(!PilhaVazia (p1))) InsereFila (&f1, RetiraPilha (&p1)); } InserePilha (&p1,c); } } } fclose (arq); while (!PilhaVazia (p1)) InsereFila (&f1, RetiraPilha (&p1)); while (!FilaVazia (f1)) printf ("%c", RetiraFila (&f1)); } Tira Teima 11 4.3.5 Simulação de recursividade As pilhas podem ser utilizadas para simular a recursividade na resolução de certos problemas. É útil entender como as pilhas permitem a recursividade, pois é através delas que as linguagens implementam a recursividade. Como exemplo tomemos a geração da seqüência de Fibonacci. A seqüência de Fibonacci pode ser definida do seguinte modo: fib (0) = 1 fib (1) = 1 fib (n) = fib (n-1) + fib (n-2) , para n > 1 A seqüência gerada de acordo com esta definição é a seguinte: n 0 1 2 3 4 5 6 ... fib 1 1 2 3 5 8 13 ... A função abaixo calcula o n-ésimo termo, que fica armazenado na variável fib. Supõe-se defi- nido o tipo tipopilha. int Fibonacci (int n) { struct tipopilha *p1; int fib, x; InicializaPilha (&p1); InserePilha (&p1, n); fib = 0; while (!PilhaVazia (p1)) { x = RetiraPilha (&p1); if ((x == 0) || (x == 1)) fib = fib + 1; else { InserePilha (&p1, x-1); InserePilha (&p1, x-2); } } return fib; } 4.3.6 Pilhas ou listas de usogeral Em todas as aplicações vistas até aqui, sempre se supõe definidos os tipos tipopilha e tipofila, que são pilhas ou filas contendo caracteres. É interessante a criação de pilhas ou filas de uso geral, nas quais é possível armazenar qualquer tipo de dado. Isto é possível na lingua- gem C, se utilizarmos um ponteiro apontando para void como informação contida na pilha ou na fila. Este ponteiro é de uso genérico, podendo apontar para qualquer tipo de estrutura. Tira Teima 12 Como exemplo de aplicação de uma pilha de uso geral, vamos resolver o problema das Torres de Hanói, simulando a recursividade através de uma pilha de uso geral. [ O problema das Torres de Hanói é um exemplo clássico de uso de recursividade. São dados três pinos A, B, e C, e um conjunto de discos furados, de diferentes tamanhos, que se encaixam nesses pinos. Os discos estão inicialmente no pino A e devem ser transferidos para o disco B, utilizando o pino C como auxiliar. Tanto na posição inicial como em qualquer posição intermediária, os discos devem estar dispostos de modo que nunca um disco maior fique sobre um disco menor. Os movimen- tos devem ser feitos com um disco de cada vez. ] { FIGURA DE TRÊS PINOS COM 3 DISCOS } Para resolver o problema de forma recursiva, deve-se raciocinar da seguinte forma: para levar n discos de A para B, basta levar os n – 1 discos superiores de A para C, em segui- da levar o disco maior de A para B, e depois levar os n – 1 discos de C para B. No entanto, a operação de transferir n – 1 discos não é permitida pela própria regra do problema, e deve então ser abordada como uma chama da recursiva ao mesmo problema, mas com o grau de dificuldade reduzido de n para n – 1. Definamos, então, o movimento de n discos, do pino A para o pino B, utilizando o pino C como auxiliar, através da função Hanoi (A,B,C,n). Para todo n > 1, esta função pode ser decomposta em outras três: Hanoi (A,C,B,n-1) Hanoi (A,B,C, 1 ) Hanoi (C,B,A,n-1) As três chamadas são recursivas, mas a primeira e a terceira diminuem o número de discos, de n para n-1, enquanto a segunda constitui um caso trivial, em que o número de dis- cos é 1, ou seja, neste caso o movimento do disco é efetuado. Definamos um tipo registro que contenha os parâmetros de Hanoi, do seguinte modo: struct reghanoi { char o; char d; char a; int k; }; Definamos todas as operações de manipulação de uma pilha cujo tipo denominaremos tipopilhageral, que é uma pilha cujo elemento constituinte é um ponteiro que aponta para void, do seguinte modo: struct tipopilhageral { void *dado; struct tipopilhageral *prox; } ; A função Hanoi pode então ser escrita, com a ajuda das funções EmpilhaHanoi e De- sempilhaHanoi, que servem para criar ou eliminar os registros apontados pelos ponteiros da pilha de tipo tipopilhageral. 13 //--------------------------------------------- void Hanoi (char origem, char destino, char auxiliar, int n) { struct tipopilhageral *phanoi; struct reghanoi { char o; char d; char a; int k; }; //--------------------------------------------- void *EmpilhaHanoi (struct tipopilhageral *pilha, char origem, char destino, char auxiliar, int n) { struct reghanoi *paux1; void *paux2; paux1 = malloc (sizeof (struct reghanoi)); paux1->o = origem; paux1->d = destino; paux1->a = auxiliar; paux1->k = n; paux2 = paux1; InserePilha (&pilha, paux2); return pilha; } //--------------------------------------------- void *DesempilhaHanoi (struct tipopilhageral *pilha, char *origem, char *destino, char *auxiliar, int *n) { struct reghanoi *paux1; void *paux2; paux2 = RetiraPilha (&pilha); paux1 = paux2; *origem = paux1->o; *destino = paux1->d; *auxiliar = paux1->a; *n = paux1->k; free (paux2); return pilha; } //---------------------------------------------- InicializaPilha (&phanoi); phanoi = EmpilhaHanoi (phanoi, origem, destino, auxiliar, n); while (!PilhaVazia (phanoi)) { phanoi = DesempilhaHanoi (phanoi, &origem, &destino, &auxiliar, &n); if (n == 1) printf ("movimento de %3c para %3c \n", origem, destino); else { phanoi = EmpilhaHanoi (phanoi, auxiliar, destino, origem, n-1); phanoi = EmpilhaHanoi (phanoi, origem, destino, auxiliar, 1); phanoi = EmpilhaHanoi (phanoi, origem, auxiliar, destino, n-1); } } } Tira Teima 14 [página de conclusão] O estudo das pilhas e filas fornece ao aluno um bom instrumental para resolver diver- sos problemas computacionais, em especial aqueles que envolvem recursividade. Além disso, seu conhecimento é fundamental para auxiliar na solução de alguns problemas que utilizam estruturas que serão vistas nos próximos tópicos. 1 ED – esquema para tutorial 04 Capítulo 4 – Árvores [página de abertura] Ao concluir este capítulo, você deverá ser capaz de: - compreender o que são as árvores - conhecer os algoritmos básicos que utilizam as árvores binárias - representar árvores genéricas por meio de árvores binárias - utilizar árvores binárias de pesquisa - fazer balanceamento de árvores binárias de pesquisa 2 4.1 Conceitos Uma árvore é uma estrutura de dados que se caracteriza por uma relação de hierarquia entre os elementos que a compõem. Exemplos de estruturas em forma de árvore são: - o organograma de uma empresa; - a divisão de um livro em capítulos, seções, tópicos; - a árvore genealógica de uma pessoa. De um modo um pouco mais formal, podemos dizer que uma árvore é um conjunto finito de um ou mais nós, tais que: a) existe um nó denominado raiz; b) os demais nós formam m>=0 conjuntos disjuntos s1, s2, ... sm, tais que cada um desses conjuntos também é uma árvore (denominada sub-árvore). As árvores podem ser representadas de diversos modos, como por exemplo: a) representação hierárquica {inserir desenho tirado, ou do livro, ou do CBT} Neste exemplo, A é a raiz da árvore. Os nós B e C são "filhos" de A, e dão origem a duas sub-árvores, nas quais D e E são "filhos” de B, e F é "filho" de C. b) representação por conjuntos {inserir desenho tirado, ou do livro, ou do CBT} A árvore aqui representada é a mesma do desenho anterior. Neste caso, cada nó é representa- do por um conjunto formado por seus "filhos". c) representação por expressão parentetizada A mesma árvore pode ser representada pela expressão: ( A ( B ( D ( ) E ( ) ) C ( F ( ) ) ) ) Cada conjunto de parêntesis correspondentes contém um nó e seus filhos. Se um nó não tem filhos, ele é seguido por um par de parêntesis sem conteúdo. d) representação por expressão não parentetizada Ainda utilizando a mesma árvore, podemos usar a representação abaixo: A 2 B 2 D 0 E 0 C 1 F 0 Cada nó é seguido por um número que indica a quantidade de filhos desse nó, e em seguida por esses filhos, representados do mesmo modo. 3 Pode-se representar uma árvore de muitos outros modos, mas é interessante notar que, dentre os exemplos apresentados, a representação a) é a que permite uma melhor visualização, e que será utilizada a partir deste ponto. As representações c) e d) não permitem boa visuali- zação da estrutura, mas podem ser úteis para guardar em arquivos os dados de uma árvore. Como, por definição, os sub-conjuntos s1, s2, ... sm são disjuntos, cada nó só pode ter um "pai". Assim, o desenho abaixo, por exemplo, não representa uma árvore: {inserir desenho tirado, ou do livro, ou do CBT} Vejamos agora alguns termos utilizados para identificar algumas características de uma árvoreou de alguma parte dela. {ver o esquema montado no CBT para este caso, tente fazer algo igual ou parecido} - raiz – nó de origem da árvore - folhas – nós que não têm filhos - grau de um nó – número de filhos de cada nó - nível de um nó – por definição, é zero para a raiz, e, para os demais nós, é o número de "linhas" que ligam o nó à raiz - altura da árvore – é o nível mais alto da árvore - floresta – conjunto de árvores disjuntas (eliminando-se a raiz de uma árvore, obtém-se uma floresta 4 4.2 Árvores Binárias Árvores binárias são árvores em que todos os nós têm grau menor ou igual a dois. Desse modo, cada nó pode ter no máximo dois filhos, que são identificados como filho à es- querda e filho à direita. 4.2.1 Caminhamento em Árvores Binárias Dada uma árvore binária, é possível estabelecer várias formas para percorrer todos os seus nós, sem repetir nenhum e sem deixar de passar por nenhum. Esta operação é denomina- da caminhamento, que é usada, por exemplo, para consultar ou alterar as informações conti- das nos nós. As três maneiras mais usuais para percorrer os nós são: - caminhamento pré-fixado: 1. visita a raiz 2. percorre a sub-árvore da esquerda 3. percorre a sub-árvore da direita - caminhamento in-fixado: 1. percorre a sub-árvore da esquerda 2. visita a raiz 3. percorre a sub-árvore da direita - caminhamento pós-fixado: 1. percorre a sub-árvore da esquerda 2. percorre a sub-árvore da direita 3. visita a raiz Tomando como exemplo a árvore abaixo, temos, para cada tipo de caminhamento, a corres- pondente seqüência de nós: {veja a animação do CBT, acho que podemos fazer uma animação como um applet} caminhamento pré-fixado: A B D E G C F H I caminhamento in-fixado; D B G E A C H F I caminhamento pós-fixado: D G E B H I F C A 4.2.2 Implementação de Árvores Binárias Na implementação das árvores binárias, utilizaremos para cada nó um registro contendo um campo para dados (dado), um ponteiro que aponta a sub-árvore da esquerda (esq) e um pon- teiro que aponta a sub-árvore da direita (dir). O acesso à árvore é feito através de um ponteiro que aponta para a raiz (ainicio). 5 typedef struct nodo { char dado; struct nodo *esq, *dir; } arvore ; arvore *ainicio; Pela própria definição, pode-se perceber que as árvores são estruturas adequadas para o uso de algoritmos recursivos. A própria definição formalizada acima para uma árvore é uma definição recursiva. [ De um modo um pouco mais formal, podemos dizer que uma árvore é um conjunto finito de um ou mais nós, tais que: a) existe um nó denominado raiz; b) os demais nós formam m>=0 conjuntos disjuntos s1, s2, ... sm, tais que cada um desses conjuntos também é uma árvore (denominada sub-árvore). ] 4.2.3 Algoritmos Básicos em Árvores Binárias A maior parte dos algoritmos apresentados aqui são recursivos. Para efeitos didáticos, em alguns casos se apresenta também o algoritmo que realiza a operação equivalente, fazendo uso de uma pilha de ponteiros tipo void, isto é, ponteiros que podem apontar para qualquer elemento. No caso, são utilizados apontando para os nós da árvore. Os algoritmos recursivos, em geral, são mais simples. Muitas vezes, o acesso à árvore pode se modificar durante a execução de uma função. Neste caso, será passado como parâmetro o endereço do ponteiro de acesso à árvore, denomi- nado eainicio. As definições dessas variáveis ficam sendo então: arvore *ainicio; arvore **eainicio; Por exemplo: Se uma função é definida como: void nome_da_funcao (arvore **eainicio) Então a chamada a essa função será efetuada da seguinte forma: nome_da_funcao (&aini) onde aini é um ponteiro para arvore, ou seja, fora da função foi definido que: arvore *aini; 6 Abaixo são examinados alguns dos algoritmos básicos que utilizam árvores binárias: a) Algoritmo de caminhamento em ordem pré-fixada utilizando pilha – versão 1 //--- definição do tipo de pilha de uso geral typedef struct tipopilhageral { void *dado; struct tipopilhageral *prox; } pilhageral; //--------------------------------------------- void CaminhaPreComPilha1 (arvore *ainicio) { arvore *a1; pilhageral *p1; char c; a1 = ainicio; InicializaPilha (&p1); InserePilha (&p1, a1); while (!PilhaVazia (p1)) { a1 = RetiraPilha (&p1); if (a1 != NULL) { printf ("%c", a1->dado); InserePilha (&p1, a1->dir); InserePilha (&p1, a1->esq); } } } Observação: Nesta versão, a função de caminhamento insere na pilha os ponteiros NULL en- contrados no caminhamento. Consequentemente, ela verifica se ainicio é NULL em cada reti- rada da pilha. b) Algoritmo de caminhamento em ordem pré-fixada utilizando pilha – versão 2 //--- definição do tipo de pilha de uso geral typedef struct tipopilhageral { void *dado; struct tipopilhageral *prox; } pilhageral; //--------------------------------------------- void CaminhaPreComPilha2 (arvore *ainicio) { arvore *a1; pilhageral *p1; char c; a1 = ainicio; InicializaPilha (&p1); Tira Teima 7 if (a1 != NULL) InserePilha (&p1, a1); while (!PilhaVazia (p1)) { a1 = RetiraPilha (&p1); printf ("%c", a1->dado); if (a1->dir != NULL) InserePilha (&p1, a1->dir); if (a1->esq != NULL) InserePilha (&p1, a1->esq); } } Observação: Nesta versão, a função de caminhamento não insere na pilha os ponteiros NULL encontrados no caminhamento. Consequentemente, ela não verifica se ainicio é NULL em cada retirada da pilha. Na primeira inserção na pilha, deve ser feito um teste para verificar se a árvore é vazia Comparando-se as duas versões, tem-se que a versão 1 executa muito mais inserções e retiradas de elementos nas pilhas (aproximadamente metade dos ponteiros de uma árvore binária são NULL). c) Algoritmo de caminhamento em ordem in-fixada utilizando pilha – versão 1 Para o caminhamento em ordem in-fixada, cada elemento da árvore entra na pilha duas vezes (acompanhe o funcionamento no TiraTeima). Para discernir se o elemento está entrando pela primeira ou pela segunda vez, criou-se um novo tipo de pilha, com um elemento int, que recebe o valor 0 quando a entrada é feita na primeira vez, e recebe o valor 1 quando a entrada é feita pela segunda vez. Para facilitar a compreensão, foi desenvolvida a função Empilha, interna à função de caminhamento, que procede à inserção na pilha. A versão 1 processa o caso de ainicio ser igual a NULL. void CaminhaInComPilha1 (arvore *ainicio) { pilhageral *pilha; void *paux1; struct reg { arvore *arv; int pri; }; struct reg *paux2; arvore *a1; //------------------------------------- void Empilha (arvore *a, int n){ paux2 = malloc (sizeof (struct reg)); paux2->arv = a; paux2->pri = n; InserePilha (&pilha, paux2); } //-------------------------------------- Tira Teima Tira Teima 8 a1 = ainicio; InicializaPilha(&pilha); Empilha(a1, 1); while (!PilhaVazia (pilha)) { paux2 = RetiraPilha (&pilha); a1 = paux2->arv; if (a1 != NULL) { if (!paux2->pri) printf ("%c", paux2->arv->dado); else { Empilha (a1->dir, 1); Empilha (a1, 0); Empilha (a1->esq, 1); } } } } d) Algoritmo de caminhamento em ordem in-fixada utilizando pilha – versão 2 Neste caso a pilha utilizada é igual à da versão 1. A função de caminhamento não inse- re nem retira da pilha os ponteiros iguais a NULL. Valem as mesmas observações feitas nos itens a)
Compartilhar