ii-estrutura de dados

16
0 Unidade I: Estrutura de Dados: Pilhas

Upload: others

Post on 04-Jul-2022

0 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: II-Estrutura de dados

 

 

 

 

 

 

   

 Unidade I:

Estrutura de Dados:

Pilhas

 

Page 2: II-Estrutura de dados

  1 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

Unidade: Pilhas

1 – Pilhas

Já conhecemos na Unidade anterior as Listas Lineares, cujo princípio fundamental é o de ordenar unidimensionalmente seus elementos. Dependendo de como seus elementos são manipulados, as listas lineares recebem nomes especiais e têm utilizações diversas.

Conheceremos três estruturas lineares, as pilhas, as filas e as listas ordenadas. Nesta Unidade, vamos aprender a mais fácil delas, as Pilhas.

A Pilha é um tipo especial de lista linear em que todas as inserções e remoções são feitas em uma mesma extremidade, denominada topo. Então, temos acesso somente ao topo da pilha, ou seja, quando queremos inserir um elemento na pilha, colocamos no topo e, se quisermos excluir um elemento, só podemos excluir aquele que está no topo.

Esses tipos de listas nas quais a inserção e remoção são feitas por uma única extremidade são conhecidas como listas LIFO (Last-In/First-Out) - Último elemento que entra é o primeiro que sai.

Uma Pilha possui três operações básicas:

TOP: acessa o elemento posicionado no topo da pilha e retorna; PUSH: insere um novo elemento no topo da pilha; POP: remove um elemento do topo da pilha e retorna.

Page 3: II-Estrutura de dados

  2 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

Exemplo: vamos supor uma Pilha de Nome P.

-------- P:[ ] Inicialmente a Pilha está vazia

push(a) P:[a] Insere o elemento a na Pilha P

push(b) P:[a, b] Insere o elemento b na Pilha P. Note que agora o topo é b

push(c) P:[a, b, c] Insere o elemento c na Pilha P. Agora o topo contém c

pop() P:[a,b] Remove o elemento do topo e retorna o conteúdo. No caso c

push(d) P:[a, b, d] Insere o elemento d no topo.

top() P:[a, b, d] Não altera a Pilha, apenas retorna o valor do topo da Pilha (d)

push(top()) P:[a, b, d, d] Insere no topo mais uma cópia do topo

push(pop()) P:[a, b, d, d] Insere no topo o que foi excluído do topo (fica do mesmo jeito)

Para exemplificá-la, podemos pensar em uma pilha de pratos, em que sempre colocamos o prato em seu topo e retiramos também do topo. Porém, não seria possível inserir pratos indefinidamente, pois existe um limite, no caso o teto; tampouco tirar pratos indefinidamente, pois, uma hora, a pilha estaria vazia.

Na implementação computacional, também existem tais limites, pois a quantidade de elementos não pode exceder à quantidade de memória alocada e não podemos retirar elementos de uma pilha vazia. Para tanto, surge a necessidade de mais duas funções:

isEmpty - verifica se a pilha está vazia; isFull - verifica se a pilha está cheia.

Page 4: II-Estrutura de dados

  3 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

Para a implementação de uma Pilha, podemos escolher dois modelos de alocação de memória, a Estática-Sequencial ou a Dinâmica-Encadeada.

Vamos começar mostrando uma implementação mais simples com alocação Estática-Sequencial usando a linguagem Java, porém, o conceito pode ser utilizado em qualquer outra linguagem de programação

1.1) Pilha Estática

Segue abaixo uma implementação de Pilha Estática Seqüencial, na linguagem Java.

1. //TAD que implementa uma Pilha 2. public class Pilha { 3. private int topo; //Topo da Pilha 4. private int MAX; //Tamanho da Pilha 5. private Object memo[]; //Elementos da Pilha (objeto genérico)

6. //Método que inicializa a Pilha no estado vazia 7. public Pilha() { 8. topo=-1; 9. MAX=30; 10. memo = new Object[MAX]; 11. }

12. //Método que verifica se a Pilha está Vazia 13. public boolean isEmpty() { 14. return(topo==-1); 15. }

16. //Método que verifica se a Pilha está cheia 17. private boolean isFull() { 18. return(topo==MAX-1); 19. }

20. //Método para inserir um valor na Pilha 21. public void push(Object x) { 22. if(!isFull()) { 23. topo++; 24. memo[topo]=x; 25. } 26. else { 27. System.out.println("Pilha Cheia!!"); 28. } 29. }

30. //Método para exibir o conteúdo da Pilha 31. public void print() { 32. if(!isEmpty()) { 33. String msg = "";

Page 5: II-Estrutura de dados

  4 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

34. for(int i=0; i<=topo; i++) { 35. msg += memo[i].toString() + ", "; 36. } 37. System.out.println("P: [ "+msg+" ]"); 38. } 39. else { 40. System.out.println("Pilha Vazia!!"); 41. } 42. }

43. //Método para retornar o topo da Pilha e remove-lo 44. public Object pop() { 45. if(!isEmpty()) 46. return memo[topo--]; 47. else 48. return null; 49. }

50. //Método que retorna o topo da pilha sem removê-lo 51. public Object top() { 52. if(!isEmpty()) 53. return memo[topo]; 54. else 55. return null; 56. }

57. }

Note que nas linhas 4 e 9 temos uma variável MAX que define a quantidade máxima de elementos que iremos armazenar na pilha. Isso define a alocação como estática (quantidade de memória pré-definida).

Vemos nas linhas 5 e 10 a criação de um vetor e a alocação desse vetor, definindo quais elementos ficarão na sequência física de memória ou alocação sequencial.

O método isEmpty (verifica se está vazio) e isFull (verifica se está cheia) são usados para fazer a inserção de elementos na pilha (push), consulta de topo (top) e remoção de elementos (pop).

O método print foi criado apenas para exibir os elementos da Pilha na tela, da primeira para a última posição.

1.2 – Pilha Dinâmica

Um problema encontrado nas Pilhas Estáticas é a limitação da inserção de seus elementos, pois ela estava limitada pelo tamanho do vetor declarado. Tanto em termos de superdimensionamento, quanto subdimensionamento. A Pilha dinâmica faz uma requisição à heap (área de memória disponível no sistema) toda vez que um novo elemento precisa ser inserido na Pilha, assim como desaloca a memória usada por um elemento que foi excluído dela. Com isso, usamos somente a quantidade de memória exata de que o programa

Page 6: II-Estrutura de dados

  

nece

elem(estrelemportaPilha

aloc

Pentecadade Lseu ende

essita.

Para immento serárutura LIFO

mentos nãoanto, cadaa.

Então, teraremos es

Para a imender comoa elementoLista Simplpróximo e

ereço do n

mplementaá um nó, O). A Pilho necessara elemento

remos quespaço para

mplementaço funcionao deve armesmente E

e não do seó anterior,

armos a Ppor meio

ha Dinâmicriamente eo deverá c

e implemea mais um

ção de uma uma LISTmazenar o Encadeadaeu anterio teríamos

Pilha Dinâdo qual t

ca usa aestão dispoconter o e

entar um nnó, até o li

ma Pilha TA ENCADendereço

a, pois o elr. Se fosseuma lista d

âmica, vateremos aalocação ostos na oendereço d

nó da Pilhaimite da he

Dinâmica,DEADA. E do próximlemento sóe necessáduplament

amos penacesso som

encadeadordem físicdo próximo

a e, para eap.

vamos, Em uma lismo elemenó conhece rio armaze

te encadea

sar que mente ao a, ou seja

ca da memo element

cada inse

primeiramsta encadento. Chama

o endereçenar tambéada.

cada topo

a, os mória, to da

rção,

ente, eada, amos ço do ém o

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

Page 7: II-Estrutura de dados

  6 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

// Definindo a estrutura de um nó (Lista Encadeada) public class Node { private Object dado; //Dado a ser armazenado no nó private Node prox; //Referência para o próximo nó //Devolve o conteúdo do nó public Object getDado() { return dado; } //Atribui um valor ao conteúdo do nó public void setDado(Object novovalor) { dado = novovalor; } //Devolve a referência do próximo nó public Node getProx() { return prox; } //Atribui uma referência para o próximo nó public void setProx(Node novoprox) { prox = novoprox; } }

Como em uma Pilha só manipulamos uma das extremidades denominada topo, precisamos ter a referência somente de um nó. Como a alocação será dinâmica, não teremos a implementação de IsFull, pois a Pilha não estará cheia e, sim, não teremos mais memória na heap para alocar.

// Implementação de uma Pilha Dinâmica como Lista Encadeada

public class PilhaDin {

//Nó que representa o Topo (única extremidade a ser manipulada) Node topo;

//Construtor para iniciar a Pilha no estado vazia (sem nós, portanto, nula) public PilhaDin() {

topo = null; }

//Retorne se a Pilha está vazia public boolean isEmpty() {

return topo==null; }

Page 8: II-Estrutura de dados

  7 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

//Insere um elemento na Pilha public void push(Object x) {

Node novo = new Node(); //Cria um novo nó novo.setDado(x); //Insere o valor no novo nó novo.setProx(topo); //Configura como próximo do novo o topo atual topo = novo; //O novo topo será esse novo nó

}

//Retorna o elemento do Topo (sem removê-lo) public Object top() {

if(!isEmpty()) return topo.getDado();

else return null;

} //Retorna o elemento do topo, removendo-o public Object pop() {

if(!isEmpty()) { Object resp = topo.getDado(); //Captura quem está no topo topo = topo.getProx(); //O topo será o próximo return resp; //Retorna a resposta

} else

return null; }

//Exibe o conteúdo da Pilha dinâmica na tela public void print() {

if(!IsEmpty()) { String resp = new String(); Node aux; aux = topo; while(aux!=null) {

resp = ", " + aux.getDado().toString() + resp; aux = aux.getProx();

} System.out.println("P:[ "+resp+" ]");

} else

System.out.println("Pilha Vazia!"); }

}

Page 9: II-Estrutura de dados

  8 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

1.3 – Controle de Chamadas de Rotinas e Retornos

Qualquer sistema ou jogo, em que a ordem de entrada dos elementos é contrária à ordem de saída, é um candidato ao uso de Pilhas para implementação de seus elementos. Vamos ver agora alguns exemplos de uso de pilhas na computação.

Tanto na programação estruturada, quanto na orientada a objetos, as funções ou métodos são técnicas bastante utilizadas para o reaproveitamento de algoritmos implementados. Essas funções são subrotinas ou subprogramas dentro de seu programa principal.

Para formar os programas, as rotinas são conectadas entre si através de um mecanismo de chamada e retornos e as pilhas desempenham um papel fundamental no seu controle. Considere uma linguagem hipotética com as seguintes instruções:

print - imprime mensagem no vídeo call - chama a subrotina return - retorna à subrotina que chamou

Veja o programa a seguir dividido em 3 subrotinas (rotina A, rotina B e rotina C). A execução começa pela rotina A e o controle é passado às subrotinas e retornado de onde parou na rotina que chamou, até encerrar na última linha da rotina A.

Rotina A Rotina B Rotina C

1 - print "Um" 5 - call C 8 - print "Dois" 2 - call B 6 - print "Três" 9 - return 3 - call C 7 - return 4 - return

Cada vez que chamamos uma subrotina, o sistema operacional precisa guardar o endereço de retorno, continuando de onde parou, quando o controle foi transferido. Nesse caso, os endereços de retorno são empilhados. Veja a tabela:

Page 10: II-Estrutura de dados

  9 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

Instrução Pilha de Controle - - - - - - P:[ 0 ] 1 - print "Um" P:[ 0 ] 2 - call B P:[ 0, 3 ] 5 - call C P:[ 0, 3, 6 ] 8 - print "Dois" P:[ 0, 3, 6 ] 9 – return P:[ 0, 3 ] 6 - print "Três" P:[ 0, 3 ] 7 – return P:[ 0 ] 3 - call C P:[ 0, 4 ] 8 - print "Dois" P:[ 0, 4 ] 9 – return P:[ 0 ] 4 – return P:[ ]

Podemos notar que a pilha começa com 0 que simboliza o programa sendo executado na memória. A cada chamada de subrotina (comando call), o próximo endereço é armazenado na pilha para que seja executado, quando a subrotina é encerrada (comando return). Ao final do processo, a pilha de controle deverá estar vazia, simbolizando que o programa encerrou com sucesso.

Caso haja uma falha na execução do programa no meio do caminho, a pilha de memória não é esvaziada normalmente, ocorrendo erros como “Este programa executou uma operação ilegal e será finalizado ...”. E, nesse caso, o sistema operacional vai esvaziar a pilha de controle.

1.4 – Avaliação de Expressões

Estamos tão acostumados a digitar expressões e receber resultados, que raramente paramos para pensar sobre o que deve acontecer dentro do computador, a partir do momento em que fornecemos uma expressão até o momento em que recebemos a resposta.

Esse é um exemplo do uso de pilhas que, por si só, é um importante tópico da Ciência da Computação.

Para simplificar nossos estudos, vamos restringir as expressões aritméticas compostas, exclusivamente, pelos seguintes elementos:

- Identificador ou operando com uma única letra maiúscula [A...Z]; - Operadores aritméticos binários [ +, -, *, / ]; - Parênteses ( ou ) . - Exemplos de expressões válidas: - A + B , A - B * C , A / ( B * ( C - D ) + E )

As operações serão efetuadas de acordo com a ordem determinada pelas regras usuais da matemática, ou seja, primeiramente, as multiplicações e divisões e, posteriormente, as somas e subtrações, sendo que os parênteses

Page 11: II-Estrutura de dados

  10 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

podem alterar a ordem de prioridade entre as operações.

Existem duas dificuldades em avaliar uma expressão:

1 - A existência de prioridades diferentes para os operadores, que não nos permite efetuá-los na ordem em que encontramos na expressão;

2 - A existência de parênteses, que alteram a prioridade das operações.

Felizmente, um lógico polonês chamado Jan Lukasiewicz conseguiu criar uma representação para expressões nas quais não existem prioridades e nem a necessidade do uso dos parênteses.

Habitualmente, nós usamos a representação em que o operador fica no meio dos operandos, mas existem outras possibilidades.

- INFIXA: operador aparece entre os operandos ( A + B )

- PRÉ-FIXA: operador aparece antes dos operandos ( +AB )

- PÓS-FIXA: operador aparece após os operandos ( AB + )

A forma pré-fixa é chamada Notação Polonesa e a pós-fixa de Notação Polonesa Reversa (NPR). A Notação Polonesa Reversa tem se mostrado mais eficiente no uso para a avaliação de expressões e estudaremos mais profundamente esse caso.

A vantagem do uso dessa notação é que, como o operador aparece imediatamente após os operandos que deve operar, a ordem em que aparecem é a ordem em que devem ser efetuados, sendo desnecessário o uso de parênteses ou regras de prioridade.

Notação Infixa NPR A + B * C A B C * +

A * ( B + C ) A B C + *

( A + B ) / ( C - D ) A B + C D - /

( A + B ) / ( C - D ) * E A B + C D - / E *

Page 12: II-Estrutura de dados

  11 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

Para converter uma expressão infixa para NPR, usamos o algoritmo abaixo:

1) Parentetizar completamente a expressão (definir a ordem de avaliação)

2) Varrer a expressão da esquerda para a direita e, para cada símbolo: a) Se forem parênteses de abertura, ignorar; b) Se for operando, copiar direto para a saída; c) Se for operador, empilhá-lo; d) Se forem parênteses de fechamento, copiar para a saída o

último operador empilhado.

Exemplo: Transformar a expressão A + B * C - D em NPR:

1) ( ( A + ( B * C ) ) - D ) - parentetizar 2) Varrer a expressão:

Símbolo Pilha Saída ( P: [ ]

( P: [ ]

A P: [ ] A

+ P: [ + ] A

( P: [ + ] A

B P: [ + ] A B

* P: [ +, * ] A B

C P: [ +, * ] A B C

) P: [ + ] A B C *

) P: [ ] A B C * +

- P: [ - ] A B C * +

D P: [ - ] A B C * + D

) P: [ ] A B C * + D -

Page 13: II-Estrutura de dados

  12 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

1.4.1 – Avaliação de Expressões sem Parentetizar

O problema da primeira versão é que precisamos parentetizar a expressão antes de colocá-la no programa. Na prática, o usuário não utiliza dessa forma a expressão.

Isso se deve ao fato que a prioridade deve ser dada pelo usuário, para que o programa saiba por onde começar. Para resolver esse problema, usaremos uma função chamada “prio” que dá a prioridade dos operadores da expressão.

A função é a seguinte:

public static int prio(char op) { int resp=0; switch(op) { case '(': resp = 1; break; case '+': resp = 2; break; case '-': resp = 2; break; case '*': resp = 3; break; case '/': resp = 3; } return resp; }

O algoritmo versão 2.0 que usaremos será:

1) Inicie com uma pilha vazia;

2) Realize uma varredura na expressão infixa, copiando todos os identificadores encontrados diretamente para a saída.

- Ao encontrar um operador:

a) Enquanto a pilha não estiver vazia e houver no seu topo um operador com prioridade maior ou igual ao encontrado, desempilhe o operador e coloque-o na saída.

b) Empilhe o operador encontrado.

- Ao encontrar um parêntese de abertura, empilhe-o;

- Ao encontrar um parêntese de fechamento, remova um símbolo da pilha e copie-o na saída, até que seja desempilhado o parêntese de abertura correspondente. 3) Ao final da varredura, esvazie a pilha e copie para a saída os símbolos removidos.

Page 14: II-Estrutura de dados

  13 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

1.4.2 – Avaliando a forma Pós-Fixa

Percorrendo qualquer expressão em notação polo nesa reversa, da esquerda para a direita, ao enc ontrarmos um operador, sabemos que deve operar os dois últimos valores pelos quais passamos. Percebemos novamente a idéia de que “os últimos serão os primeiros processados” e novamente a aplicação de pilhas.

Vejamos um exemplo na expressão em NPR:

AB+CD-/E*

Vamos atribuir valores numéricos às variáveis da expressão a ser avaliada:

A=7; B=3; C=6; D=4; E=9.

Agora, seguiremos o algoritmo a seguir:

a) Iniciamos com uma pilha vazia; b) Varremos a expressão da esquerda para a direita e para cada elemento

encontrado: i. Se for operando, empilhar; ii. Se for operador, desempilhar os dois últimos valores,

efetuar a operação com eles e empilhar de volta o resultado obtido;

c) No final do processo, o resultado da avaliação estará no topo da pilha.

Vamos ver o estado da pilha em cada etapa:

Elemento Ação Pilha A Empilha valor de A P:[7] B Empilha valor de B P:[7,3]

+ Desempilha um y=3 Desempilha outro x=7 Empilha resposta x+y=10

P:[10]

C Empilha valor de C P:[10,6] D Empilha valor de D P:[10,6,4]

- Desempilha um y=4 Desempilha outro x=6 Empilha resposta x-y=2

P:[10,2]

/ Desempilha um y=2 Desempilha outro x=10 Empilha resposta x/y=5

P:[5]

E Empilha valor de E P:[5,9]

* Desempilha um y=9 Desempilha outro x=5 Empilha resposta x*y=45

P:[45]

Page 15: II-Estrutura de dados

  14 

Est

rutu

ra d

e D

ados

: Des

envo

lvim

ento

de

Jogo

s D

igita

is

 

Uma alternativa para dar valores aos operandos é criar um vetor de 26 posições (máximo de letras que pode ser utilizado) e para cada letra encontrada na expressão, perguntar o seu valor.

 

Page 16: II-Estrutura de dados

24

Responsável pelo Conteúdo: Prof. Ms. Amilton Sousa Martha Revisão Textual:

Prof. Ms. Rosemary Toffolli

www.cruzeirodosul.edu.br

Campus Liberdade

Rua Galvão Bueno, 868

01506-000

São Paulo SP Brasil

Tel: (55 11) 3385-3000