Algoritmos e Estruturas de Dados
Lição n.º 4
Pilhas
Pilhas
• Implementação com arrays redimensionáveis.
• Implementação com listas ligadas.
• Aplicação: avaliação de expressões
aritméticas usando o algoritmo da pilha
dupla, de Dijkstra.
Pilhas
• As pilhas chamam-se pilhas porque funcionam como pilhas...; pilhas de livros, de pratos, de papéis, não
pilhas no sentido de baterias elétricas!
• A ideia é que o elemento que sai é o último que entrou.
• Dizemos que as pilhas são coleções LIFO: last in first out.
20140225 Algoritmos e Estruturas de Dados 3
API da pilha
• Já sabemos:
• Comparando com Bag<T>, temos push em vez de add, temos pop e o iterador emitirá os elementos por ordem inversa de entrada na pilha (os que
entraram mais recentemente surgem primeiro).
public class Stack<T> implements Iterable<T>
{
public Stack()
public void push(T x) public boolean isEmpty() public int size()
public T pop()
public Iterator<T> iterator()
}
Implementação com arrays redimensionáveis
• É análoga à da classe Bag<T>:
20140225 Algoritmos e Estruturas de Dados 5
public class Stack<T> implements Iterable<T>
{
private T[] items;
private int size;
public Stack() {
// ...
}
private void resize(int capacity) {
// ...
}
public void push(T x) {
if (size == items.length) resize(2 * items.length);
items[size++] = x;
}
// ...
Se o array cresce quando está cheio, será que
deve encolher quando fica relativamente vazio?
Encolhendo no pop
• A estratégia será encolher para metade quando o tamanho estiver a 25% da capacidade:
public class Stack<T> implements Iterable<T>
{
// ...
public T pop() {
T result = items[--size];
items[size] = null;
if (size > 0 && size == items.length / 4) resize(items.length / 2);
return result;
}
// ...
Anula-se o apontador para que não
permaneça no array uma referência para o elemento removido, o que atrasaria a
garbage collection. Se não anulássemos, teríamos “vadiagem” (em inglês, loitering):
referências no array para valores inúteis.
Note bem: não seria sensato encolher para metade quando o array estivesse meio cheio, pois nesse caso ele ficava cheio logo após ter encolhido, e se houvesse um push a seguir, teria de crescer de novo.
Iterador reverso
• Nas pilhas, queremos iterar os elementos de maneira a ver primeiro os que entraram na pilha mais recentemente:
20140225 Algoritmos e Estruturas de Dados 7
// ...
private class StackIterator implements Iterator<T>
{
private int i = size;
public boolean hasNext() {
return i > 0;
}
public T next() {
return items[--i];
}
// ...
}
public Iterator<T> iterator() {
return new StackIterator();
}
// ...
Função de teste
• Eis uma função de teste, que empilha os números lidos e
desempilha com ‘-’. No fim mostra o tamanho, se é vazia e os elementos, por iteração:
private static void testStackInteger() {
Stack<Integer> s = new Stack<Integer>();
while (!StdIn.isEmpty()) {
String t = StdIn.readString();
if (!"-".equals(t))
s.push(Integer.parseInt(t));
else if (!s.isEmpty()) {
int x = s.pop();
StdOut.println(x);
} }
StdOut.println("size of stack = " + s.size());
StdOut.println("is stack empty? " + s.isEmpty());
StdOut.print("items:");
for (int x : s)
StdOut.print(" " + x);
StdOut.println();
}
Ctrl-D aqui!
Implementação com listas ligadas
• A lista ligada é programada em termos da classe
interna Node, a qual detém um campo para o valor e uma referência para o próximo nó:
20140225 Algoritmos e Estruturas de Dados 9
public class StackLists<T> implements Iterable<T>
{
private class Node {
private T value;
private Node next;
Node(T x, Node z) {
value = x;
next = z;
} }
// ...
Ver transparentes de POO,
página 13-8 e seguintes.
O primeiro nó
• Na pilha, um dos membros é o primeiro nó da lista; o outro é o tamanho:
• Omitimos o construtor por defeito, uma vez que os valores iniciais automáticos (null para first e 0 para size) são os que nos interessam.
public class StackLists<T> implements Iterable<T>
{
// ...
private Node first;
private int size;
// ...
Empilhar, desempilhar
• Para empilhar, criamos um novo nó, que passa a ser o primeiro, ligando-o ao que estava em primeiro antes:
• Para desempilhar, devolvemos o valor do primeiro nó, e avançamos para o seguinte a referência:
20140225 Algoritmos e Estruturas de Dados 11
public void push(T x) {
first = new Node(x, first);
size++;
}
public T pop() {
T result = first.value;
first = first.next;
size--;
return result;
}
isEmpty, size
• Os métodos isEmpty e size são muito simples:
public boolean isEmpty() {
return first == null;
}
public int size() {
return size;
}
Poderíamos ter omitido o campo size e ter programado a função size contando
iterativamente os nós, mas, por decisão de
desenho preferimos que função size seja
calculada em tempo constante.
Iterador de listas
• O cursor é uma referência para o nó corrente:
20140225 Algoritmos e Estruturas de Dados 13
private class StackIterator implements Iterator<T>
{
private Node cursor = first;
public boolean hasNext() {
return cursor != null;
}
public T next() {
T result = cursor.value;
cursor = cursor.next;
return result;
}
// ...
}
public Iterator<T> iterator() {
return new StackIterator();
}
Repare que o nó first contém o valor que mais recentemente foi empilhado, de entre todos os que estão na pilha.
Tal como na outra implementação,
os valores são enumerados pela
ordem inversa de entrada na pilha.
Avaliação de expressões aritméticas
• Como exemplo de aplicação das listas, estudemos a avaliação de expressões aritméticas usando o algoritmo da pilha dupla de Dijkstra.
• As expressões aritmética que nos interessam são
completamente parentetizadas, para evitar as questões da precedência dos operadores.
• Admitiremos os quatro operadores básicos (“+”, “-”, “*” e “/”) e a raiz quadrada (“sqrt”).
• Os operandos são números decimais, com ou sem parte decimal.
• Exemplos:
((7*2)+5)
((sqrt(9+16))/3)
(((6*3)+(7-3))/2)
Pilha dupla de Dijkstra
• Usamos duas pilhas: uma pilha de cadeias, para os operadores, e uma pilha de números, para os operandos e para os
resultados parciais.
• Varremos a expressão da esquerda para a direita, recolhendo os operadores, os operandos e os parênteses:
• Cada operador é empilhado na pilha dos operandos.
• Cada operando é empilhado na pilha dos números.
• Os parênteses a abrir são ignorados.
• Ao encontrar um parêntesis a fechar, desempilha-se um operador, depois desempilham-se os operandos (em número apropriado ao operador), aplica-se a operação a esses operando e empilha-se o resultado, na pilha dos números.
• Depois de o último parêntesis ter sido processado, a pilha dos
números terá um único elemento, cujo valor é o valor da expressão.
20140225 Algoritmos e Estruturas de Dados 15
Classe DijkstraTwoStack
• Primeiro as operações de suporte:
public final class DijkstraTwoStack {
private DijkstraTwoStack() { }
private static String[] knownOperators = {"+", "-", "*", "/", "sqrt"};
private static String[] binaryOperators = {"+", "-", "*", "/"};
private static String[] unaryOperators = {"sqrt"};
private static <T> boolean has(T[] a, T x) {
for (T i : a) if (x.equals(i)) return true;
return false;
}
private static boolean isOperator(String s) {
return has(knownOperators, s);
}
private static boolean isBinaryOperator(String s) {
return has(binaryOperators, s);
}
private static boolean isNumeric(String s) {
Busca linear.
Avaliação da expressão
• A seguinte função meramente exprime as regras do algoritmo:
20140225 Algoritmos e Estruturas de Dados 17
private static double evaluationSimple(String[] ss) {
Stack<String> operators = new Stack<String>();
Stack<Double> values = new Stack<Double>();
for (String s : ss)
if ("(".equals(s)) { } // nothing to do else if (isOperator(s)) operators.push(s);
else if (isNumeric(s)) values.push(Double.parseDouble(s));
else if (")".equals(s)) {
String op = operators.pop();
double z;
// ...
values.push(z);
}
else throw new UnsupportedOperationException();
return values.pop();
}
A expressão é repre- sentada pelo array dos tóquenes: operandos, operadores e
parêntesis.
Aplicação dos operadores
{
String op = operators.pop();
// StdOut.println(op);
double y = values.pop();
// StdOut.println(y);
double x = isBinaryOperator(op) ? values.pop() : 0.0;
// StdOut.println(x);
double z;
if ("+".equals(op)) z = x + y;
else if ("-".equals(op)) z = x - y;
else if ("-".equals(op)) z = x + y;
else if ("*".equals(op)) z = x * y;
else if ("/".equals(op)) z = x / y;
else if ("sqrt".equals(op)) z = Math.sqrt(y);
else throw new UnsupportedOperationException();
// will not happen;
Comprido e desinteressante.
Função de teste
• Para abreviar, determinamos que os tóquenes na expressão vêm separados por espaços.
• Assim, podemos obter o array de tóquenes diretamente, usando a função split:
20140225 Algoritmos e Estruturas de Dados 19
public static void testEvaluationSimple() {
while (StdIn.hasNextLine()) {
String line = StdIn.readLine();
double z = evaluationSimple(line.split(" "));
StdOut.println(z);
}
} split
public String[] split(String regex)
Splits this string around matches of the given regular expression.
O método split, da
classe String, devolve
um array de String.
Experiência
• Na linha de comando:
• Próximas tarefas:
• Evitar ter de separar os tóquenes por espaços.
• Evitar ter de distinguir os operadores com if-else em cascata
pedros-‐imac-‐4:bin pedro$ java -‐cp ../stdlib.jar:. DijkstraTwoStack ( 8 + 1 )
9.0
( sqrt 2 )
1.4142135623730951 ( ( 6 + 9 ) / 3 ) 5.0
( ( 5 + 1 ) / ( 7 + 17 ) ) 0.25
( sqrt ( 1 + ( sqrt ( 1 + ( sqrt ( 1 + ( sqrt ( 1 + 1 ) ) ) ) ) ) ) ) 1.6118477541252516