Você está na página 1de 6

Estruturas de Dados CK0109

Pilhas
Universidade Federal do Ceara
Tiberius O. Bonates (tb@ufc.br)
2014.2
1 Introducao
Pilhas estao entre as estruturas de dados mais simples, e provavelmente mais frequentemente uti-
lizadas, juntamente com las e listas. Uma estrutura de pilha oferece as operacoes de empilhar
(adicionar) e desempilhar (remover) um elemento, analogas `as operacoes de empilhar um livro sobre
uma pilha de livros, ou desempilhar o livro que se encontra na posi cao mais alta da pilha. Conforme
veremos nos exemplos e exerccios deste captulo, pilhas surgem naturalmente em diversas aplicacoes.
Seja E um conjunto de elementos quaisquer (n umeros inteiros, por exemplo), alguns dos quais
desejamos armazenar para posterior acesso ou modicacao. Uma pilha de elementos de E e uma
estrutura que armazena uma sequencia de objetos provenientes de E. A ideia de sequencia entre os
objetos e essencial na denicao de uma pilha, pois a ordem dos elementos nesta sequencia determina
a ordem em que os elementos poderao ser acessados posteriormente: os elementos inseridos na pilha
so podem ser recuperados de acordo com a ordem inversa de sua inser cao, tal como na pilha de livros
utilizada como analogia.

E importante destacar que, de acordo com nossa deni cao, uma pilha pode
conter elementos repetidos.
Seja P uma estrutura de dados pilha. Se P nao for vazia, o elemento que foi adicionado mais
recentemente a P e denominado de topo de P. Caso contrario, dizemos que P nao possui topo.
Durante uma operacao empilhar aplicada sobre P, um novo elemento x e adicionado a P e passa
a ser o novo topo da pilha. Durante uma operacao desempilhar aplicada sobre P, tres objetivos
principais sao atingidos: (i) o elemento y que era o topo da pilha e removido de P; (ii) caso P ainda
possua algum elemento, aquele que foi adicionado mais recentemente a P passa a ser o novo topo
de P; e (iii) o elemento y e retornado ao codigo que invocou a operacao desempilhar.
Em resumo, podemos dizer que o seguinte conjunto de regras dene as opera coes de empi-
lhar/desempilhar sobre uma pilha P:
(ii) ao empilhar um elemento x, este elemento passa a ser o topo da pilha;
(ii) ao desempilhar o topo da pilha (assumindo que a pilha nao se encontra vazia), o elemento
(topo) e removido de P e retornado ao codigo que requisitou a operacao de desempilhar, e a
informa cao sobre o topo de P e atualizada;
(iii) uma vez que o elemento x foi inserido na pilha, ele somente sera removido se uma opera cao
de desempilhar for realizada enquanto x for o topo de P.
1
Exemplo 1.1 Considere uma pilha de n umeros inteiros P contendo tres elementos, conforme mos-
trado na Figura 1(a). Cada elemento e representado por um retangulo que contem o n umero inteiro
em questao. O topo da pilha e apontado por uma seta. A Figura 1(b) ilustra o resultado da operacao
de desempilhar sobre a pilha da Figura 1(a). Similarmente, a Figura 1(c) mostra o resultado das
aplicacoes sucessivas das operacoes de empilhar o elemento 5 e empilhar o elemento 8 sobre a pilha
da Figura 1(b).
(a) Pilha original (b) Apos opera cao de desempilhar (c) Apos operacoes de empilhar
Figura 1: Exemplo de operacoes de empilhar e desempilhar.

E importante refor car a ideia de que apenas um dos objetos armazenados na pilha esta realmente
acessvel em um determinado momento, e pode ser obtido prontamente por meio de uma opera cao de
desempilhar. Este fato pode parecer uma limita cao articial e desnecessaria, mas a realidade e que a
estrutura de pilha pode ser muito conveniente para assegurar que o acesso a um determinado conjunto
de dados aconte ca de acordo com uma ordem bastante especca, garantindo que o processamento
dos dados armazenados se dara na ordem desejada. Como exemplo, considere a funcionalidade tpica
de um editor de texto, discutida a seguir no Exemplo 1.2.
Exemplo 1.2 Desfazer/Refazer. Considere as operacoes de Desfazer/Refazer (Undo/Redo, em
Ingles) de um editor de texto padrao. Imagine que as seguinte operacoes foram realizadas sobre um
texto, nesta ordem: justicar paragrafo, adicionar palavra, mudar cor do texto. Imagine que, apos
a terceira operacao, a funcao Desfazer foi utilizada. O sistema deve garantir que a operacao mais
recentemente efetuada sobre o texto (e somente ela) seja desfeita. Ou seja, a cor original do texto
sera restaurada. Similarmente, se a operacao Desfazer for novamente utilizada, a operacao mais
recentemente aplicada sobre o texto atual (adicionar palavra) sera desfeita. A funcao Desfazer de
um editor de texto deve seguir, portanto, a regra de funcionamento de uma pilha. Da mesma forma,
a operac ao de Refazer deve utilizar uma segunda pilha, permitindo que um conjunto de operacoes
previamente desfeitas sejam refeitas, ou seja, reaplicada sobre o texto, na ordem em que haviam
sido originalmente aplicadas.
2 Implementacao por vetor
A maneira mais simples de se implementar uma estrutura de pilha e por meio de um vetor. Considere
o exemplo de uma pilha de inteiros. Neste caso, um vetor de inteiros sera utilizado para armazenar
de forma sequencial os elementos armazenados na pilha. Juntamente com este vetor, uma variavel
inteira, chamada topo, armazenara a posicao do elemento armazenado no topo da pilha em um
dado momento. Por m, uma variavel inteira adicional, chamada tamanhoMax, sera utilizada para
guardar o n umero maximo de elementos que a pilha podera armazenar. Por questao de organiza cao,
utilizaremos um registro para armazenar as variaveis topo e tamanhoMax, alem de um ponteiro para
inteiro dados, que ira apontar para o vetor de dados, conforme mostrado a seguir.
2
struct Pilha {
int * dados;
int topo;
int tamanhoMax;
};
2.1 Iniciando a pilha
Antes de se usar uma variavel do tipo Pilha, e necessario inicia-la adequadamente. Ha varias razoes
para isto: (a) uma quantidade de memoria deve ter sido reservada para armazenar a pilha, (b) a
pilha deve estar inicialmente vazia (sem nenhum elemento armazenado), e (c) a variavel topo deve
assumir um valor que reita a situa cao de pilha vazia. Utilizaremos a convencao de que a variavel
topo tem valor 1 em um pilha vazia. De forma geral, a variavel tera valor k 1 em uma pilha con-
tendo k elementos. O primeiro elemento armazenado na pilha ocupara a posicao 0 do vetor apontado
por dados, o segundo elemento ocupara a posicao 1, e assim por diante. Desta forma, se a estrutura
for corretamente iniciada (e as operacoes subsequentes corretamente implementadas) e possvel se
determinar o n umero de elementos atualmente armazenados na pilha por meio da inspecao do valor
de topo.
O seguinte codigo pode ser utilizado para se iniciar uma variavel do tipo Pilha, passada como
parametro, por referencia:
void iniciar(Pilha &p, int tamanho) {
p.tamanhoMax = tamanho;
p.topo = -1; // convencao: topo -1 significa que a pilha esta vazia
p.dados = new int[p.tamanhoMax]; // vetor de dados alocado dinamicamente
}
2.2 Empilhando um elemento
Conforme descrito anteriormente, a opera cao de empilhar um elemento em uma pilha envolve o
armazenamento do elemento e a atualizacao da informacao sobre o topo da pilha. Em nossa imple-
mentacao por vetor, antes de armazenar o novo elemento no vetor dados e atualizar a variavel topo,
e importante vericar se a pilha ainda comporta mais um elemento, ou seja, se o tamanho da pilha
ainda nao atingiu seu limite, fornecido pelo campo tamanhoMax. No codigo a seguir, o parametro
dado armazena o novo elemento que se deseja armazenar na pilha.
void empilhar(Pilha &p, int dado) {
if (p.topo < (p.tamanhoMax-1)) {
++ p.topo;
p.dados[p.topo] = dado;
}
else
cout << "Pilha esta cheia!" << endl;
}
Perceba que, se o valor de topo for p.tamanhoMax-1, entao a pilha contem tamanhoMax elementos
e nao podemos realizar a operacao empilhar. Assim sendo, se nao sabemos de antemao que a
pilha nao esta cheia, e prudente vericar se este fato e verdade. Para tanto, podemos escrever uma
fun cao simples, que podemos chamar de cheia, para retornar um booleano informando se a pilha
encontra-se ou nao com sua capacidade maxima utilizada. A implementacao desta fun cao e deixada
como exerccio para o leitor.
3
2.3 Desempilhando um elemento
Assim como na opera cao de empilhar, e necessario vericar o tamanho da pilha antes de aplicar
a opera cao desempilhar: a pilha deve conter pelo menos um elemento para que a operacao seja
realizada. Para tanto, e suciente vericar se a variavel topo nao indica uma pilha vazia (ou seja,
topo e diferente de -1). Como a fun cao deve retornar o elemento que ocupava o topo da pilha, e
preciso denir o valor que sera retornado caso a pilha esteja vazia. Uma conven cao bastante utilizada
em tais casos e a de retornar um valor que nao esteja no domnio dos valores possveis para a pilha.
Em nossa implementacao, iremos convencionar um valor de retorno igual a -999999 para sinalizar
que a pilha estava vazia e que nada foi desempilhado.
Caso a pilha possua algum elemento armazenado, a funcao deve remover o elemento do topo
e atualizar a informacao sobre a posicao do elemento que se encontrara no topo apos a operacao
ser concluda. Em nossa implementa cao por vetor, a remocao do elemento no topo e desnecessaria,
sendo suciente atualizar o valor da variavel topo (anal, sobrescrever o valor no topo com algum
outro valor inteiro nao nos traz nenhum ganho). Perceba que e possvel que a pilha se torne vazia
apos a operacao de desempilhamento, isto e, topo pode assumir o valor -1.
int desempilhar(Pilha &p) {
int valor = -999999;
if (p.topo >= 0) {
valor = p.dados[p.topo];
-- p.topo;
}
else
cout << "Pilha esta vazia!" << endl;
return valor;
}

E recomendado vericar se a pilha encontra-se vazia antes de se requisitar uma opera cao de
desempilhamento. Caso contrario, dependendo dos valores que se pretende armazenar na pilha, e
possvel que o valor -999999 retornado seja confundido com um valor valido que estava armazenado
no topo da pilha. A opera cao de vericar se a pilha encontra-se vazia e de implementacao simples
e sugerida como exerccio ao leitor. Uma maneira de se fazer uso seguro do codigo de desempilhar
seria:
if (! vazia(P)) {
int valor = desempilhar(P);
// processamento do dado armazenado em valor...
}
Outra maneira, que pode ser considerada mais elegante, de se implementar a fun cao desempilhar
seria conforme descrito a seguir. A funcao retornaria um valor booleano, informando se houve ou
nao o desempilhamento. Alem disso, a funcao receberia um parametro inteiro por referencia, o qual
seria utilizado para armazenar o valor que se encontrava no topo da pilha antes do desempilhamento
(caso a pilha nao estivesse vazia). O seguinte codigo ilustra um uso seguro desta implementa cao
alternativa:
int valor;
if (desempilhar(P, valor)) {
// processamento do dado armazenado em valor...
}
4
3 Implementacao por encadeamento
Sugere-se que o aluno tome nota do conte udo a ser visto durante a aula e estude o codigo fornecido.
4 Exerccios
1. Escrever a funcao vazia, que recebe uma pilha e retorna um booleano indicando se a pilha
encontra-se ou nao vazia.
2. Escrever uma fun cao esvaziar, que recebe uma pilha (implementada por vetor) e a transforma
em uma pilha vazia, porem ainda perfeitamente funcional: isto e, a pilha nao e destruda
durante a operacao. Sua funcao deve executar em tempo O(1) isto e, deve executar uma
quantidade de operacoes que nao depende do tamanho maximo da pilha, nem do n umero de
elementos atualmente armazenados nela.
3. Realizar a mesma tarefa da questao anterior, mas sobre uma pilha implementada por encade-
amento. Ainda e possvel realizar tal operacao em tempo constante, isto e, em tempo O(1)?
Por gentileza, explique sua resposta.
4. Utilizar uma pilha para vericar se uma expressao e balanceada por parenteses, colchetes e
chaves (BPCC). Uma expressao e dita BPCC se as duas condicoes a seguir sao verdadeiras:
Os n umeros de parenteses, colchetes e chaves abrindo isto e, (, [ e { devem
ser iguais aos n umeros de parenteses, colchetes e chaves fechando isto e, ), ] e }
, respectivamente;
Os caracteres de fechamento (), ] e }) devem respeitar a ordem inversa em que os
caracteres de abertura ((, [ e {) aparecem na expressao.
Ou seja, se imaginarmos que cada par de caracteres de abertura e fechamento dene um
intervalo na expressao, entao a rela cao entre dois intervalos I e J quaisquer deve ser uma das
seguintes op coes: I contem J, J contem I, I termina antes de J comecar, ou J termina
antes de I comecar.
Exemplos:
aaab(5+3*[3-1+a] expressao invalida
aaab(5+3*[3-1+a]} expressao invalida
aaab(5+3*[3-1+a)] expressao invalida
aaab(5+3*[3-1+a]) expressao valida
((([((i))]))) expressao valida
(a+4[6*3-1a)e] expressao invalida
(a+4 expressao invalida
(a+4} expressao invalida
5. Na implementa cao do algoritmo Quicksort, utilizamos recursao para ordenar vetores menores
a cada parti cao do subvetor atual. Como discutido em sala, chamadas recursivas utilizam uma
estrutura de pilha que e gerenciada automaticamente pelo programa. Se a profundidade da
recurs ao for muito grande, ou se a linguagem de programa cao utilizada nao permitir recursao, e
possvel ainda implementar algoritmos recursivos, utilizando uma estrutura de pilha projetada
pelo programador. Nesta questao, vamos adaptar o codigo do algoritmo Quicksort visto em
sala para evitar o uso de recursao.
5
Inicialmente, note que cada chamada recursiva da fun cao qs esta associada com um subvetor
do vetor original (inicialmente, o subvetor trata-se do vetor original completo). Assim, a cha-
mada qs(v, a, j) consiste da aplicacao do algoritmo Quicksort ao subvetor v[a] ...v[j-1].
Como este intervalo [ a, j ) e a informacao essencial `a recursao (e ele quem dene os dados que
serao utilizados em cada chamada de qs), vamos criar uma pilha de intervalos para simular
a recursao. Para tanto, precisamos criar um registro do Intervalo, que contera dois campos
do tipo int: os campos a e b, correspondentes aos parametros a e b de qs.
Uma vez com tal registro em maos, e possvel adaptar qualquer uma de nossas implementacoes
de pilha para lidar com dados que armazena elementos do tipo Intervalo. Feito isso, podemos
ajustar o codigo do Quicksort da seguinte maneira. A fun cao qs deixa de ser recursiva, passa
a ter uma pilha PI do tipo Intervalo e um la co de repeticao que executara enquanto PI nao
for vazia. Inicialmente, PI contera o intervalo [ 0, n ). A cada iteracao do laco, teremos os
seguintes passos:
(i) um intervalo itvl = [ a, b ) sera desempilhado de PI;
(ii) a funcao particionar sera chamada para o subvetor denido por itvl; e
(iii) as chamadas recursivas de qs (que eram qs(v, a, j) e qs(v, j+1, b)) serao subs-
titudas pelas operacoes de empilhamento dos intervalos [ a, j ) e [ j+1, b ) em PI.
6