Capítulo 18: Pilhas¶
Tópicos
18.1 Tipos abstratos de dados¶
Os tipos de dados que você viu até agora são todos concretos, no sentido que nós especificamos completamente como eles são implementados. Por exemplo, a classe Card(XXX ver como foi traduzido) representa uma carta utilizando dois inteiros. Como discutimos no momento, esta não é a única maneira de representar uma carta; existem muitas implementações alternativas.
Um tipo abstrato de dado, ou TAD, especifica um conjunto de operações (ou métodos) e a semântica das operações (o que elas fazem), mas não especifica a implementação das operações. Isto é o que o faz abstrato.
Por que isto é útil?
Simplifica a tarefa dde especificar um algoritmo se você pode XXXdenotar(denote) as operações que você precisa sem ter que pensar, ao mesmo tempo, como as operações são executadas.
Uma vez que existem geralmente muitas maneiras de implementar um TAD, pode ser útil escrever um algritmo que pode ser usado com qualquer das possíveis implementações.
TADs bastante conhecidos, como o TAD Pilha deste capítulo, já estão implementados em bibliotecas padrão, então eles podem ser escritos uma vez e usado por muitos programadores.
As operações em TADs provêm uma linguagem de alto nível comum para especificar e falar sobre algoritmos.
Quando falamos sobre TADs, geralmente distinguimos o código que usa o TAD, chamado cliente, do código que implementa o TAD, chamado código fornecedor.
18.2 O TAD Pilha¶
Neste capítulo, iremos olhar um TAD comum, a pilha. Uma pilha é uma coleção, ou seja, é uma estrutura de dados que contei múltiplos elementos. Outras coleções que vimos incluem dicionários e listas.
Um TAd é definido pelo conjunto de operações que podem ser executadas nele, que é chamado interface. A interface para uma pilha consiste nestas operações:
__init__
: Inicializa uma nova pilha vazia.
push
: Adiciona um novo item na pilha
pop
: Remove um ítem da pilha e o retorna, O ítem que é retornadao é sempre o último adicionado.
isEmpty
: Verifica se a pilha está vazia.
Uma às vezes é chamada uma estrutura de dados “last in, first out” ou LIFO, porque o último ítem adicionad é o primeiro a ser removido.
18.3 Implementando pilhas com listas de Python¶
As operações de lista que Python oferecem são similares às operações que definem uma pilha. A interface não é exatamente o que se supõe ser, mas podemos escrever um código para traduzir do TAD Pilha para as operações nativas.
Este código é chamado uma implementação do TAD Pilha. Geralmente, uma implementaçõa é um conjunto de métodos que satisfazem os requisitos sintáticos e semânticos de uma interface.
Aqui está uma implementação do TAD Pilha que usa uma lista do Python:
class Stack :
def __init__(self) :
self.items = []
def push(self, item) :
self.items.apend(item)
def pop(self) :
return self.items.pop()
def isEmpty(self) :
return (self.items == [])
Um objeto Stack
contém um atributo chamado items
que é uma lista de ítens na pilha. O método de inicialização define items como uma lista vazia.
Para adicionar um novo ítem na pilha, push
o coloca em items
. Para remover um ítem da pilha, pop usa o método de lista homônimo¹ para remover e retornar um último ítem da lista.
Finalmente, para verificar se a pilha está vazia, isEmpty
comprara items a uma lista vazia.
Uma implementação como esta, na qual os métodos consistem de simples invocações de métodos existentes, é chamado revestimento. Na vida real, revestimento é uma fina camada de madeira de boa qualidade usado em XXX*furniture-making* para esconder madeira de menor qualidade embaixo. Cientistas da Computação usam esta metáfora para descrever um pequeno trecho de código que esconde os detalhes de uma implementação e fornece uma interface mais simples, ou mais padronizada.
18.4 Empilhando e desempilhando¶
Uma pilha é uma estrutura de dados genérica, o que significa que podemos adicionar qualquer tipo de ítem a ela. O exemplo a seguir empilha dois inteiros e uma XXXstring na pilha:
>>> s = Stack()
>>> s.push(54)
>>> s.push(45)
>>> s.push("+")
Podemos usar isEmpty
e pop para remover e imprimir todos os ítens da pilha:
- while not s.isEmpty() :
priint s.pop()
A saída é + 45 54. Em outras palavras, usamos a pilha para imprimir os ítens ao contrário! Sabidamente, este não é o formato padrão de imprimir uma lista, mas, usando uma pilha, foi notavelmente fácil de fazer.
Você deve comparar este trecho de código com a implementação de printBackward na seção 17.4. Existe um paralelo natura entre a versão recursiva de printBackward e o algoritmo de pilha aqui. A diferenã é que printBackward usa a pilha de execução para XXXmanter a trilha(keep track) dos nós enquanto percorre a lista, e então imprime-a no retorno da recursão. o algoritmo de pilha faz a mesma coisa, exceto que usa o objeto Stack ao invés da pilha de execução.
18.5 Usando uma pilha para avaliar expressões pós-fixas¶
Em muitas linguagens de programação, expressões matemáticas são executadas com o poerador entre os roid operandos, como em 1 + 2. Este formato é chamado notação infixa. Uma alternativa usada por algumas calculadoras é chamada notação pós-fixa. Em notação pós-fixa, o operador segue os operandos, como em 1 2 +.
A razão pela qual a notação pós-fixa é útil algumas vezes é que existe uma maneira natural de avaliar uma expressão pós-fixa usando uma pilha:
começando no início da expressão, peque um termo (operador ou operando) de cada vez.
Se o termo é um operando, empilhe-o
Se o termo é um operador, desempilhe dois operandos, execute a operação neles, e empilhe o resultado.
Quando chegar ao fim da expressão, deve existir exatamente um operando sobrando na pilha. Este operando é o resultado.
Como um exercício, aplique este algoritmo à expressão 1 2 + 3 * .
Este exemplo demonstra uma das vantagens da notação pós-fixa - não é necessário usar parênteses para controlar a ordem das operações. Para ter o mesmo resultado em notação infixa, deveríamos escrever (1 + 2) * 3.
Como um exercício, escreva a expressão pós-fixa que é equivalente a 1 + 2 * 3.
18.6 Análise sintática¶
Para implementar o algoritmo anterior, necessitamos estar prontos para percorrer uma string e quebrá-la em operandos e operadores. Este processó é um exemplo de XXXparsing, e o resultado - os pedaços da string - são chamados XXXtokens. Você deve lembrar estas palavras do capítulo 1.
Python fornece um método split nos módulos string e re (expressões regulares). A função string.split
separa uma string numa lista usando um único caracter como delimitador. Por exemplo:
>>> import string
>>> string.split ("Now is the time", " ")
['Now', 'is', 'the', 'time']
Neste caso, o delimitador é o caracter de espaço, então a string é dividida a cada espaço.
A função re.split
é mais poderosa, permitindo-nos fornecer uma expresão regular ao invés de um delimitador. Uma expressão regular é uma maneira de especificar um conjunto de strings. Por exemplo, [A-z] é o conjunto de todas as letras e [0-9] é o conjunto de todos os dígitos. O operador ^nega um conunto, então [^0-9] é o conjunto de tudo o que não é número, que é exatamente o que queremos para dividir expressões pós-fixas.
>>> import re
>>> re.split ("[^0-9]", "123+456*/")
['123', '+', '456', '*', '', '/', ' ']
Note que a ordem dos argumentos é diferente de string.split, o delimitador vem antes da string.
A lista resultante inclui os operandos 123
e 456
, e os operadores *
e /
. Também inclui duas strings vazias que são inseridas depois dos operadores.
18.7 Avaliando em pós-fixo.¶
Para avaliar uma expressão pós-fixa, usaremos o parser da seção anterior e o algoritmo da seção anterior a ela. Para manter as coisas simples, começaremos com um avaliador que implementa somente os operadores +
e .
def evalPostfix (expr) :
import re
tokenList = re.split ("([^0-9])", expr)
stack = Stack()
for token in tokenList
if token == '' or token = ' ' :
continue
if token == '+' :
sum = stack.pop() + stack.pop()
stack.push(sum)
if token == '*' :
product = stack.pop() * stack.pop()
stack.push(product)
else:
stack.push(int(token))
return stack.pop()
A primeira condição cuida de espaços e strings vazias. As duas próximas condições manipulam os operadores. Nós assumimos, agora que qualquer coisa é um operador. É claro, seria melhor chegar por entrada errônea e enviar uma mensagem de erro, mas faremos isto depois.
Vamos testá-lo avaliando a forma pós-fixa de (56 + 47) * 2
>>> print (evalPostfix("56 47 + 2 *"))
206
XXXthat’s close enough
18.8 Clientes de fornecedores.
Um dos objetivos de um TAD é separar os interesses do fornecedor, quem escreve o código que implementa o TAD, e o cliente, que usa o TAD. O fornecedor tem que se preocupar apenas se a implementação está correta - de acordo com a especificação do TAD - e não como ele será usado.
Inversamente, o cliente assume que a implementação do TAD está correta e não se preocupa com os detalhes. Quando você está usando um tipo nativo do Python, tem o luxo de pensar exclusivamente como um cliente.
É claro, quanto você implementa um TAD, você também tem que escrever código cliente para testá-lo. Neste caso, você faz os dois papéis, o que pode ser confuso. Você deve fazer algum esforçõ para manter a trilha do papel que está fazendo a cada momento.
18.9 Glossário¶
- tipo abstrato de dados (TAD) (abstract data type(ADT)):
Um tipo de dado(geralmente uma coleção de objetos) que é definidopor um conjunto de operações, que podem ser implementadas de várias maneiras.
- interface (interface):
É o conjunto de operações que definem um TDA.
- implementação (implementation):
Código que satisfaz(preenche?) os requisitos sintáticos e semânticos de uma interface.
- cliente (client):
Um programa (ou o programador que o escreveu) que faz utilização de um TDA.
- fornecedor (provider):
Código (ou o programador que o escreveu) que implementa um TDA.
- revestimento (veneer):
Definição de classe que implementa um TDA com definições de métodos que são chamadas a outros métodos, às vezes com pequenas modificações. A lâmina faz um trabalho insignificante, mas melhora ou padroniza a interface dada ao cliente.
- estrutura de dados genérica (generic data structure):
Tipo de estrutura de dados que contem data de um tipo qualquer(tipo genérico).
- infixa (infix):
Notação matemática em que os operadores se situam entre os operandos.
- pós-fixa (postfix):
Notação matemática em que os operadores se situam após os operandos.
- XXX parse (parse):
Ler um conjunto de caracteres(string de caracteres) ou tokens(símbolos atômicos) e analisar sua estrutura gramatical.
- XXX token (token):
Conjunto de caracteres que são tratados como uma unidade atômica para fins de análise gramatical, como as palavras na linguagem natural.
- delimitador (delimiter):
Um caracter que é utilizado para separar os símbolos atômicos(tokens), como a pontuação na linguagem natural.