Pr´atica de Algoritmos e Estrutura de Dados I – DIM0426 Umberto Souza da Costa - 11/09/2007
– Terceira Lista de Exerc´ıcios – Utiliza¸c˜ao dos TADs Pilha e Fila
1
Objetivos
O objetivo deste exerc´ıcio de programa¸c˜ao ´e utilizar as estruturas de dados do tipo pilha e fila para a resolu¸c˜ao de problemas pr´aticos. Ambas as classes j´a devem ter sido imple-mentadas como classes gen´ericas. A aplica¸c˜ao dos TAD pilha e fila ser´a na avalia¸c˜ao de express˜oes simples, cujo detalhes encontram-se descritos na Se¸c˜ao 2.
2
Aplica¸
c˜
ao Pilha: Avaliador de Express˜
oes Simples
A aplica¸c˜ao consiste em desenvolver um programa capaz de receber express˜oes simples e retornar o resultado de sua avalia¸c˜ao.
2.1 Express˜oes
O programa aves (Avaliador de Express˜oes Simples) dever´a lidar com express˜oes aritm´e-ticas simples envolvendo inteiros, formadas por:
⊲ constantes num´ericas inteiras, no intervalo de −32768 a 32767; ⊲ operadores (+, −, /, ∗,ˆ, %), com precedˆencia descrita em Tabela 1, e; ⊲ delimitadores de escopo (i.e. parˆenteses).
Precedˆencia Operadores Associa¸c˜ao
1 () −→
2 ^ ←−
3 * / % −→
4 + - −→
Tabela 1: Precedˆencia e ordem de associa¸c˜ao de operadores em express˜oes aves. O operador % representa o resto da divis˜ao inteira e o ˆ representa a opera¸c˜ao de exponencia¸c˜ao.
Segue abaixo exemplos de express˜oes v´alidas para o aves: ⊲ 35 - 3 * (-2 + 5)^2
⊲ 54 / 3 ^ (12%5) * 2 ⊲ ((2-3)*10 - (2^3*5))
O fim de linha (‘\n’) ser´a o indicador de fim de express˜ao, ou seja, o programa dever´a receber uma express˜ao por linha de entrada de dados.
2.2 A Tarefa
Sua tarefa consiste em elaborar um programa em C++ denominado aves.cpp que dever´a receber, via entrada padr˜ao (cin + getline)1, uma s´erie de express˜oes aves de at´e 256 caracteres, v´alidas ou inv´alidas.
O programa dever´a, ent˜ao, avaliar cada express˜ao e imprimir seu respectivo resultado na sa´ıda padr˜ao, cout, e em um arquivo texto de resultados (log.txt). Note que em caso de haver algum problema com a express˜ao (e.g. escopo aberto, operador inv´alido, falta de operando, etc.) o programa dever´a indicar tal erro claramente nas sa´ıdas definidas.
Por exemplo, a resposta que o programa deveria oferecer para as express˜oes exemplo da Se¸c˜ao 2.1 seria: 8, 12 e −50 (uma resposta por linha).
2.3 Solucionando o Problema
Uma das poss´ıveis solu¸c˜oes para o problema descrito nas se¸c˜oes anteriores ´e transformar a express˜ao original do formato infixo para o formato posfixo2. A transforma¸c˜ao de um formato para outro apresenta trˆes vantagens:
1. Durante o procedimento de transforma¸c˜ao ´e poss´ıvel detectar alguns erros de forma-¸c˜ao de express˜ao;
2. A express˜ao no formato posfixo n˜ao necessita da presen¸ca de delimitadores (parˆen-teses) por ser uma representa¸c˜ao n˜ao-amb´ıgua;
3. O algoritmo para avaliar uma express˜ao posfixa ´e mais simples do que um algoritmo para avaliar uma express˜ao infixa.
2.3.1 Nota¸c˜oes de Express˜oes
Uma express˜ao para somar A e B pode ser descrita como A + B. Essa representa¸c˜ao ´e denominada de forma infixa. Al´em dessa existem outras duas representa¸c˜oes, a saber:
+ A B prefixa A B + posfixa
Na nota¸c˜ao prefixa o operador (no caso o ‘+’) ´e introduzido antes de seus dois operandos (A e B). J´a na nota¸c˜ao posfixa o operador aparece logo ap´os seus dois operandos.
As regras que devem ser consideradas durante o processo de convers˜ao s˜ao: i) as opera¸c˜oes com a precedˆencia mais alta s˜ao convertidas em primeiro lugar (quando existe ambig¨uidade) e; ii) uma express˜ao convertida para a forma posfixa deve ser tratada como um ´unico operando. Veja na Tabela 2 alguns exemplos adicionais de convers˜ao da forma infixa para a posfixa.
2.3.2 Avaliando Uma Express˜ao Posfixa
Para realizar a avalia¸c˜ao de uma express˜ao na forma posfixa pode-se utilizar uma estru-tura de dados do tipo pilha. Cada vez que um operando (i.e. uma constante inteira) ´e
1
Usar while ( cin.getline(...) ) de forma que para encerrar a entrada de dados basta pressionar <Ctrl+D>. Tamb´em ´e poss´ıvel redirecionar a entrada de dados a partir de um arquivo ascii com o comando < na linha de comando. Ex.: $ ./aves < expressoes.txt.
2
Forma Infixa Forma Posfixa
A + B AB+
A + B − C AB + C−
(A + B) ∗ (C − D) AB + CD − ∗
AˆB ∗ C − D + E/F/(G + H) ABˆC ∗ D − EF/GH + /+ ((A + B) ∗ C − (D − E)) ˆ(F + G) AB + C ∗ DE − −F G+ˆ A − B/(C ∗ D ˆE) ABCDE ˆ∗/−
Tabela 2: Exemplos de equivalˆencias entre forma infixa e posfixa de express˜oes.
encontrado na express˜ao o mesmo deve ser introduzido na pilha. Quando um operador (i.e. +, ˆ, ∗, etc.) ´e encontrado na express˜ao, os dois elementos no topo da pilha s˜ao seus operandos. Portanto, devemos retirar esses dois elementos da pilha, realizar a opera¸c˜ao indicada pelo operador3e, a seguir, (re)introduzir o resultado de volta na pilha, tornando-o dispon´ıvel para uso como operando do pr´oximo operador. Confira o Algoritmo 1.
Algoritmo 1 Avalia¸c˜ao de express˜ao no formato posfixo.
01: fun¸c~ao AvalPosfixa( LPosfixa: Tipo Lista Componentes ): Inteiro 02: | % A Lista com express~ao na forma posfixa j´a foi gerada em LPosfixa
03: | Enquanto ( !LPosfixa.IsEmpty() ) Fa¸ca 04: | | symb = pr´oximo n´o de LPosfixa; 05: | | Se ( symb ´e um operando ) Ent~ao 06: | | | P.push( symb );
07: | | Sen~ao
08: | | | opnd2 = P.pop( ); 09: | | | opnd1 = P.pop( );
10: | | | valor = resultado de aplicar symb `a opnd1 e opnd2; 11: | | | P.push( valor ); % Resultado de volta `a pilha
12: | | Fim-Se 13: | Fim-Enquanto
14: | % Retornar o valor final da express~ao
15: x Retorne( P.pop( ) ). % S´o deveria haver um elemento
2.3.3 Convertendo Express˜oes: de infixa para posfixa
Para realizar a avalia¸c˜ao da express˜ao conforme descrito na Se¸c˜ao 2.3.2, faz-se necess´ario converter a express˜ao de infixa para posfixa. Essa convers˜ao deve ser feita de tal forma a lidar de forma correta com, digamos, os casos A + B ∗ C e (A + B) ∗ C — produzindo, respectivamente, as express˜oes ABC ∗ + e AB + C∗.
Ao analisar os casos acima percebe-se que o algoritmo de convers˜ao deve possuir algum tipo de mecanismo para armazenar os operadores temporariamente de tal forma que a regra de precedˆencia de operadores seja respeitada. Esse mecanismo de armazenamento tamb´em ser´a uma estrutura de dados do tipo pilha.
O Algoritmo 2 apresenta uma forma de converter uma express˜ao (sem parˆenteses4) no formato infixo para o posfixo. Para tanto assuma que existe uma fun¸c˜ao denominada
3
Cuidado com a ordem dos operandos, pois a pilha ´e uma TAD LIFO (Last In, First Out).
4
prcd(op1,op2)— onde op1 e op2 s˜ao operadores — que retorna VERDADEIRO se op1 tiver precedˆencia sobre op2 ou ambos tiverem a mesma precedˆencia, e FALSO caso contr´ a-rio. Por exemplo, prcd(‘*’,‘+’) e prcd(‘-’,‘+’) retornam VERDADEIRO, enquanto prcd(‘+’,‘*’)´e FALSO.
Algoritmo 2 Convers˜ao de express˜ao no formato infixo para posfixo.
01: fun¸c~ao Infx2Psfx( LInfixa: Tipo Lista Componentes ): Tipo Lista Componentes 02: | LPosfixa = lista vazia de componentes de express~ao;
03: | OP = uma pilha vazia de operadores ; 04: | Enquanto ( !LInfixa.IsEmpty() ) Fa¸ca 05: | | symb = pr´oximo n´o de LInfixa; 06: | | Se ( symb ´e um operando ) Ent~ao 07: | | | LPosfixa.Insert( symb ); 08: | | Sen~ao
09: | | | Enquanto ( !OP.IsEmpty() E prcd( OP.top(),symb ) ) Fa¸ca 10: | | | | topsymb = OP.pop( ); 11: | | | | LPosfixa.Insert( topsymb ); 12: | | | Fim-Enquanto 13: | | | OP.push( symb ); 14: | | Fim-Se 15: | Fim-Enquanto
16: | % Elimina¸c~ao de quaisquer operadores restantes
17: | Enquanto ( !OP.IsEmpty() ) Fa¸ca 18: | | topsymb = OP.pop();
19: | | LPosfixa.Insert( topsymb ); 20: | Fim-Enquanto
21: x Retorne LPosfixa.
O interessante do algoritmo s˜ao as linhas 09–12, que indicam o seguinte: se o operador da express˜ao for de menor precedˆencia do que aquele que est´a atualmente no topo da pilha ent˜ao desempilhe (linha 10) at´e achar um operador de menor precedˆencia (em outras palavras, um ‘+’ n˜ao pode ficar sobre um ‘*’ na pilha OP); simultaneamente envie os operadores desempilhados para a sa´ıda (linha 11).
Teste o algoritmo acima com as seguintes entradas: A ∗ B + C ∗ D e A + B ∗ CˆDˆE. Agora pense nas modifica¸c˜oes necess´arias que devem ser feitas no algoritmo e na fun¸c˜ao prcd de forma a acomodar o uso de parˆenteses (que pode modificar a precedˆencia dos operadores).
2.4 Implementa¸c˜ao
Inicialmente fa¸ca um levantamento de todos os erros poss´ıveis que podem acontecer na compila¸c˜ao de uma express˜ao. Documente cada erro, atribuindo-lhes um c´odigo ´unico, associando-os `a mensagens de erros significativas (i.e. objetivas e auto-explicativas) — essa abordagem facilita a parte de tratamento de erros de avalia¸c˜ao da express˜ao. De uma forma geral ´e poss´ıvel separar os erros em categorias: entrada de dados inv´alidas (e.g. n´umeros em ponto flutuante ou inteiros fora dos limites especificados), elabora¸c˜ao incorreta da express˜ao (e.g. falta de parˆenteses e s´ımbolos n˜ao reconhecido), e erro na avalia¸c˜ao da express˜ao (e.g. divis˜ao por zero, falta operando).
A
C´
odigo Exemplo de Leitura de Dados
Nesta Se¸c˜ao temos um exemplo de como realizar a leitura de dados da entrada padr˜ao, o que permite realizar o redirecionamento para um arquivo de entrada.
Algoritmo 3 C´odigo exemplo de leitura de dados.
#include <iostream> #include <sstream> using namespace std; int main( void ) {
// expects either space-delimited numbers or lines that start with // two forward slashes (//)
string s;
// While there is input to read... while( getline( cin, s ) ) {
// In case the string starts with // we ignore it completely if( s.size() >= 2 && s[0] == ’/’ && s[1] == ’/’ ) {
cout << " ignoring comment: " << s << endl; }
else {
// Transforms s into a string stream. This is necessary // in order to extract the numbers inside the string. istringstream ss( s );
int d;
// Estract first number into d variable. while( ss >> d ) {
// Just output variable.
cout << " got a number: " << d << endl; }
} } }