Algoritmos e Estrutura de Dados (AED) 2º semestre 2022/2023 Semana 4 Pilhas e Filas - implementação dinâmica Sumário ▪ Este módulo pretende introduzir implementação dinâmica (Linked-Lists) dos tipos de dados Pilhas e Filas ▪ Parte 1: ▪ Stacks – Implementação com Linked-Lists ▪ Queues – Implementação com Linked-Lists ▪ Parte 2: ▪ Genéricos (Generics) ▪ Iteradores ▪ Aplicações Referência ▪ Algorithms, 4th edition by R. Sedgewick and K. Wayne, Addison-Wesley Professional, 2011: ▪ Chapter 1.3 ▪ Lecture Slides: Bags, Queues, and Stacks h t t p : / / a l g s 4. c s . p r i n c e t o n . e d u Pilha (Stack) – Implementação com Linked-List Stack – Implementação com Linked-List ▪ Representação: Sequência de nós ligados ▪ A gestão do espaço na memória é dinâmica: • Cria espaço para um novo nó sempre que necessário (cria nova instância com new) • Liberta o espaço de um nó sempre que este deixa de ser necessário (Garbage Collector) ▪ first: atributo usado para manter a referência do primeiro nó (representa o topo da pilha) ▪ push(item): adiciona um novo item antes do primeiro ▪ pop(): remove o item apontado por first topo da pilha first be to not or be to null Stack – Implementação com Linked-List ▪ Classe Node: ▪ Classe interna que representa um nó ▪ item: contém um item da pilha ▪ next: contém a referência para o próximo nó da pilha, ou null private class Node { String item; Node next; nó item hello null next } classe interna privada (não necessita de definir modificadores de acesso para os atributos) Stack – Implementação com Linked-List ▪ Exemplo: to first push("to") be first push("be") or first push("or") not push("not") push("to") first to push("be") pop() to null be to null or be to null not first or be to null not pop() null first be or be to null not first or be to null not first or be to null Stack – Implementação com Linked-List Classe Nó ▪ Operação pop(): private class Node { String item; ▪ Salva item do topo da pilha Node next; } String item = first.item; ▪ Remove topo da pilha first or be to null first = first.next; first ▪ Devolve o item return item; or be to null Stack – Implementação com Linked-List Classe Nó ▪ Operação push(item): private class Node { String item; Node next; } ▪ Salva a referência do topo da pilha oldfirst Node oldfirst = first; first or be to null ▪ Cria um novo nó para o topo oldfirst or first = new Node(); be first to null ▪ Inicializa os atributos do novo nó oldfirst first.item = item; first.next = oldfirst; not first or be to null Stack – Implementação com Linked-List public class LinkedStackOfStrings { private Node first = null; private class Node { String item; Node next; } public boolean isEmpty() { return first == null; } public void push(String item) { Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; } public String pop() { String item = first.item; first = first.next; return item; } } Stack – Linked-List: Desempenho ▪ Proposição (Tempo de execução): ▪ Cada operação usa tempo constante no pior caso ▪ Proposição (Memória): ▪ Uma pilha de Strings com N itens usa ~ 40 N bytes Classe interna 16 bytes (cabeçalho) private class Node 8 bytes (cabeçalho extra para classe interna) { } String item; 8 bytes (referência para String) Node next; 8 bytes (referência para Node) 40 bytes para cada Node ▪ Nota: O valor só está a contabilizar a memória necessária para a pilha (não considera a memória utilizada para as Strings) Stack – Array Redimensionável vs. Linked-List ▪ Compromissos: ▪ Pode-se implementar uma pilha tanto com array redimensionável como com linked-list ▪ Cliente pode escolher a implementaçáo de forma intercambiável ▪ Implementação com array redimensionável: ▪ Cada operação leva um tempo amortizado constante ▪ Menos desperdício de espaço (memória) N=4 to be or not null null null null ▪ Implementação com linked-list: ▪ Cada operação leva tempo constante no pior caso ▪ Utiliza tempo e espaço extra para lidar com as ligações entre nó Fila (Queue) – Implementação com Linked-List Queue – Implementação com Linked-List ▪ Representação: Sequência de nós ligados ▪ A gestão do espaço na memória é dinâmica: • Cria espaço para um novo nó sempre que necessário (cria nova instância com new) • Liberta o espaço de um nó sempre que este deixa de ser necessário (Garbage Collector) ▪ first: atributo usado para manter a referência do primeiro nó ▪ last: atributo usado para manter a referência do último nó ▪ enqueue(item): adiciona novo item depois do apontado por last ▪ dequeue(): remove o item apontado por first last first to be or not to be null Queue – Implementação com Linked-List ▪ Classe Node: ▪ Classe interna que representa um nó ▪ item: contém um item da fila ▪ next: contém a referência para o próximo nó da fila, ou null private class Node { String item; Node next; nó item hello null next } classe interna privada (não necessita de definir modificadores de acesso para os atributos) Queue – Implementação com Linked-List ▪ Exemplo: enqueue("to") first to null to enqueue("be") first enqueue("not") enqueue("to") dequeue() enqueue("be") dequeue() first first be null to enqueue("or") last be last or null to be or last not null to be first or not last to null be first or not to null be or first not to last last be null first or not to be null last last Queue – Implementação com Linked-List Classe Nó ▪ Operação dequeue(): private class Node { String item; ▪ Salva item da primeira posição da fila Node next; } String item = first.item; ▪ Remove o primeiro nó da fila last to first be or null last first = first.next; first to be if (first == null) last = null; null (caso especial, em que a fila fica vazia) ▪ Devolve o item salvo return item; or last first to null last first to null null Queue – Implementação com Linked-List ▪ Operação enqueue(item): oldlast ▪ Salva a referência do último nó last to first Node oldlast = last; be null ▪ Cria um novo nó para o último e inicializa-o last = new Node(); last.item = item; last.next = null; oldlast last to first be or not null null oldlast ▪ Liga o novo nó à fila to if (!isEmpty()) oldlast.next = last; else first = last; or first be or last not null (caso especial, em que a fila era vazia) first last last oldlast null first to null Queue – Implementação com Linked-List public class LinkedQueueOfStrings { private Node first, last; private class Node {/* mesmo que em LinkedStackOfStrings */ } public boolean isEmpty() { return first == null; } public void enqueue(String item) { Node oldlast = last; last = new Node(); last.item = item; ~ constante last.next = null; if (isEmpty()) first = last; else oldlast.next = last; } public String dequeue() { String item = first.item; first = first.next; if (isEmpty()) last = null; return item; } } ~ constante Genéricos (Generics) Genéricos – Motivação ▪ Tipo implementado: Pilha de Strings (StackOfStrings) ▪ Como implementar outros tipos? ▪ Ex: Pilha de URLs, Pilha de Inteiros, Pilha de Carrinhas, … ▪ Tentativa 1: Implementar uma classe separada para cada tipo de pilha ▪ Ex: StackOfURLs, StackOfIntegers, StackOfVans, … ▪ A reescrita do código é entediante e suscetível à erros ▪ Repetição de código (“copy & paste”) deve ser evitado! ▪ Mas… foi a abordagem utilizada até o Java JDK 1.5 Genéricos – Motivação ▪ Como implementar outros tipos? ▪ Ex: Pilha de URLs, Pilha de Inteiros, Pilha de Carrinhas, … ▪ Tentativa 2: Implementar uma pilha genérica StackOfObjects Todas as classes do Java derivam com itens do tipo Object da classe Object ! ▪ Requer conversões (casting) do tipo Object para String, Integer, Van,… ▪ Casting é suscetível à erros - pode gerar erros em tempo de execução (run-time error) se os tipos não corresponderem public static void main(String[] args) { StackOfObjects s = new StackOfObjects(); Apple a = new Apple(); Não especifica se será uma pilha Orange b = new Orange(); de Apples ou de Oranges … s.push(a); s.push(b); a = (Apple) (s.pop()); } run-time error – Erro na conversão de Orange para Apple ! Solução com Genéricos (Generics) ▪ Implementar um tipo genérico Stack<E> que aceita itens de qualquer tipo E ▪ Stack<E>: Tipo genérico de pilha ▪ E: Parâmetro de tipo genérico (similar a parâmetros de métodos) ▪ Vantagens: • Evita casting • Descobre os erros de correspondência entre os tipos em tempo de compilação (evita run-time error) public static void main(String[] args) { Stack<Apple> s = new Stack<Apple>(); Apple a = new Apple(); Orange b = new Orange(); s.push(a); s.push(b); a = s.pop(); } compile-time error Genéricos (Generics) – Definições ▪ Definições: ▪ Stack<E>: Tipo genérico de pilha ▪ E: Parâmetro de tipo genérico (similar a parâmetros de métodos) ▪ Tipo parametrizado: Obtido por instanciação do tipo genérico, substituindo o parâmetro E por um tipo de objetos concretos • Ex: Stack<String> Tipo Genérico Stack<E> instanciação Tipo Parametrizado Stack<String> Parâmetro de tipo genérico Parâmetro de tipo concreto public class Stack<E> { ... } public static void main(String[] args) { Stack<String> stack = new Stack<String>(); ... } Genéricos (Generics) – Definições ▪ Parâmetro de tipo genérico: ▪ Declaração: • A seguir ao nome da classe • Entre parêntesis angulares <...> public class Stack<E> { ... } ▪ Nome da variável: • Aceita qualquer nome (Ex: E, Item, T, K, …) • Aceita mais que uma variável, separadas por vírgula • Convenção Java: Utilizar letra maiúscula (Ex: E para elemento de uma coleção) ▪ Parâmetro de tipo concreto: ▪ Só aceita tipos referenciados (tipos de objetos) ! • Ex: String, Date, ... Genéricos (Generics) – Definições ▪ Pode ser usado para declarar: ▪ Variáveis locais e atributos public class LinkedStackOfStrings { ... private class Node { String item; Node next; } ... public class Stack<E> { ... private class Node { E item; Node next; } ... ▪ Parâmetros de métodos public void push(String item) { ... } public void push(E item) { ... } ▪ Tipos de retorno de métodos public String pop() { ... } public E pop() { ... } Stack<E> – Implementação com Linked-List public class LinkedStackOfStrings { private Node first = null; } public class Stack<E> { private Node first = null; private class Node { String item; Node next; } private class Node { E item; Node next; } public boolean isEmpty() { return first == null; } public boolean isEmpty() { return first == null; } public void push(String item) { Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; } public void push(E item) { Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; } public String pop() { String item = first.item; first = first.next; return item; } public E pop() { E item = first.item; first = first.next; return item; } } Stack<E> – Implementação com Array Fixo ▪ Instanciação de array genérico não é permitida em Java: ▪ Sem genérico: public class FixedCapacityStackOfStrings { private String[] s; private int N = 0; public FixedCapacityStackOfStrings(int capacity) { s = new String[capacity]; } ... ▪ Com genérico (com erro de compilação !): public class FixedCapacityStack<E> { private E[] s; private int N = 0; public FixedCapacityStack(int capacity) { s = new E[capacity]; } ... Stack<E> – Implementação com Array Fixo ▪ Instanciação de array genérico não é permitida em Java: ▪ Sem genérico: public class FixedCapacityStackOfStrings { private String[] s; private int N = 0; public FixedCapacityStackOfStrings(int capacity) { s = new String[capacity]; } ... ▪ Com instância de Object + casting: (solução válida, apesar de não ser muito “elegante”) public class FixedCapacityStack<E> { private E[] s; private int N = 0; public FixedCapacityStack(int capacity) { s = (E[]) new Object[capacity]; } ... “the ugly cast" Stack<E> – Implementação com Array Fixo public class FixedCapacityStack<E> { private E[] s; private int N = 0; public FixedCapacityStack(int capacity) { s = (E[]) new Object[capacity]; } public boolean isEmpty() return N == 0; } { public void push(E item) { s[N++] = item; } public E pop(){ E item = s[--N]; s[N] = null; return item; } } Stack<E> – Implementação com Array Fixo ▪ Nota: Quando forem compilar o código, o compilador Java irá apresentar uma mensagem do tipo “Unchecked cast” % javac FixedCapacityStack.java Note: FixedCapacityStack.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. % javac -Xlint:unchecked FixedCapacityStack.java FixedCapacityStack.java:26: warning: [unchecked] unchecked cast found : java.lang.Object[] required: E[] a = (E[]) new Object[capacity]; ^ 1 warning Classes Empacotadoras & Autoboxing ▪ Classes empacotadoras (Wrapper Type): ▪ Usadas para processar tipos primitivos (int, char, double, …) como objetos ▪ Exemplos: ▪ Para int: Classe Integer ▪ Para char: Classe Character ▪ Para double: Classe Double ▪ Autoboxing: ▪ Conversão automática entre um tipo primitivo e a empacotadora public static void main(String[] args) { Stack<Integer> s = new Stack<Integer>(); s.push(17); // s.push(Integer.valueOf(17)); int a = s.pop(); // int a = s.pop().intValue(); } Iteradores (Iterator) Iteradores ▪ Desafio: Possibilitar a iteração sobre uma coleção de elementos abstraindo-se da forma como essa coleção está implementada ▪ Iterar: Percorrer os itens da coleção, um a um. A ordem em em que se percorre não é especificada ▪ Ex: Suportar a iteração sobre os itens de uma Pilha (Stack), sem revelar a sua representação interna ao cliente (vem de um vetor? De uma lista? De uma pilha? Não é relevante!) i s[] it was the best of times null null null null 0 1 2 3 4 5 6 7 8 9 first times N current of best the was it null Iteradores no Java ▪ Solução: O Java oferece os interfaces Iterable e Iterator ▪ Iterable: Define o comportamento dos objetos iteráveis ▪ Os objetos que implementam o interface Iterable, podem ser processados com base num objeto iterador (Iterator) ▪ Ex: Para se conseguir iterar sobre um objeto Stack, a classe Stack deverá implementar a interface Iterable public class Stack<E> implements Iterable<E>{ ... } ▪ Iterator: Define o comportamento de um objeto iterador ▪ Iterar sobre os itens de um objeto iterável, abstraindo-se da forma como esse objeto está implementado Iteradores no Java ▪ Interface Iterable<E>: ▪ iterator(): Devolve o objeto iterador a utilizar para percorrer o objeto iterável public interface Iterable<E> { Iterator<E> iterator(); } ▪ Interface Iterator<E>: ▪ hasNext(): Devolve true caso ainda haja algum item por processar. Desta forma sabe-se se é ou não seguro usar next() ▪ next(): Devolve o item atual e avança para o seguinte. Deve ser lançada a exceção NoSuchElementException caso não se encontre nenhum elemento a processar public interface Iterator<E> { boolean hasNext(); E next(); default void remove(); } [opcional] Remove o último elemento obtido com next() Iteradores no Java ▪ Ex: public class Stack<E> implements Iterable<E>{ ... private class StackArrayIterator implements Iterator<E>{ ... } } .next() StackArray Iterator Stack<String> .iterator() .next() times of .next() best i s[] N it was the best of times null null null null 0 1 2 3 4 5 6 7 8 9 Iteradores no Java ▪ Porquê tornar as estruturas de dados iteráveis (Iterable)? ▪ Utilizar as ferramentas do Java para suportar códigos de cliente mais elegantes ▪ Ex: Ciclo for-each ▪ Forma mais abstrata de iterar sobre uma estrutura de dados (“para cada elemento, fazer…”) public static void main(String[] args) { Stack<String> stack = new Stack<String>(); stack.push("what"); stack.push("am I"); stack.push("doing"); for(String str : stack) System.out.println(str); } Simplificação do código abaixo (equivalente à): Iterator<String> i = stack.iterator(); while (i.hasNext()) { String str = i.next(); System.out.println(str); } Stack iterator – Implementação com array import java.util.Iterator; public class Stack<E> implements Iterable<E> { private E[] s; private int N = 0; ... // Implementação vista nos slides anteriores public Iterator<E> iterator() { return new StackArrayIterator(); } private class StackArrayIterator implements Iterator<E> { private int i = N; public boolean hasNext() { return i > 0; } public void remove() { /* not supported */ } public E next() { return s[--i]; } i N } } s[] it was the best of times null null null null 0 1 2 3 4 5 6 7 8 9 Stack iterator – Implementação com Linked-List public class Stack<E> implements Iterable<E> { private Node first = null; private class Node { E item; Node next; } ... // Implementação vista nos slides anteriores public Iterator<E> iterator() { return new StackListIterator(); } private class StackListIterator implements Iterator<E> { private Node current = first; public boolean hasNext() { return current != null; } throw new UnsupportedOperationException public void remove() { /* not supported */ } } throw new NoSuchElementException Se não houver mais itens para iterar public E next() { E item = current.item; current = current.next; return item; first } current } times of best the was it null Aplicações (Slides extras em Inglês) Java collections library ▪ List interface: java.util.List is API for an sequence of items public interface List<Item> implements Iterable<Item> List() create an empty list boolean isEmpty() is the list empty? int size() number of items void add(Item item) append item to the end Item get(int index) return item at given index Item remove(int index) return and delete item at given index boolean contains(Item item) Iterator<Item> iterator() does the list contain the given item? iterator over all items in the list ... ▪ Implementations. java.util.ArrayList uses resizing array; java.util.LinkedList uses linked list caveat: only some operations are efficient (from KW slides, used with permission) Java collections library ▪ java.util.Stack ▪ Supports push(), pop(), and iteration. ▪ Extends java.util.Vector, which implements java.util.List interface from previous slide, including get() and remove(). ▪ Bloated and poorly-designed API (why?) ▪ java.util.Queue: An interface, not an implementation of a queue ▪ Best practices: Use our implementations of Stack, Queue, and Bag (from KW slides, used with permission) War story (from Assignment 1) ▪ Generate random open sites in an N-by-N percolation system ▪ Jenny: pick (i, j) at random; if already open, repeat. Takes ~ c1 N 2 seconds. ▪ Kenny: create a java.util.ArrayList of N 2 closed sites. Pick an index at random and delete. Takes ~ c2 N 4 seconds. Why is my program so slow? Kenny ▪ Lesson: Don't use a library until you understand its API! ▪ This course: Can't use a library until we've implemented it in class (from KW slides, used with permission) Stack applications ▪ ▪ ▪ ▪ ▪ ▪ ▪ Parsing in a compiler. Java virtual machine. Undo in a word processor. Back button in a Web browser. PostScript language for printers. Implementing function calls in a compiler. ... (from KW slides, used with permission) Function calls ▪ How a compiler implements a function. ▪ Function call: push local environment and return address. ▪ Return: pop return address and local environment. ▪ Recursive function: Function that calls itself. ▪ Note: Can always use an explicit stack to remove recursion. gcd (216, 192) p = 216, q = 192 static int gcd(int p, if (q == 0) return else return gcd(q, } p = 192, q = 24 p = 24, q = 0 int q) { p; pgcd % q); (192, 24) static int gcd(int p, int q) { if (q == 0) return p; else return gcd(q, p % q); gcd (24, 0) } static int gcd(int p, int q) { if (q == 0) return p; else return gcd(q, p % q); } (from KW slides, used with permission) Arithmetic expression evaluation ▪ Goal: Evaluate infix expressions. value stack operator stack (1+((2+3)*(4*5))) ▪ Two-stack algorithm: [E. W. Dijkstra] ▪ Value: push onto the value stack. ▪ Operator: push onto the operator stack. ▪ Left parenthesis: ignore. ▪ Right parenthesis: pop operator and two values; push the result of applying that operator to those values onto the value stack. ▪ Context: An interpreter! (from KW slides, used with permission) Arithmetic expression evaluation public class Evaluate { public static void main(String[] args) { Stack<String> ops = new Stack<String>(); Stack<Double> vals = new Stack<Double>(); while (!StdIn.isEmpty()) { String s = StdIn.readString(); if (s.equals("(")) ; else if (s.equals("+")) ops.push(s); else if (s.equals("*")) ops.push(s); else if (s.equals(")")) { String op = ops.pop(); if (op.equals("+")) vals.push(vals.pop() + vals.pop()); else if (op.equals("*")) vals.push(vals.pop() * vals.pop()); } else vals.push(Double.parseDouble(s)); } StdOut.println(vals.pop()); } % java Evaluate } (1+((2+3)*(4*5))) 101.0 (from KW slides, used with permission) Correctness ▪ Q. Why correct? ▪ A. When algorithm encounters an operator surrounded by two values within parentheses, it leaves the result on the value stack. (1+((2+3)*(4*5))) ▪ as if the original input were: (1+(5*(4*5))) ▪ Repeating the argument: ( 1 + ( 5 * 20 ) ) ( 1 + 100 ) 101 ▪ Extensions. More ops, precedence order, associativity. (from KW slides, used with permission) Stack-based programming languages ▪ Observation 1: Dijkstra's two-stack algorithm computes the same value if the operator occurs after the two values. (1((23+)(45*)*)+) ▪ Observation 2. All of the parentheses are redundant! 123+45**+ ▪ Bottom line. Postfix or "reverse Polish" notation. ▪ Applications. Postscript, Forth, calculators, Java virtual Jan Lukasiewicz machine, … (from KW slides, used with permission) Laboratório desta Semana ▪ Exercício 12 (Folha de Exercícios): ▪ Implemente dinamicamente a seguinte API public class Queue<Item> implements Iterable<Item> Queue() void enqueue(Item item) Item dequeue() boolean isEmpty() int size() Iterator<Item> iterator() create an empty queue add an item remove the least recently added item is the queue empty? number of items in the queue support iteration