Baixe o app para aproveitar ainda mais
Prévia do material em texto
Universidade Federal de Itajubá – UNIFEI Campus Itabira Capítulo 3 Pilhas e Filas 2º Semestre de 2016 Sandro Carvalho Izidoro 1 Considerações Iniciais Existem determinadas estruturas de dados que não utilizam de ordem para organizar seus dados. Para determinadas aplicações é imposto um critério que restringe a inserção e retirada dos elementos que compõem um conjunto de dados, ou seja, são definidas disciplinas de acessos. Disciplina de acesso é a forma pela qual os elementos de uma determinada estrutura de dados serão inseridos e retirados desta estrutura. Os dois critérios mais usuais são: • LIFO (Last In First Out): dentre os elementos que ainda permanecem no conjunto, o primeiro a ser retirado é o ultimo que tiver sido inserido. Este critério é utilizado na estrutura de dados denominada Pilha. • FIFO (First In First Out): dentre os elementos que ainda permanecem no conjunto, o primeiro elemento a ser retirado é o primeiro que tiver sido inserido. Esse critério caracteriza a estrutura de dados Fila. 2 Pilha Uma Pilha é uma estrutura linear de dados que pode ser acessada somente por uma de suas extremidades para armazenar e recuperar dados. Ela é como uma pilha de bandejas em uma lanchonete: bandejas são colocadas e retiradas do topo da pilha. A última bandeja colocada é a primeira bandeja removida da pilha [1]. Pode-se pegar uma bandeja somente se existirem bandejas na pilha, e uma bandeja pode ser adicionada à pilha somente se houver espaço suficiente, isto é, se a pilha não estiver muito alta. Assim, uma pilha é definida em termos das operações que modificam e das que verificam seus status. As operações são as seguintes: Estrutura de Dados – Capítulo 3 – 2 • IniciarPilha - Limpa a pilha; • PilhaVazia - Verifica se a pilha está vazia; • PilhaCheia - Verifica se há espaço para inserir novos elementos na pilha; • Empilha - Insere um novo elemento na pilha; • Desempilha - Retira um elemento da pilha (o elemento mais alto da pilha); • TopoPilha - Retorna o elemento mais alto da pilha sem removê-lo. Da mesma forma que a estrutura de dados lista, pode-se implementar uma pilha usando dois recursos básicos das linguagens de programação: 1. Vetor; 2. Alocação dinâmica de memória. 2.1 Implementação de Pilha usando vetor Para implementar uma pilha através de uma estrutura estática, além do vetor onde serão armazenados os elementos da pilha, será necessário uma variável inteira que sinalize a posição do último elemento inserido, ou seja, o topo da pilha. A definição desta estrutura é semelhante àquela utilizada na lista estática sequencial, conforme declaração a seguir: #define TAM 10 struct No { char Chave; int Valor; }; struct Pilha { No Dados[TAM]; int Topo; }; Para iniciar esta estrutura, basta atribuir o valor -1 para o campo Topo. Geralmente, a pilha é muito útil nas situações em que os dados tem que ser armazenados e então recuperados na ordem inversa. Uma aplicação da pilha é o casamento de delimitadores em um programa. Esse é um exemplo importante, porque o casamento de delimitadores é parte de qualquer compilador: nenhum programa é considerado correto se os delimitadores não estão casados. Outra característica desta estrutura é a simplicidade dos códigos utilizados para implementar as primitivas. Isto pode ser observado nas primtivas de inserção e remoção a seguir: Estrutura de Dados – Capítulo 3 – 3 bool Empilha (Pilha& P, No Novo) { if (P.Topo == TAM 1) return false; P.Dados[++P.Topo] = Novo; return true; } bool Desempilha (Pilha& P, No& Novo) { if (P.Topo == 1) return false; Novo = P.Dados[P.Topo]; return true; } 2.2 Implementação de Pilha usando alocação dinâmica Neste tipo de implementação não há necessidade da primitiva PilhaCheia, uma vez que, teoricamente, toda memória está disponível para a alocação. As primitivas de inserção e remoção são descritas a seguir: typedef No * Pilha; void Empilha (Pilha& Topo, char Elemento) { No *Aux = new No; Aux>Info = Elemento; Aux>Lig = Topo; Topo = Aux; } bool Desempilha (Pilha& Topo, char& Elemento) { if (Topo == NULL) return false; Elemento = Topo>Info; No *Aux = Topo; Topo = Topo>Lig; delete Aux; return true; } Estrutura de Dados – Capítulo 3 – 4 2.3 Exemplo de aplicação de Pilha: Notação Polonesa Uma representação para expressões aritméticas, que seja conveniente do ponto de vista computacional, é assunto de interesse, por exemplo, na área de compiladores. A notação tradicional é ambígua e, portanto, obriga ao preestabelecimento de regras de prioridade. Isso, naturalmente, torna a tarefa computacional menos simples. Outras representações são apresentadas a seguir, considerando-se somente operações binárias [2]. • Notação completamente parentizada: acrescenta-se sempre um par de parênteses a cada par de operandos e a seu operador. Exemplo: Notação tradicional: A B − C/D ∗ Notação parentizada: ((A B) − (C/D)) ∗ • Notação polonesa: os operadores aparecem imediatamente antes dos operandos. Esta notação explicita quais operadores, e em que ordem, devem ser calculados. Por esse motivo, dispensa o uso de parênteses, sem ambiguidades. Exemplo: Notação tradicional: A B − C/D ∗ Notação polonesa: − AB/CD ∗ • Notação polonesa reversa (ou pósfix): é semelhante a notação polonesa, porém os operadores aparecem após os operandos. Esta notação é tradicionalmente utilizada em máquinas de calcular. Exemplo: Notação tradicional: A B − C/D ∗ Notação polonesa: AB CD/− ∗ Tanto a expressão original quando a expressão transformada devem ser armazenadas em listas. Um método para efetuar a conversão da notação parentizada na notação polonesa reversa é apresentado a seguir [2]. Algumas observações auxiliam em tal conversão: • Os operandos aparecem na mesma ordem na notação tradicional e na notação polonesa reversa; • Na notação polonesa reversa, os operadores aparecem na ordem em que devem ser calculados (da esquerda para a direita); • Os operadores aparecem imediatamente depois de seus operandos. Nota-se que, pela primeira observação, a ordem dos operandos não se modifica, podendo estes ser ignorados. A principal preocupação do algoritmo deve ser a ordem e a posição dos operadores. Na notação parentizada, essa ordem é indicada pelos parênteses; os mais internos significam operações prioritárias. Estrutura de Dados – Capítulo 3 – 5 Pressupõem-se que foi realizada uma análise sintática prévia na expressão. Para efetuar a conversão, devem-se remover os parênteses e estabelecer a ordem conveniente de operadores. Estes então devem ser armazenados até que um “)” seja encontrado, o que indica que a operação mais interna, e, por conseguinte, a primeira a ser executada, foi detectada. O último operador armazenado corresponde a essa operação. Portanto, a estrutura utilizada no armazenamento dos operadores deve ser uma pilha. No código a seguir, a expressão a ser convertida se encontra no vetor Expressão e o resultado da conversão, no vetor Polonesa. O parâmetro Tam indica a dimensão da expressão e o parâmetro Operandos contém os símbolos que representam operandos válidos. Na variável Expressao os caracteres que não forem símbolos representão necessariamente, variáveis da expressão. bool Conversor(char *Expressao, char *Polonesa, int Tam, char *Operadores) { Pilha P; IniciaPilha (P); char Operador; int IndicePol = 0; for (int i=0; i < Tam; i++) { if (!strchr(Operadores, Expressao[i])) Polonesa[++IndicePol]= Expressao[i]; else if ((Expressao[i] != ’(’) && (Expressao[i] != ’)’)) Empilha(P, Expressao[i]); else if (Expressao[i] == ’)’) { if (Desempilha(P, Operador)) Polonesa[++IndicePol] = Operador; else return false; } } Polonesa[++IndicePol] = ’\0’; return true; } 3 Fila Uma Fila é simplesmente uma linha de espera que cresce somando elementos ao seu final e que diminui tomando elementos de sua frente. Diferente de uma pilha, uma fila é uma estrutura na qual ambas as extremidades são usadas: uma para adicionar novos elementos e outra para removê-los. Em consequência, o último elemento tem que esperar até que todos os elementos que o precedem na fila sejam removidos [1]. As operações das filas são similares às operações das pilhas. As seguintes operações são necessárias para gerenciar apropriadamente uma fila: Estrutura de Dados – Capítulo 3 – 6 • IniciarFila - Limpa a fila; • FilaVazia - Verifica se a fila está vazia; • FilaCheia - Verifica se há espaço para inserir novos elementos na fila; • InsereFila - Insere um novo elemento na fila; • RetiraFila - Retira um elemento da fila; • Primeiro - Retorna o primeiro elemento da fila sem removê-lo. 3.1 Implementação de Fila usando vetor Da mesma forma que as estruturas anteriores, pode-se implementar uma fila usando vetor ou alocação dinâmica de memória. Como na implementação por vetor da estrutura pilha, os elementos são adicionados ao final da fila, mas, como as posições liberadas durante a operação de remoção devem ser reaproveitadas, o final da fila pode ocorrer antes da posição de início da fila dentro do vetor. Esta situação é mais bem visualizada como uma matriz circular, como ilustra a figura 1c. A fila está cheia se o primeiro elemento imediatamente precede na direção anti-horária o último elemento. No entanto, como uma matriz circular é implementada com um vetor “normal”, a fila está cheia tanto se o primeiro elemento está na primeira célula e o último elemento na ultima (figura 1a) como se o primeiro elemento está logo depois do último (figura 1b). Similarmente, InsereFila e RetiraFila tem que considerar a possibilidade de fechar a matriz quando se está adicionando ou removendo os elementos. Por exemplo, InsereFila pode ser vista como operando em uma matriz circular (figura 1c), mas na realidade está operando em um vetor unidimensional. Em consequência, se o ultimo elemento está na ultima célula e se quaisquer células estão disponíveis no início da matriz, um novo elemento é colocado lá (figura 1d). Se o último elemento está em qualquer outra posição, então o novo elemento é colocado depois do último, se houver espaço (figura 1e). Essas duas situações precisam ser reconhecidas quando se implementa uma fila vista como uma fila circular. Estrutura de Dados – Capítulo 3 – 7 Figura 1: Situações em uma implementação de fila circular O código a seguir descreve uma implementação de fila circular. Para facilitar o teste de fila vazia e fila cheia, um campo Tam foi adiconado à estrutura para armazenar a quantidade de elementos da fila. Estrutura de Dados – Capítulo 3 – 8 struct Fila { No Dados[T]; int Tam; int Com; int Fim; }; bool InsereFila (Fila& F, No Novo) { if (F.Tam == T) return false; F.Tam++; F.Fim = (F.Fim + 1) % T; F.Dados[F.Fim] = Novo; return true; } bool RetiraFila (Fila& F, No& Novo) { if (F.Tam == 0) return false; F.Tam; Novo = F.Dados[F.Com]; F.Com = (F.Com + 1) % T; return true; } 3.2 Implementação de Fila usando alocação dinâmica A implementação utilizando alocação dinâmica de memória pode ser realizada com o uso de um descritor que indica o ponteiro para o início e o fim da fila. A seguir as primitivas de inserção e remoção em fila por alocação dinâmica de memória: struct Fila { NoPtr Com; int Nro; NoPtr Fim; }; void InsereFila (Fila& D, char Novo) { NoPtr P = new No; P>Info = Novo; P>Lig = NULL; if (D.Nro == 0) D.Com = D.Fim = P; else { Estrutura de Dados – Capítulo 3 – 9 D.Fim>Lig = P; D.Fim = P; } D.Nro++; } bool RetiraFila (Fila& D, char& Valor) { if (D.Nro == 0) return false; else { NoPtr P = D.Com; Valor = P>Info; D.Com = P>Lig; D.Nro; if (D.Nro == 0) D.Fim = NULL; delete P; return true; } } 4 Exercícios 1. Implemente as primitivas PilhaVazia e TopoPilha (Utilizando vetor e ponteiro). 2. Implemente as rotinas Empilha, Desempilha, PilhaVazia e PilhaCheia para uma pilha de inteiros usando um vetor P[100], onde P[0] é usado como ponteiro de topo da pilha, ao invés do ponteiro Topo e P[1] até P[100] contém os elementos da pilha. Qual a desvantagem neste tipo de implementação? 3. Faça uma função para colocar os elementos de uma pilha S na ordem ascendente, usando uma pilha adicional. 4. Faça funções para inverter a ordem dos elementos na pilha S: (a) usando duas pilhas adicionais; (b) usando uma fila adicional. Estrutura de Dados – Capítulo 3 – 10 5 Referências [1] Adam Drozdek. Estrutura de Dados e Algoritmos em C++. Pioneira Thomson Learning, São Paulo, 2002. [2] Jayme Luiz Szwarcfiter and Lilian Markenzon. Estruturas de Dados e Seus Algoritmos. LTC Editora, Rio de Janeiro, 1994. Observações Material elaborado a partir das notas de aula do professor Edmilson Marmo Moreira (UNIFEI). 1 Considerações Iniciais 2 Pilha 2.1 Implementação de Pilha usando vetor 2.2 Implementação de Pilha usando alocação dinâmica 2.3 Exemplo de aplicação de Pilha: Notação Polonesa 3 Fila 3.1 Implementação de Fila usando vetor 3.2 Implementação de Fila usando alocação dinâmica 4 Exercícios 5 Referências
Compartilhar