CAP.

2 – OPTIMIZAÇÂO DE DESEMPENHO

2.1. Introdução
A sobrecarga de operadores em classes com alojamento dinâmico (String, Bigint) que possam atingir grandes dimensões, requer a adopção de técnicas tendentes a evitar graves penalizações de desempenho. Os problemas são o aumento significativo de instanciações de objectos temporários e locais. Isto porque algumas funções de sobrecarga de operadores implicam o retorno, por valor, de objectos locais. O objectivo deste capítulo é descrever técnicas para minimizar o número de instanciações e cópias, de modo a não desmerecer em desempenho face ao paradigma procedimental. As classes de inteiros e fracções com múltipla precisão são pretexto para introduzir algoritmos numéricos e conceitos básicos que dão suporte a aplicações no domínio da segurança das comunicações.

2.2. Classe String
É entendida como uma classe contentora de caracteres, que providencia um nº alargado de métodos, como concatenação (+), append (+=), afectação (=), extracção e inserção sobre streams ( >> e << ), além de pesquisa, substituição, inserção e remoção de sub-strings, decomposição em palavras, etc. Uma estrutura string C-style consiste simplesmente numa sequência de caracteres terminada pelo carácter \O (carácter terminal). A classe String que a seguir se apresenta utiliza uma string C-style para a representação dos caracteres que contém. 2.2.1. Versão Básica Interface Pública: Construtores: String ( ) Construtor de string vazia String ( String&) Construtor por cópia String (char*) Construtor a partir de uma string C-style Destrutor: ~String ( ) Devolve ao heap o espaço nele reservado Métodos de acesso: int size ( ) Retorna o nº corrente de caracteres int capacity ( ) const char *c_str( ) Retorna a string C-style Operadores de afectação: String &operator= (const String&) Afectação por cópia String &operator= (const char*) Afectação com string C-style String &operator+= (const String&) append Métodos de acesso a caracteres: char readAt (int index) const void writeAt(int index, char c) Operadores globais String operator+ (const String&, const String&) canónicos são o construtor por cópia, operador afectação e o destrutor. Cada objecto da classe String terá como atributos um apontador para a representação, a dimensão da sequência de caracteres e a dimensão da memória alojada. A construção de um objecto com alojamento dinâmico da representação deve incluir os passos: - Reserva de espaço na memória livre (heap) - Afectação do respectivo atributo apontador com o endereço da estrutura - Cópia para o espaço reservado, dos dados a inserir A afectação por cópia, envolve: - Reservar, caso necessário, um novo espaço onde caiba a nova representação - Copiar - Devolver o espaço

- Afectar o respectivo atributo apontador com o endereço do novo espaço. O destrutor deve providenciar a devolução da memória ocupada. String – forma canónica --> pg. 60 Justifica-se a definição de métodos auxiliares, sempre que venham a ser invocados mais que uma vez na definição de outros métodos. è o caso do init ( ), do assign ( ) e do dimNormalize ( ). Na reserva de espaço adoptamos dimensões múltiplas de DIM_MIN (por potências de 2), excedentes a sz. O operador concatenação é definido como função global, dado tratar-se de um operador simétrico entre uma string e uma string C-style. Não necessita ser declarado como friend da classe String pois não precisa aceder a atributos privados dessa classe. Foi definido a partir do +=, o que é um estilo muito recomendável. O operador += devolve uma referência para o objecto alvo, pelo que não é penalizante para o tempo de execução. O que se torna gravoso na concatenação é a necessidade de retornar por valor o objecto String resultante e a construção do objecto String tmp, local à função. Veremos como melhorar. O uso do tipo size_t é recomendável para a portabilidade 2.2.2. Versão Handle/Body O desempenho da versão canónica anterior pode ser melhorado, evitando as múltiplas cópias da representação string C-style aquando da execução do construtor por cópia e do operador afectação por cópia. Isso consegue-se usando o idioma handle/body, que consiste em criar 2 classes copperantes: - Uma classe String (handle), única vísivel ao utilizador - Uma classe Srep (body) que se comporta como intermediária entre a classe String e a representação string C-style. A classe String tem como único atributo um apontador para Srep e apresenta uma interface idêntica à canónica. A classe Srep toma os atributos que na versão anterior pertenciam à classe String e além deles um contador de referências à representação string C-style, que memoriza o número de objectos String que em cada momento partilham dessa representação. Õs métodos são do encargo de Srep. A cada representação de String fica univocamente associado um objecto Srep. Instanciaçõe spor cópia ou afectações de objectos String com outros já existentes terão como consequência que uma mesma cadeia de caracteres ficará partilhada por vários objectos String, evitando assim múltiplas cópias de cadeias de caracteres. O custo disto é a atenção necessária quando uma alteração num objecto String poder vir a repercutir-se nos outros objectos que com ela partilham essa cadeia; terá pois de desligar-s epreviamente. 2.2.2.1. Definição da classe String (handle/body) ---> ver pg. 66 O tempo de desempenho é metade do canónico e menor que com a classe string da biblioteca do C++. 2.2.3. Optimização Quanto a Objectos Temporários O idioma handle/body tem impacto nas aplicações em que os objectos são construídos ou afectados por cópia frequentemente mas não são depois alterados. Nos casos como concatenação em strings ou operadores aritméticos em classes numéricas o idioma pouco beneficia pois os objectos são muitas vezes alterados. Então, seguindo o nosso ex. vai-se definir uma nova classe StrTmp (string temporária) e providenciar que em todas as circunstâncias em que a sobrecarga de operadores envolva a instanciação de objectos temporários, o objecto retornado por esses operadores seja dessa classe. Não esquecer que a principal causa de perda de desempenho em classes de grande porte com alojamento dinâmico reside nos operadores que retornam objectos por valor, criando localmente buffers de resultados. Por exemplo a concatenação de mais que 2 objectos string implica a criação de vários temporários, todos diferentes: s=s1+s2+s3+s4 envolve as seguintes acções: String tmp1 (operator+(s1,s2));

String tmp2 (operator+ (tmp1,s3); String tmp3 (operator+)tmp2,s4); s.operator=(tmp3); O ideal seria distinguir o comportamento dos objectos temporários dos objectos String comuns, criando uma classe auxiliar StrTmp, tal que, se tmp1 tivesse espaço para conter a concatenação global dos quatro objectos String, não seja necessário criar múltiplas representações para as concatenações intermédias. Ficaríamos com: StrTmp tmp (opearot+(s1,s2); tmp.operator+(s3); tmp.operator+(s4); s.operator=(String (tmp)); Ver alterações na pg. 74 --< tempo de execução é reduzido de 4 para 1. 2.2.4. Operadores de Índice para Leitura A classe String disponibiliza normalmente a sobrecarga do operador índice, que permite o acesso ao carácter indexado, para leitura ou para escrita. -Modo trivial na versão canónica: char &String::operator[] (inti) { assert(i<size()); return rep[i]; Permite que os objectos string tanto possam ficar do lado esquerdo como do direito de uma afectação, só que do lado esq. seria catastrófico (escrita) pois com o uso do idioma handle/body, há que desligar-se e alterar a representação. Assim requer-se que o operador indexação execute readAt() quando invoacdo do lado direito e writeAt()quando invocado do lado esquerdo. O método de Colpien a doptar diz que: A ideia básica consiste em definir uma classe auxiliar CRef (referência inteligente para carácter) com sobrecarga do operador afectação com carácter e um operador coerção para carácter. Em vez de se definir a sobrecarga do operador índice retornando uma referência para char, define-se essa sobrecarga retornando um objecto de uma classe auxiliar CRef que s e comporte como uma referência inteligente para o carácter a aceder. Conforme o operador índice seja evocado à esquerda ou à direita de um operador afectação, assim será posto em execução o operador afectação de CRef ou o operador coerção para char de Cref. String s(“trolaro”); char c; //s[i] é um objecto do tipo CRef //Leitura: operador coerção de CRef para char c=s[2]; //c=s.operator[](2).operator char() //Escrita: operador afectação com char de CRef s[4] = c; //s.operator[](4).operator=(c); Ver implementação na página 76 2.2.5. Classe String com Apontador Inteligente ... 2.4. Classe Fraction (pg. 116) #include <iostream.h> classe Fraction { int num; //Numerador (positivo ou negativo). int den; //Denominador (sempre positivo). //Simplificação da fracção com o MDC (num,den). void norm( ) public: Fraction (int a) : num(a), den (1) {} Fraction (const int &a) : num(a), den(1) {} Fraction (const int &a, const int &b) : num(a), den(b) {norm( );} const int &getNum( ) const {return num;} const int &getDen( ) const {return den;} Fraction &operator+=(const Fraction &a);

Fraction &operator-=(const Fraction &b); Fraction &operator*=(const Fraction &b); Fraction &operator/=(const Fraction &a); Fraction operator-( ) const {Fraction x =+this; x.num.neg( ); return x; } #define oper(op) \ Fraction operator op(const Fraction &b) const \ {return Fraction (*this)op##&b; } oper(+) oper(-) oper(*) oper(/) undef oper friend ostream &operator<<(ostream &out, const Fraction &f) {return out << f.num << ‘/’ << f.den; } }; void Fraction::norm () { if (num.isZero()) {den=1; return;} if (num.isOne() || den.isOne()) return; if (num == den) {num=den=1; eturn; } if (den < 0L) {num.neg(); den.neg(); } //algoritmo de Euclides int n=num, d=den, MDC; while (!d.isZero()) {MDC = n%d; n=d; d=MDC;} MDC=n; //Fim do algoritmo de euclides if (MDC.isOne()) return; num/= MDC; den/=MDC; } Fraction &Fraction::operator+=(const Fraction &b) { if (den==b.den) num+=b.num; else { num = num * b.den + den * b.num; den *= b.den; } norm ( ); return * this; } Fraction &Fraction::operator-=(const Fraction &b) { if (den==b.den) num -= b.num; else { num = num * b.den – den * b.num; den *= b.den; } norm(); return *this; } Fraction &Fraction::operator/=(const Fraction &b) { num *= b.den; den*=b.num; norm(); return *this; } Fraction &Fraction::operator*= (const Fraction &b) { num*=b.num; den*=b.den; norm(); return *this; }

CAP. 3 TEMPLATES E DERIVAÇÃO 3.2. Templates de Classes e de Funções Um template de fine uma família de classes ou de funções, que tomam como parâmetros type template parameters (parâmetros-tipo) e non type template parameters (parâmetros-valor). Das técnicas fundamentais ligadas a templates realçam-se as seguintes: - Mecanismos básicos para definir e usar template de classes; - Template de funções e dedução de argumentos; - Template de membros; - Parâmetros template que são templates de classes. 3.2.1. Templates de Classes Os de uso mais frequente são as classes contentoras. Designam-se por contentores os objectos destinados a conter outros objectos de qualquer tipo e que disponibilizam ao utilizador métodos cómodos de acesso (inserir, remover, pesquisar, etc.) aos objectos contidos. Ex: Stack: 3.2.1.1. Classe Stack class Stack { public: typedef char T; //O tipo de cada elemento static const unsigned DIM = 32; //Dimensão do stack private: T data[Dim]; //Contentor estático que suporta o stack unsigned size; //número de objectos contidos public: Stack() {size=0);} bool empty() const {return !size;} //Retorna a cópia do objecto situado no topo do stack. T &top() {return data[size-1];} //Insere objecto no topo do stack void push(const T &p) {assert(DIM!=size); data[size++]=p;} //Remove o objecto do topo do stack void pop() {assert(size>0); --size;} }; Esta definição é válida, seja qual for o tipo de objectos que se defina como T e seja qual for o valor inteiro da constante DIM. Mas tem de se alterar as primeiras linhas conforme o caso. É possível alargar esta definição de Stack tornando-a parametrizável no tipo T e no valor DIM. 3.2.1.2. Classe Parametrizável Stack O termo “template de classes” é sinónimo de “classe parametrizável” ou “classe genérica”. Um template de classes é uma especificação para criar uma família de classes aparentadas. ex: template <typename T, unsignedDIM> //em vez de typename também pode ser class class Stack { T data[DIM]; unsigned size; public: Stack () {size=0; } int empty () const {return !size; } T &top () {return data [size-1]; } void push (const T &p) {assert (DIM!=size); data[size++]=p; } void pop () {assert(size>0); --size; } }; Ex. de declaração de objectos: Stack<char,32> stk1; Stack<int,100> stk2;

O código dos métodos da generalidade das classes de contentores tem a particularidade interessante de ser independente do tipo dos objectos neles contidos (como o exemplo realça), o que propicia definir um template de classes tendo como parâmetros, entre outros, o tipo dos objectos a conter (parâmetro-tipo) e eventualmente, como no caso presente, a dimensão inicial do contentor (parâmetro-valor). Uma classe gerada (instanciada) a partir de um template de classes é chamada classe template. Um template de classes pode ser definido com um número qualquer de parâmetros-tipo e de parâmetros-valor. ex: template <typename T, class W, int XX, char YY> class Xpto; typedef Xpto<int, String, 3, ‘a’> MyXpto; MyXpto ob1, ob2, ob3; - Ex. definição de um novo nome para a classe Stack<char, 20> typedef Stack<char, 20> CharStack; Stack definido antes não tem natureza dinâmica. Geralmente tem sendo suportada por contentores sequenciais. 3.2.2. Templates de Funções Define uma família de funções. Ex: template<class T> void swap(T &a, T &b) {T aux=a; a=b; b=aux;} É importante notar desde já que enquanto na instanciação de um objecto de uma classe template o tipo do objecto deve sempre explicitar o valor dos seus argumentos template, no caso da chamada a uma função template prescinde-se geralmente dessa explicitação. Ex. para função anterior: int x=10, y=20; swap(x,y); //compilador infere, mas também podia swap<int>(x,y); 3.2.2.1. Membros Função de Templates de Classes Um membro função de um template de classes é implicitamente um template de funções membro. Toma como seus, os parâmetros do template de classes a que pertence. Ex: template <class T> class Array { T *v; //Apontador para a representação unsigned dim; //Capacidade da representação unsigned size; //Número de elementos contidos Array (const Array&); //Impossibilitar a construção por cópia void oerator=(const Array&) //Impossibilitar afectação public: explicit Array(unsigned d); //Iniciado com dim=d ~Array () {delete [] v; } T &operator[](unsigned n) {assert (n<dim); return v[n]; } const T &operator[] (unsigned n) const {assert(n<dim); return v[n]; } unsigned size() const {return sz; } void operator << (const T &e) //Inserir no fim do Array. {assert(size() < dim); v[sz++] = e;} }; //Declaração de dois templates de funções Template <class t> void sort(Array<T>&; Template <class T> ostream &operator<<(ostream&, const Array<T>&); Quando um mebro de um template de classes é definido fora da sua classe, deve ser explicitamente declarado como template. Ex: template <class T> Array<T>::Array(unsigned d) {v=new t [dim = d]; sz=0, } O template de funções globais define-se com sintaxe semelhante ao template de funções membro: ex. com bubble-sort

template <class T> void sort(Array<T> &a) {

for(unsigned i=a.size()-1; i<0; --i) for unsigned j=1; j<=i; ++j= if (a[j] < a[j-1] swap (a[j], a[j-1]); } 3.2.3. Templates com Parâmetros por Omissão tal como acontece com os parâmetros comuns das funções, os parâmetros dos templates de classes podem ser declarados com valor por omissão. Ex: template<class t, class A = allocator<T>> List {/*...+/} Isto significa que se podem instanciar classes template List, com um ou os dois argumentos template: List<int> list1 List>double, AllocPool<double>> list2 Quando declaramos todos os parâmetros por omissão. a lista de argumentos da classe template pode ser vazia. Ex: template<class T = char> class Stack {/*...+/}; Stack<> *p; //OK, apontador para Stack<char> Stack *q //Erro: Imprescindível <> Não é permitido usdar parâmetros por omissão em templates de funções. 3.2.4. Parâmetros Template que são Templates template<class T> class List {/* ... */ }; template<class K, class V, template<class> class C = List> class map { struct pair { C<K> key; C<V> value; }; //... }; Map trata-se de um template de tabelas associativas, cujos elementos Pair têm 2 membros (key e value). Os membros key e value são contentores do tipo C, de objectos tipo K e tipo V. Deste template podem ser instanciados objectos “tabelas asociativas” tão varaiadas como sejam: Map<int, double> //Chaves do tipo List<int> e dados do tipo List<double> Map<char, int, Stack> //Chaves do tipo Stack<char> e dados do tipo Stack<int> Para usar um parâmetro template que é template, é necessário especificar os parâmetros que ele próprio requer. Os parâmetros template que são template são sempre templates de classes. 3.2.5. Palavra-Chave typename É utilizada no corpo da declaração dos templates e tem por objectivo desfazer ambiguidades em expressões nas quais o compilador não tenha possibilidade de inferir se um dado nome de um membro de um argumento template, corresponde ou não a um tipo. typename especifica corresponde de facto a um tipo. A biblioteca standard define em todos os templates de contentores um conjunto de nomes de tipos standard. Por ex: template<class T, class A = allocator<T>> class vector { public: typedef T value_type; //Tipo de elemento typedef A allocator_type; /Tipo de gestor de memória typedef typename A::size_type size_type; typedef typename A::difference_type difference_type; typedef typename A::pointer iterator; //... };

3.2.6. Templates de Membros Podemos declarar um template de mebros dentro de uma classe ou de um template de classes. Ex: template<class T< class X { public: //Template de funções membro template<class T2> int compare(const T2&) {/*...*/} // Template de estruturas nested template<class T2> struct rebind {typedef X<T2> other; }; //... }; Também pode ser definido fora da classe: template<class T> template<class T2> int X<T>::compare(const T2 &s) {... se dentro tiver: template<class T2> int compare(const T2&); 3.2.7. Especialização de Templates Podemos querer que uma dada classe template tenha comportamento diferente quando tiver um tipo de argumento específico. Seja por ex. um template vector: template<class T> class vector {/*...*/} no qual pretendemos que a classe template «vector<bool> tenha um comportamento diferenciado para economia de espaço de representação. Então temos de, além de definir o template primário, explicitar também a definição especializada (sempre depois): template<> vector<bool> {/*...*/} No caso dos templates de funções... 3.2.8. Exemplos de Aplicação dos Templates Ao longo do livro 3.2.9. Técnicas (de programação muito interessantes) que Envolvem o Uso de Templates - Templates traits destinados à especificação de tipos - Execução de algoritmos em tempo de compilação 3.2.9.1. TRAITS É uma família de templates de classes, destinada a especializar a definição de tipos noutras classes ou funções template. Para tal o template que usa traits adopta os tipos nele definidos, tirando partido das suas especializações. Seja, por exemplo, o template UserTraits que define, nested e public, um nome de tipo: template<class T> struct UserTraits { typedef T UserType; }; Impondo especializações ao template UserTraits, podemos, para tipos específicos de argumentos com que sejam invocadas instâncias deste template, modificar a definição de UserType: template<> struct UsertTraits<int> 7 typedef float UserType; }; template<> struct UserTraits<char> { typedef int UserType; }; Seja o template de funções associado: template<class T> typename UserTarits<T>::UserType f(T t) {...} Quando em main() forem invocadas funções template f() com diversos tipos de argumentos, o retorno dessas funções será do tipo imposto pelo UserTraits e pelas suas especializações. ex: float x=f(10); int y=f(‘a’); double dd= f(4.27);

3.2.9.2. Algoritmos Executados em Tempo de Compilação ... 3.3. DERIVAÇÃO DE CLASSES E MÉTODOS VIRTUAIS O paradigma de programação Abstract Data Type (tipos de dados definidos pelo utilizador) esgota-se com a definição de classes de objectos isoladas entre si ou, quando muito, de classes que se associam com outras através de relações, tais como inclusão ou agregação. O paradigma da Programação Orientada por Objectos pressupões também que as classes têm relações de herança e usam polimorfismo, conceitos suportados pelos mecanismos de derivação e de métodos virtuais do C++ 3.3.1. Relação de Herança

Numa dada aplicação complexa o programador deve encontrar classes com propriedades comuns, de modo a que a definição de algumas delas possa ser feita a partir de outras mais genáricas. À herdeira chama-se derivada, e herda membros da base. A derivada acrescenta normalmente novos membros dados (atributos) ou membros função (métodos), pelo que é mais rica (em interface e atributos). Para além de acrescentar também pode alterar membros. A herança pode ser simples ou múltipla (de classes base directas). Quanto à acessibilidade que os membros herdados tomam numa classe derivada, depende do tipo de derivação que for adoptado: pública; protegida; privada. O mais comum é a pública, em que os membros especificados como públicos na classe na classe base mantêm-se públicos nas classes derivadas e os privados na base passam a ocultos, preservando assim o encapsulamento de membros. Os objectos da classe derivada incluem um objecto da classe base, mas o inverso já não é verdade. assim, existe conversão implícita da classe derivada para a classe base mas não o inverso. Ex. Classe B derivada de classe A //Classe base class A { int x; //atributo privado, oculto na classe B. public: void set10() {x=10;} void show() {cout << x;} }; class B: public A { //Classe derivada de A char c; //Atributo acrescentado public: //void setA10() {c=’A’; x=10;} //Erro. x oculto. void setA10() {c=’A’; set10();} //Método acrescentado void show() {cout << c; A::show();} //Método alterado }; void main () { A a; B b; a.set10(); cout << “Show A – “; a.show(); cout << endl; b.setA10(); cout << “Show B – “; b.show(); cout << endl; a = b; //Ok um B também é um A b = a; //erro: A não é um B. } 3.3.2. Arborescência de Classes Derivadas (Públicas)

A classe derivada é mais específica do que a sua classe base, ou seja, representa um subconjunto dos objectos que a sua classe base representa. atributo = atributo de estado método = atributo de comportamento Os métodos públicos de uma classe (próprios ou herdados) chamam-se interface da classe. Uma consequência importante que advém da derivação de classes consiste na reutilização de código da classe básica. Derivação Pública é uma relação Is A (É Um). 3.3.3. Header da Declaração de Classes Derivadas
Por forma a suportar a herança, a sintaxe de classe permite adicionar ao header da classe uma lista de derivação dessa classe, que especifica a classe (ou classes) base directa. Na declaração de uma classe derivada, a lista de derivação segue-se ao nome da classe, antecedida por 2 pontos. EX: class D : public B1, private B2 { 3.3.3.1. Construtores de Classes Derivadas Os construtores e os operadores de afectação das classes base não são herdados pela classe derivada. Após a lista de argumentos do construtor da classe derivada e antes do corpo do construtor, antecedida por 2 pontos e separados por vírgulas, segue-se a lista de iniciação, de que constam os construtores das classes base e os construtores dos objectos membros da classe derivada. Se isso não for feito pressupõe-se que sejam invocados os seus construtores sem parâmetros, caso existam. É importante notar que estes só existem por omissão, se não for definido nenhum construtor com parâmetros (excepção ao construtor por cópia), o construtor implicitamente invocado limita-se a reservar espaço de memória, sem iniciação de valores. Da lista de iniciação (das classes base e membros) pode também constar a iniciação dos membros dados tipo-básico usando para sua iniciação o formalismo da iniciação dos membros tipo-classe. ex: class Complex 7 int x, y; public: Complex(int xx=10, int yy=10) {x=xx; y=yy; } //ou Complex(intxx=10, int yy=10) : x(xx), y(yy) {} //... }; class B { Complex zz; int a; public: B(Complex v1, int v2) : zz(v1), a(v2) {} B() 7a=0} //zz é iniciado com x=y=10. //... }; class D : public B { int d; public: D(Complex v3, int v4, int v5) : B(v3, v4), d(v5) {} D() {d=5;} //zz é iniciado com x=y=10 e a com 0. //... }; As acções realizadas por um construtor de uma classe são as seguintes:

1º - Se for uma classe derivada, põe em execução o(s= construtor(es) da(s) classe(s) base directas, pela ordem indicada na lista de derivação 2º - Se a classe contém atributos, põe em execução os seus construtores pela ordem da sua declaração na classe 3º executa as instruções que constam do seu corpo. 3.3.4. Especificadores de Acesso a Membros Por forma a preservar o encapsulamento dos membros especificados como private, também não são acessíveis aos métodos das suas classe derivadas. Os especificados como protected mantêm-se inacessíveis às entidades externas mas são acessíveis directamente pelos métodos das classes derivadas. Ex: class B { int aa; //Zona apenas acessível pelos métodos de B protected: //Zona acessível aos derivados de B int bb; public: //Zona pública int cc; }; class D : public B { public: int dd; void f() { aa=0; //Erro. membro aa é privado da classe B. bb=1; //OK cc=2; //oK } }; void main () { B b; D d; b.aa=0; b.bb=1; b.cc=2; b.dd=3; d.aa=4; d.bb=5; d.cc=6 d.dd=7; }

//Erro. aa é privado de B (não pode ser acedido de main) //Erro. bb é protegido de B //OK. cc é público de B //Erro, dd não é membro de B. //Erro. aa não é acessível a D //Erro. bb mantém-se protegido em D //OK. cc mantém-se público em D //Ok. dd é público em D

3.3.5. Especificadores de Acesso às Classes Base A acessibilidade que um membro herdado toma na classe derivada depende conjuntamente do especificador de acesso à classe base e do tipo de acesso que esse membro possuía na sua classe. Derivação Pública: públicos --> públicos; protegidos --> protegidos; privados e ocultos --> ocultos Derivação Protegida: públicos e protegidos --> protegidos; privados e ocultos --> ocultos Derivação Privada: públicos e protegidos --> privados; privados e ocultos --> ocultos O membro oculto, no entanto, existe na classe e poderá ser acedido indirectamente por métodos públicos ou protegidos herdados da classe base. Se for omitido, a derivação, por omissão é privada. Ex: Acesso a membros da classe base class B { int n; public:

B(in nn) {n=nn; } int getn() {return n; } void display() {cout << “n= “ << n << endl; } }; class D : public B { char c; public: D(int nn, in cc) : B(nn), c(cc) {} //int get1() {return n+c; } //Erro. n inacessível em D int getn() {return B::getn() +c; } //OK. invoca getn() de B //Oculta (override) B::display(). void display() {cout <<”n= “ << getn() <<, c= “ << c << endl; } }; void f() { B b(7); D d(5,4); cout << b.n; //Erro. n é privado a B cout << d.B::getn(); //OK. n acedido por B através do seu método getn() cout << d.getn(); //OK b.display(); //OK d.display(); //OK B *ptB=&d; //Coerção implícita de apontadores ptB --> display(); //Invoca display() de B } Os membros públicos herdados da classe base podem ser acedidos por qualquer membro função não estático da classe derivada e por qualquer função não membro, amiga ou não amiga. Os membros protegidos herdados podem ser acedidos directamente pelas funções membro da classe derivada e pelas funções declaradas amigas dessa classe. A derivação pública promove simultaneamente derivação de interface e de implementação. raramente se justifica a derivação privada ou protegida, pelo que tudo o que segue se refere à derivação pública. 3.3.6. Sobrecarga e Redefinição de Métodos Em classe derivadas, a sobrecarga de métodos segue as mesmas regras estabelecidas para a sobrecarga de funções globais, desde que se tome em consideração que o alcance (scope) da declaração de um método herdado é o de toda a classe derivada e o alcance de validade da declaração dos métodos exclusivos da classe derivada não abrange a classe base. A declaração de um método exclusivo da classe derivada oculta na classe derivada a declaração do método herdado da classe base. Ex: class B { public: void h(int); void w(); }; class D : public B { public: void h(char*); void w(); //Executando acções diferentes de B::w(). }; void f(D &d) { d.h(7); //Erro: D::(char*) oculta B::h(int). d.w(); //Ok. invoca D::w(). d.B::h(7); //Ok. Invoca B::h(int) d.B::w(); //Ok. Invoca B::w() d.h(“Hellow”); //Ok. Invoca D::h(char*). }

3.3.7. Derivação Múltipla A hereditariedade múltipla é interessante no que se refere a reutilização de código, mas pode suscitar alguns problemas de ambiguidade: - Ambiguidade de membros herdados --> Tem de se explicitar o operador resolução de alcance - Ambiguidade por duplicação de herança – Problema do diamante --> encaminhar... ex: x= dd.B1::i A palavra chave virtual aplicada às classes (base) permite evitar duplicação de cópias quando mais que uma classe deriva da base. 3.3.8. Implementação handle/body da String com Derivação ... 3.3.9. Métodos Virtuais e Polimorfismo Uma acção diz-se polimorfa se for executada de diferentes formas dependendo do contexto em que for invocada. Esse contexto pode ser determinado em tempo de compilação ou de execução. O C++ utiliza vários tipos de polimorfismo, nomeadamente overload de funções e de operadores, template de classe e funções (resolvido em tempo de compilação), e métodos definidos como virtuais numa arborescência de derivação (resolvido em tempo de execução). Em C++, o polimorfismo traduz-se na possibilidade de redefinir nas classes derivadas os métodos declarados na classe base como virtuais (com o mesmo nome, número e tipo de parâmetros), por forma a que, quando invocado um método virtual através de um apontador ou uma referência para a classe base, a versão do método posto em execução seja o da classe do objecto apontado ou referenciado e não o da classe correspondente ao tipo da declaração do apontador ou da referência como acontece nos métodos comuns. Ex: class B { public: virtual char *f() {return “B:.f()”; } //Polimorfa char *g() {return “B::g(); 0 //Normal }; class D : public B { public: char *f() 7return “D::f()” ; } //Polimorfa char *g() {return “D::g()” ; } //Normal }; void main () { D obj; B *pt = &obj; //pt é do tipo apontador para B // mas aponta para um objecto D cout << pt-->f() << endl; //Escreve: “D::f()”. cout << pt-->g() << endl; //Escreve “B..g()”. } Na redefinição (overrride) de um método declarado como virtual, é preeciso manter a assinatura do método. Se a sssinatura for diferente é interpretado como overload. 3.3.10.2. Tags ...

CAP. 4 – CONTENTORE SEQUENCIAIS 4.1. Introdução A norma ANSI/ISO da linguagem C++ e da respectiva biblioteca de templates STL, define os conceitos: Contentor – para conter objectos Iterador – para estabelecer ligação entre algoritmos genéricos e contentores Allocator – para isolar os contentores do detalhe dos modelos de alojamento de memória utilizados e para separar as acções de alojamento das de iniciação de objectos. Resumindo: Os algoritmos genéricos usam contentores por intermédio dos iteradores e usam objectos função na especialização dos algoritmos genéricos. Os contentores usam os allocators para alojar e desalojar memória e para construir e destruir objectos. As normas ANSI/ISO não impõem implementação específica mas sim os métodos públicos a disponibilizar e a respectiva complexidade em tempo de execução. Conforme os métodos disponíveis na sua interface, os contentores do STL têm diferentes nomes: vector, list, deque, set, map (tabela), stack, queue, priority_queue. Os tipos de contentores dividem-se em: - Contentores Sequenciais - Adaptadores de Contentores Sequenciais - Contentores Associativos 4.2. Generalidades Os contentores sequenciais suportam-se em estruturas de dados lineares, isto é, aquelas em que todos os elementos contidos têm um único elemento sucessor e um único antecessor. permitem acesso sequencial aos elementos e disponibilizam operadores para inserções e remoções de objectos. 4.2.1. Tipos de Contentores Sequenciais T a[n] Estrutura array, predefinida na linguagem. Providencia acesso aleatório a sequências de dimensão fixa n. Os iteradores são apontadores comuns para objectos tipo T. Acesso em tempo constante a qualquer objecto, usando indexação ou aritmética de apontadores. Inserções e remoções a meio e no início requerem tempo linear. No fim requerem tempo constante. vector – É uma generalização do array que, de modo transparente ao utilizador, aumenta a dimensão sempre que se torne necessário. list – é suportada numa estrutura em lista duplamente ligada. Permite visita sequencial (em ambos os sentidos) a todos os objectos. Inserção e remoção em tempo constante. deque – Semelhante ao vector, excepto que permite inserções e remoções rápidas em ambos os extremos da sequência. ring buffer – vector em anel – variante de fila de espera com capacidade constante preestabelecida FIFO. Objectos inseridos no fim da fila e removidos do início, com tempo constante. 4.2.2. Complexidade em Notação Big-Oh Num algoritmo que toma como domínio um conjunto de n elementos, a notação Big-Oh exprime o tipo de proporcionalidade existente entre n e o tempo t(n) que demora a sua execução. Por ex. a pesquisa de um elemento num array v não ordenado de dimensão n, requer no máximo n comparações entre a chave de pesquisa e os conteúdos v[i] do array. Demorando k cada passo, virá: t(n) <= kn ----> tempo de pesquisa linear com n ou t(n) é O(n) na notação Big-Oh. No caso de usarmos pesquisa dicotómica sobre um array ordenado de n elementos, o tempo de pesquisa seria: t(n) <= k log2n ou, em notação Big-Oh: t(n) é O(log n). O que esta notação pretende realçar é o tipo de proporcionalidade que existe entre o número de elementos envolvidos num algoritmo e o seu tempo de execução. Na biblioteca STL distinguem-se cinco tipos d complexidade: t(n) é O (n2) - tempo quadrático

t(n) é O (n log n)) - tempo n log n
t(n) é O (n) - tempo linear t(n) é O (log n) - tempo logarítmico t(n) é O (1) - tempo constante A diferença de tempos concretos pode ser abissal. Nalguns casos é melhor usar o tempo amortizado. Por ex. no push_back de um array, que insere no fim do array, mas quando não há mais espaço dobra dinamicamente este, de forma automática. Aloja espaço para 2n elementos, muda para lá os n anteriores e devolve espaço anterior. Alojamento e cópia são O(n) e as inserções são O(1). Amortizando, podemos dizer que é O(1)+ - algoritmo de tempo constante amortizado. 4.2.3. Iteradores Associados a Contentores Um objecto iterador, definido para um determinado contentor, comporta-se como um apontador inteligente para apontar para os objectos nele contidos, dispondo para tal de operadores que lhe permitem percorrer o conjunto desses objectos. Ex. operador incremento ++ ; e a sobrecarga do operador desreferência * sobre um iterador retorna o objecto por ele apontado. No caso de contentores sequenciais (objectos ocupam bloco compacto e contíguo de memória), os iteradores por excelência são os próprios apontadores para esses objectos (T*). No caso de contentores sequenciais como a lista duplamente ligada, em que os objectos já não ocupam bloco compacto de memória, há que definir uma classe específica que seja iterador e sobrecarregar os operadores ++, -- e * iterator &iterator::operator++(); //Avança o iterador iterator &iterator::operator—(); //Recua o iterador T &iterator::operator*(); //Retorna a data iterada 4.2.3.1. Domínio de Iteradores em Contentores Sequenciais Os conceitos de domínio (range) e de iterador (iterator) são fundamentais em tudo o que vamos referir acerca dos contentores. Os algoritmos ganham acesso a uma dada sequência de objectos, recebendo como argumentos um par de iteradores, first e last, do contentor onde esses objectos se situem. Designa-se por domínio de iteradores num contentor a sequência de iteradores que apontam para uma sequência de objectos contidos. Denota-se por [first, last[ . last aponta para a primeira posição a seguir ao último objecto do domínio. Um domínio é válido se last for atingível a partir de first, por incrementos sucessivos. O apontador last não pode ser desreferenciado mas pode entrar na aritmética de apontadores. ex: const int MAX = 10; int v[MAX]; //Iterador sobre o array typedef int *iterator; //Iterador para o início do domínio iterator begin() {return &v[0]; } //Iterador para o past the end iterator end() {return &v[MAX]; } //Afectar os elementos do domínio com o próprio índice void assign(iterator first, iterator last) 7 for (int i=0; first != last; ++first, ++i) *first=i; } //Mostrar o valor dos elementos do domínio void write(iterator first, iterator last) { for(; first != last; ++first) cout << *first << ‘ ‘, 0 void main () { assign (begin(), end()); write (begin(), end()); 0 4.2.3.2. Categorias de Iteradores O standard C++ exige que todos os operadores que constem da interface de uma classe iterator tenham complexidade O(1) ou constante amortizada O(1)+ e, assim, definem-se várias categorias

de iteradores, conforme o conjunto de operações que disponibilizam satisfazendo esta exigência. Depende do contentor para o qual foi definido. As categorias são: Output, Input; Forward; Bidirectional; Random Access. Posso definir as funções do exemplo anterior como template usando a categoria adequada (mínima) mas que funcioan também se lhe forem passados parâmetros de categorias superiores. 4.2.3.3. Selecção do Algoritmo Apropriado ao Iterador ex: template<class ItIn> unsigned distance (ItIn first, ItIn last) { unsigned res=0; while(first!=last) {++first; ++res; } return res; } Funciona para qualquer iterador da categoria input, logo funcioan para qualquer outro excepto output. Mas tem mau desempenho O(n). Se lhe forem passados iteradores de acesso aleatório fica com O(1) pois estes permitem p-q em O(1). O ideal seria ter apenas um template de funções distance() que seleccionasse o algoritmo mais eficiente, conforme a categoria dos iteradores que lhe fossem passados como argumentos. Para o algoritmo genérico inferir a categoria do iterador, basta que na definição do iterador conste um membro tipo que possa ser testado, o que é complicado dado que o que tem de ser testado é se aa categoria do iterador é igual ou maior à que lhe é necessária. A técnica de utilização de tags descrita no cap. anterior assume aqui a tarefa para que está vocacionada. struct output_iterator_tag {}; ... struct random_access_iterator_tag {}; Basta agora que qualquer classe de iterador tenha uma definição do tipo iterator_category, baseada nestes tipos de classes tag. Ex. classe iterator de list deve ter a seguinte declaração: class iterator { //Iterador de list é bidireccional. public: typedef bidirectional_iterator_tag iterator_category; //... }; Tem pois agora coerção implícita para input por exemplo quando um algoritmo necessite de um iterador da categoria input. Há ainda que usar Traits para o caso de querermos usar o tipo pointer predefinido na linguagem como categoria de iterador random acess. ver pg. 192-194 para aprofundamento. 4.2.4. Gestão de Memória nos Contentores 4.2.4.1. Gestão de Memória na Forma Trivial Conhecemos 3 modos de alojamento de objectos em memória: - Estático: quando os objectos (estáticos) são alojados na memória de dados globais, durante o carregamento do programa (load time); - Automático: quando os objectos (automáticos e temporários) são alojados em run time no stack: - Dinâmico: quando os objectos (dinâmicos) são alojados e desalojados na memória livre (heap). ex. em T a(10) é reservada memória estática ou automática. Quando o programa sai do scope da declaração, é invocado implicitamente o destrutor de T e a memória é libertada. Não declarativa como x=T(10) --> automática ... 4.2.4.2. Operadores new e delete básicos Quando invocado abreviadamente: T *p = new T(10) o operador new reserva espaço no heap para um objecto T e é posto implicitamente em execução o construtor. delete p posterior é invocado implicitamente o destrutor. Quer o operador new, quer o delete cujas assinaturas são: void *operator new(size_t n); operator delete (void *p); podem ser também invocados explicitamente no programa: T p * = ( T *) :: operator new(sizeof(T));

..operator delete(p);

Este tipo de invocação promove comportamentos diferentes da forma abreviada, pois limita-se a reservar espaço no heap sem pôr em execução o construtor. 4.2.4.3. Gestão da Memória nos Contentores Standard A STL é diferente. Adopta como critério separar as acções de reserva de espaço, das acções de iniciação de objectos (2 métodos diferentes). Da mesma a devolução do espaço fica dissociada da destruição dos objectos. A vantagem é a eficiência, como se mostra mais à frente para o template de classes vector. Este novo critério implica a sobrecarga do operador new, usando mais um parâmetro adicional (apontador para void) para permitir construir um objecto num endereço explícito da memória, previamente reservado --> técnica do placement syntax. void *operator new (size_t, void *p) {return p; } e limita-se a retornar o próprio apontador que lhe foi passado sem reservar nenhuma reserva de espaço. Assim, new(p) T(10) só contrói, no espaço anteriormente reservado pela invocação explícita do new básico. ... 4.3 Template de Classes Allocator Todos os contentores genéricos da biblioteca tomam como segundo parâmetro uma classe template allocator<T> que determina o modo como a memória é gerida. Esse template tem uma interface satndard de tipos e métodos que todos os componentes da biblioteca tomam como auxiliares, quer para reservar e devolver memória não iniciada, quer para construir e destruir objectos. Um allocator providencia métodos para alojar e desalojar memória, e métodos públicos para construir e destruir objectos. Isola assim contentores e algoritmos da gestão de memória, para além de definir nomes de tipos e métodos com que nos devemos pouco a pouco familiarizar. Veremos a seguir como se faz, na classe vector. 4.3.1. Requisitos de um Allocator ... 4.4. Template de Classes Container ...

4.5. Template de Classes vector Principais características: - Permitir acesso aleatório em tempo constante O(1) aos objectos contidos, por indexação ou aritmética de apontadores; - Permitir inserções e remoções em tempo constante amortizado O(1)+ no fim da sequência; - Permitir inserções e remoções em no início ou no meio da sequência em tempo linear O(n) - Ampliar automaticamente a capacidade da representação em memória, por realojamento. No caso duplicar ou criar unidade se zero. A definição de um template de contentor envolve sempre a definição de um iterador que lhe seja específico. No caso um simples apontador. Um vector dispõe de 3 atributos do tipo iterator: start, finish e endOfStorage.

4.5.1. Definição do Template de Classes vector
#define SUPER Container<T,A> template<class T, class A = allocator<T>> class vector: public SUPER { public: IMPORT_TYPE(pointer); IMPORT_TYPE(const_pointer); IMPORT_TYPE(reference); IMPORT_TYPE(const_reference); IMPORT_TYPE(size_type); //Tipos não herdados de Container<T,A> typedef pointer iterator; typedef const_pointer const_iterator; //Construtores e destrutor explicit vector(const A & = A()); //Default – vazio //Iniciado com n objectos tipo T explicit vector(size_type, const T & =T(), const A & =A); vector (const vector&); //Por cópia ~vector(); //Invoca destrutor de A //Sobrecarga do operador afectação vector &operator=(const vector&); //Métodos de acesso //Acesso às dimensões size_type capacity() const {return endOfStorage-begin(); } size_type size() const 7return end() – begin(); } size_type max-size() const {return allocator.max_size(); } bool empty() const {return size() = 0; } const A &get_allocator() const {return allocator;} //Acesso por iterador iterator begin() {return start; } iterator end() {return finish; } const_iterator begin() const {return start; } const_iterator end() const {return finish; } //Acesso ao elemento reference front() {return (*begin()); 0 reference back() {return (*end()-1)); } reference operator[] (size_type i) {return (*begin() + i)); } //Métodos de inserção e remoção de elementos iterator insert(iterator p, const T &x); iterator (erase (iterator p); void push_back (const T &x) {insert(end(), x); } void pop_baxck () {erase(end() – 1); } //Outros métodos void reserve(size_type n);

void clear(); void swap(vector &x); private:

//Atributos
A allocator; //allocator<T> por omissão iterator start, finish, endOfStorage; //Métodos auxiliares void destroy(iterator start, itrator finish); iterator uninitializedCopy(const_iterator first, cons_iterator last, iterator p); iterator uninitializedFill(iteartor p, size_type n, const T &x); }; //Fim da definição do template de classes vector #undef SUPER 4.5.1.1. Construtores Todos os construtores constroem por cópia o atributo allocator. O construtor sem parâmetros constrói um vector vazio, colocando os atributos start, finish e endOfStorage a NULL, e não providencia reserva de espaço. template<class T, class A> vector<T,a>::vector(const a &a1) : allocator(a1), start(), finish(), endOfStorage() {} template<class T, class A> vector<T,A>::vector(size_type n, const T &x, const AS &a1) : allocatro(a1) { start=allocator.allocate(n); //Pedir memória //Construir n elementos por cópia endOfStorage = finish = uninitializedFill(star, n, x); } O construtor é declarado explicit para que não promova a coerção automática do tipo size_type para tipo vector template<class T, class A> vector<T,A>::iterator vector<A,T>::uninitializedFill(iterator dest, size_type n, const T &x) { for(; n; --n, ++dest) allocator.construct(dest, x); return dest; } template<class T, class A> vector<T,A>::vector(const vector &x) : allocator(x.allocator) { start = allocator.allocate(x.size()); finish = uninitializedCopy(x.begin(), x.end(), start); endOfStorage = finish; } template<Class T, class A> vector<T,A>::iterator vectro<T,A>::uninitializedCopy(iterator first, iterator last, iterator dest) { for(;first!=last; ++dest, ++first) allocator.construct(dest, *first); return dest; } 4.5.1.2. Destrutor O método destrutor destrói todos os objectos contidos no vector e liberta memória reservada, invocando o método deallocate() sobre o objecto allocatro. template<class T, class A> vector<T,A>::~vector() { destroy(begin(), end()); allocator.deallocate(begin(), capacity()); } O método auxiliar destroy() destrói os objectos do domínio [first, last[, invocando o método homónimo de allocator (pg.213)

4.5.1.3. Método reserve() O método reserve(size_type n) garante que, após ser invocado, a capacidade do vector é maior ou igual a n. (pg.213) 4.5.1.4. Operador Afectação Requer cuidados especiais, devido ao facto de termos separado a reserva da iniciação. - Caso haja necessidade de expansão do espaço de memória, destrói os objectos contidos, liberta a memória reservada, reserva um novo bloco de memória e constrói, nesse bloco, objectos por cópia dos contidos no vector a copiar. - No caso contrário destrói os restantes elementos do vector a afectar. (pg.214) 4.5.1.5. Método insert() Também requer cuidados: template<class T, class A> vector<T,A>::iterator vector<T,A>::insert( iterator p, const T &x) { if ( endOfStorage == end() ) { //Não existe espaço //Dimensão do antigo e do novo espaço size_type sz=size(), n=sz + (1 < sz ? sz : 1); //Pedir novo espaço iterator aux = allocator.allocate(n); //Construir por cópia no novo espaço elementos até p iterator newp=uninitializedCopy(begin(), p, aux); //Construir o elemento a inserir por cópia allocator.construct(newp, x); //Construir por cópia no novo espaço os elementos depois de p. uninitializedCopy(p, end(), newp+1); //Destruir os elementos do espaço anterior destroy(begin(), end()); //Libertar o espaço anterior allocator.deallocate(begin(), capacity()); endOfStorage = aux + n; finish = aux + sz + 1; start = aux; return newp; //fica a apontar para o inserido. } if (p==end()) //Inserir no fim //Construir o elemento a inserir por cópia allocator.contruct(end(), x); else { //Inserir no meio //Deslocar o último, construindo por cópia copy_backward(p, end()-1, end()); *p=x; //Copiar o elemento a inserir } ++finish; return p; } template<class ItBi1, class ItBi2> ItBi2 copy_backward(ItBi1 first, ItBi1 last, ItBi2 res) { while (first!=last) *--res = *--last; return res; } 4.5.1.6. Método Erase() Desloca o domínio [p+1, end()[ para o domínio [p,end()-1[ e destrói o último elemento do vector. template<class T, class A> vector<T,A>::iterator vector<T,A>::erase(iterator p) {

finish = copy (p+1, end(), p); //Desloca elementos (esmaga o que é para apagar). Retorna iterador para o elemento past the end copiado. allocator.destroy(finish); //Destruir o excedente return p; } 4.5.1.7. Método clear() template<class T, class A> void vector<T,A>::clear() {destroy(begin(), end()); finish = start; } 4.5.1.8. Método swap() template<class T, class A> void vector<T,A>::swap(vector &vx) { if (this != &X) if (allocator == x. allocator) { ::swap(start, x.start); ::swap(finish, x.finish); ::swap(endOfStorage, x.endOfStorage); } else ::swap(*this, x); template <class U> inline void swap(U &a, U &b) {U aux(a); a=b; b=aux; }

4.6. TEMPLATE DE CLASSES LIST É uma estrutura linear de elementos (Nodes), encadeados entre si através de apontadores. A biblioteca standard tem listas duplamente ligadas com sentinela, o que permite iterações sequenciais nos dois sentidos. O nó sentinela simplifica os algoritmos tornando-os mais eficientes. A interface é muito parecida com o template de vector, diferindo no que toca ao desempenho dos métodos. As diferenças fundamentais são: - Na list a memória para um novo Node é automaticamente reservada quando se insere um objecto e é automaticamente devolvida ao gestor de memória (allocator) sempre que se remove um objecto. - a list tem desempenho em tempo constante para remoções e inserções em qualquer ponto da sequência para o qual se disponha previamente um iterador.

4.6.1. Definição do template de classes list
#define SUPER Container <T,A> template<class T, class A=allocator<T>> class list: public SUPER { public: IMPORT_TYPE(size_type); IMPORT_TYPE(difference_type); IMPORT_TYPE(reference); IMPORT_TYPE(const_reference); IMPORT_TYPE(pointer); IMPORT_TYPE(const_pointer); private: struct Node; //Define ANode como tipo allocator de Node, da mesma família template da qual A é o tipo allocator para T. typedef typename A::rebind<Node>::other ANode; typedef typename Anode::pointer NodePtr; typedef typename Anode::const_pointer ConstNodePtr; struct Node { NodePtr prev; NodePtr next; T data; }; public: //Definição dos iteradores class const_iterator {...}; //Definidos adiante class iterator {...}; //Os atributos da lista serão os seguintes: private: A constr; //Allocator para construir objectos T ANode alloc; //Allocator para alojar e desalojar nodes NodePtr head; //Apontador para o nó sentinela (dummy) size_type sz; //número de objectos contidos //Métodos auxiliares //Aloja o nó sentinela e inicia os membros e apontadores a apontarem para ele próprio NodePtr newNode(); //Aloja e inicia um node com e, e promove o encadeamento dos apontadores posicionando-o atrás de suc NodePtr newNode(NodePtr suc, const T &e); //Desliga da lista o Node apontado por p e destrói-o void deleteNode(NodePtr p); //Move um bloco entre duas listas se allocators iguais void moveInAlloc (iterator p, iterator f, iterator l); //Move um bloco entre 2 listas void move(iterator p, list&, iterator f, iterator l); Interface Pública public:

//Construtores e destrutor explicit list(const A &a1=A()); //Lista iniciada com n objectos cópia de k explicit list(size_type n, const T &k=T, const A &a=A()); list(const list &lst); ~list() {clear(); alloc.deallocate(head,1); } //Afectações //Afectação com os objectos do domínio [first, last[ template<class InIt> void assign(InIt first, InIt last); //Afectação com os objectos doutra lista do mesmo tipo list &operator=(const list &lst); Obtenção de iteradores iterator begin() {return head-->next; } iterator end() {return head;} //Dimensões size_type size() const {return sz;} bool empty() {return sz==0;} const A &get_allocator() const {return constr;} //Acesso aos elementos reference front() {return *begin(); } reference back() {return *(--end()); } //Inserções e remoções //Insere na posição anterior a p, um nó por cópia de e iterator insert( iterator p, const T &e = T()); } //Insere na posição anterior a p, os objectos situados no domínio [first, last[ template<class InputIter> void insert(iterator p, InputIter first, InputIter last); void push_front(const T &e) {insert (begin(), e); } void push_back(const T &e) {insert(end(), e); } //Remove da lista o nó apontado por p iterator erase(iterator p); //Remove os nós contidos no domínio [first, last[ iterator erase(iterator first, iterator last); void pop_front() {erase(begin()); } void pop_back() {erase(--end());} //Trocar o conteúdo entre duas listas void swap(list lst); //Interface especializada da lista (só existem na list) //Mover os elementos do domínio [f,l[ ou first da lista lst, para a posição anterior a p, na lista *this; //mover implica inserir numa lista e remover da outra void splice(iterator p, list &lst, iterator f, iterator l); void splice(iterator p, list &lst, iterator first); //Pressupõe listas ordenadas. Insere ordenadamente todos os elementos de lst, deixando-a vazia void merge(list &lst); void sort(); //Ordena os elementos da lista }; fim da definição do template de classes list #undef SUPER 4.6.1.1. Iteradores Dado que os nós da lista ocupam posições dispersas na memória, passar para o nó seguinte não se traduz em incrementar simplesmente o apontador. Torna-se necessário definir uma classe iterator, específica para iterar em list. ver como em pg.222 e seguintes. Faz sobrecarga dos opeardores que permitam classificá-lo como bidireccional. Assim, além dos operadores de desreferência e afectação deve dispor também dos operadores de incremento e decremento (prefixos e sufixos). Não dispõe dos operadores += e -= pois não são de tempo constante. O construtor com um apontador para nó constrói um iterador para esse nó.

A classe iterator para lista é nested, interna, à lista. Tem um único atributo: NodePtr ptr; Tem um método NodePtr current() const {return ptr;} 4.6.1.2. Métodos auxiliares newNode() A versão newNode() sem parâmetros é auxiliar dos construtores de listas tomando para si a tarefa de alojar e iniciar o nó dummy, colocando os apontadores next e prev a apontar para o próprio Node. O membro data não precisa ser iniciado. template<class T, class A> list<T,A>::NodePtr list<T,A>::newNode() { NodePtr n = alloc.allocate(1); //Aloja o nó dummy e return n->next = n->prev = n; //inicia os apontadores. //Construtor de lista vazia template<class T, class A> inline list<T.A>::list(const A &a1) : constr(a1), alloc(a1), head(newNode()), sz(0) {} A versão newNode() com 2 parâmetros é auxiliar de todos os métodos que necessitam criar e inserir na lista objectos Node. Ela reserva espaço par um nó, constrói no campo data um objecto T por cópia do objecto cuja referência lhe seja passada como segundo parâmetro e, finalmente, promove o seu encadeamento na posição anterior à do nó cujo apontador lhe seja passado como primeiro parâmetro. template<class T, class A> list<T,a>::NodePtr list<T,A>newNode(NodePtr suc, const T &e) { NodePtr n = alloc.allocate(1); constr.construct(&(n->data), e); NodePtr prev = suc ->prev; n->next = suc; n->prev = prev; prev->next = suc->prev = n; return n; } 4.6.1.3. Métodos de Inserção Resulta extremamente simples, suportada no método newNode() com 2 parâmetros, que vimos antes. Insere na posição anterior a p. template<class T, class A> list<T,A>::iterator list<T,A>::insert(iterator p, const T &e) { ++sz; return newNode(p.current(), e); } Insere na posição anterior a p, os objectos situados no domínio [first, last[ de outro contentor qualquer template<classT, class A> template<class InIter> void list<T,A>::insert(iterator p, InIter first, InIter last) { for(; first != last; ++first) insert(p, *first); } 4.6.1.4. Construtores com n objectos e por cópia 4.6.1.5. Método deleteNode() Desliga da lista o nó a remover, afectando adequadamente o membro next do nó antecessor e o membro prev do nó sucessor. Seguidamente invoca o método destroy sobre constr para destruir os dados sem libertar a memória e o método deallocate sobre alloc para desalojar o nó (devolver o espaço de memória) template<class T, class A> void list<T,A>::deleteNode(NodePtr n) { n->prev->next = n->next; n->next->prev = n->prev; constr.destroy(&(n->data)); alloc.deallocate(n,1); }

4.6.1.6. Métodos de Remoção template<class T, class A> list<T,A>::iterator list<T,A>::erase(iterator p) { deleteNode(p++).current()); --sz; return p; } 4.6.1.7. Operador afectação e método assign() 4.6.1.8. Método swap() 4.6.1.9. Método splice() 4.6.1.10. Método merge() 4.6.1.11. Método sort()

4.7. Template de Classes deque Pode ser descrito como uma fila de espera com inserção e remoção rápida em ambos os extremos. Tal como a lista, é vocacionado para cumprir a disciplina FIFO ou FILO mas permite acesso aleatório como o vector. Os contentores deque constituem uma versão mais versátil da estrutura de dados queue, muito utilizada em programação e que se suporta num deque restringido a FIFO; é um adaptador. Comparando deque com list e vector: - tal como o vector e contrariamente à list, o deque permite acesso aleatório a todos os objectos contidos (embora em tempo constante são mais lentas que no vector). - Contrariamente ao vector, permite realizar, em tempo constante, acções de inserção e remoção em ambos os extremos- também contrariamente ao vector, permite que a memória reservada diminua de dimensão quando predominam as acções de remoção. - Enquanto a list permite inserções e remoções em tempo constante no meio do seu domínio, o deque, tal como o vector, só permite executar essas acções em tempo linear. 4.7.1. Estrutura de Dados de Suporte Este contentor suporta-se em blocos de memória (DataBlock) de dimensão fixa. Em tempo de execução, o espaço reservado para o deque pode ser ampliado em ambos os sentidos, associando-lhe novos DataBlock. Os apontadores para os DataBlock situam-se, centrados e ordenados, num outro bloco de memória de comprimento variável que denominamos MapBlock. O template de classes deque tem como atributos dois iteradores: start e finish - cada iterador tem 4 apontadores -> há uma figura muito elucidativa na pg. 243 se me esquecer. Tem ainda como atributos map, que aponta para o MapBlock e mapSize que memoriza a dimensão actual do MapBlock. Adopta-se 5 como o nº mínimo para a dimensão do MapBlock. A dimensão dos dataBlock situa-se normalmente na ordem dos 100 elementos. 4.7.2. Exemplos da Construção de um Deque queremos construir um deque<int> iniciado com 8 objectos tipo int, cópias do inteiro 0 e no qual, para facilitar, restringimos a 5 a dimensão dos DataBlock (DIM_BLOCK = 5) O critério para organizar a estrutura de dados é a seguinte: - Infere-se quantos DataBlock devem ser criados para iniciar o deque com 8 objectos; - Reserva-se espaço para um MapBlock, satisfazendoo mínimo de 5 (DIM_MAP_MIN=5) e de poder armazenar um nº de apontadores duplo do nº de DataBlock a criar. - Reserva-se o nº de DataBlock inferidos e situam-se, centrados no MapBlock, os respectivos apontadores. Os critérios de inferência acima citados são: - O primeiro objecto a inserir num deque deve situar-se a meio do primeiro DataBlock, para push_back() e push_front() posteriores. - Quando o nº de objectos a inserir sequencialmente no DataBlock corrente vierem a ocupar a última posição disponível, deve-se criar mais um DataBlock (o que proporciona maior eficiência nos métodos, por simplificação dos algoritmos). - Sempre que se cria um MapBlock para n apontadores para DataBlock, reserva-se sempre um espaço duplo do necessário (2*n) e inserem-se os n apontadores centrados nesse espaço, prevendo futuras inserções, tanto em posições anteriores como posteriores às ocupadas. Segundo os critérios definidos: numOPos = numOfObj + DIM_BLOCK/2 + 1; // numOfPos = 8+5/2+1 =11 numOfBlocks = numOfPos/DIM_BLOCK + (numOfPos%DIM_BLOCK !=0); //numOfBlocks=11/5 + 1=3 mapSize = (numOfBlocks < (DIM_MAP/2))? DIM_MAP:numOfBlocks*2; mapSize = (3 < 5/2)? 5 : 6 = 6

Definição do Template de Classes Deque
#define SUPER Container<T,A> template <class T, class A = allocator<T>> class deque : public SUPER { public: IMPORT_TYPE(pointer); IMPORT_TYPE(const_pointer; IMPORT_TYPE(reference); IMPORT_TYPE(const_reference); IMPORT_TYPE(size_type); IMPORT_TYPE(difference_type); private: //<<Constantes necessários>> static const size_type DIM_MAP_MIN = 5; static const size_type DIM_BLOCK=512/sizeof(T); //<<Tipos necessários a deque e ao iterador>> typedef typename A::rebind<pointer>::other AMap; typedef typename AMap::pointer MapPointer; template <class MapPtr, class PTR, class REF> class IT {...}; public: //<<Tipos de Iteradores>> typedef IT<MapPointer, pointer, reference> iteartor; //<<Atributos>> private: A blockAlloc; AMap mapAlloc; MapPointer map; size_type mapSize; iterator start, finish; //<<Interface Pública>> public: //<<Construtores, destrutor e operador afectação>> //Constrói um deque vazio explicit deque(const A &a1=A()); //Constrói um deque com n cópias de t explicit deque(size_type n, const T &t=T(), const A &a1=A()); deque(const deque &x); ~deque(); deque &operatro=(const deque &x); //<<Métodos de acesso>> //Acesso às dimensões size_type size() const {return finish – start;} bool empty() const {return start==finish;} //Acesso por iterador iterator begin() {return start;} iterator end() {return finish;} //Acesso a elementos //Retorna uma referência para o objecto indexado reference operator[](size_type i) {return begin() [i];} //Retorna o objecto apontado por start.current reference front() {return *begin();}

4.8 Template de Classes RING BUFFER
É uma variante da queue de dimensão fixa, destinada a manter em memória os últimos n elementos que lhe tenham sido inseridos. Tal como a queue é um contentor com disciplina FIFO em que os objectos são inseridos no fim da fila e removidos no seu início. Os métodos são: push_back(T) – insere no fim da fila pop_front() – remove no início da fila front() – acede ao elemento do início para leitura back() – acede ao elemento do fim da fila para escrita O RingBuffer é implementado num array com dimensão fixa. Quando o array está cheio e se insere um novo elemento, origina-se o overflow e o elemento que está no início da fila é perdido por esmagamento. O par de iteradores start e finish mantém a dinâmica de inserção e remoção de forma similar aos iteradores de um deque. A remoção de um objecto do ring buffer limita-se a avançar o iterador start, pois o removido é sempre o do início da lista. O array deve ter sempre uma posição não usada, de modo a que o teste de contentor cheio se distinga do teste de contentor vazio. Assim, testar o contentor vazio resume-se a verificar se os iteradores start e finish apontam para o mesmo objecto. A operação de inserção é que providencia que os iteardores não apontem para o mesmo objecto se o contentor estiver cheio. O iterador do RingBuffer start, tem 2 atributos: array – aponta para o início do array; current – aponta para o elemento 1º do RingBuffer; no finish, array aponta igualmente para o início do array e current aponta para o past the end.

4.8.1. Definição do template RingBuffer
#define SUPER Container<T,A> template <class T, unsigned DIM, class A=allocator<T>> class RingBuffer : public SUPER { public: IMPORT_TYPE(pointer); IMPORT_TYPE(const_pointer); IMPORT_TYPE(reference); IMPORT_TYPE(const_reference); IMPORT_TYPE(size_type); //<<Tipo para a diferença entre iteradores>> typedef CircCount<DIM + 1> difference_type; private: template<class PTR, class REF> class IT {/*...*/}

Interface Pública
public: //<<Instanciação de Iteradores>> typedef IT<pointer, reference> iterator; typedef IT<const_pointer, const_reference> const_iterator; //<<Construtores e destrutor e operador afectação>> RingBuffer(const A &a=A()); RingBuffer(const RingBuffer&); ~RingBuffer(); RingBuffer &operator=(const RingBuffer &); //<<Métodos de acesso>> //Acesso às dimensões size_type size() const {return finish-start;} size_type max_size() const {return DIM;} bool empty() const {return start==finish; } const A &get_allocator() const {return allocator;} //Acesso por iterador iterator begin() const {return start;} iterator end() const {return finish;} //Acesso aos extremos reference front() {return *begin();}

reference back() {return *--end();} reference operator[](size_type idx) {return begin()[idx];} //<<Inserção e Remoção>> void push_back(const T&); void pop_front(); //<<Outras operações>> void clear() {destroy(start, finish); finish=start} private:

//<<Atributos>>
A allocator //Allocator para o array circular iterator start; iterator finish; //Métodos auxiliares iterator uninitializedCopy(const_iterator first, const_iterator last, iterator res); void destroy(iterator first, iterator last); }; #undef SUPER 4.8.1.1. Iterador O iterador do RingBuffer tem como atributos o índice corrente do tipo CircCount (contador circular com módulo de contagem) e um apontador para o array, de forma a ser possível as operações de desreferenciação e indexação.

4.8.1.2. e 4.8.2.4. Construtores e Destrutor e Operador Afectação
Construtor por omissão RingBuffer(const A &a=A()) : allocator(a), start(allocator.allocate(DIM+1)), finish(start) {} Construtor por cópia RingBuffer(const RingBuffer &r) : allocator(r.allocator), start(allocator.allocate(DIM+1)), finish(uninitializedCopy(r.begin(), r.end(), begin())) {} com: iterator uninitializedCopy(const_iterator first, const_iterator last, iteartor res) { for(; first!=last; ++first, ++res) allocator. construct(&(*res), *first); return res; } Destrutor ~RingBuffer() { destroy(start, finish); allocator.deallocate(start.array, DIM+1); } com: void destroy(iterator first, iteartor last) { for(;first!=last; ++first) allocator.destroy(&(*first));}

4.8.1.3. Métodos de Inserção e Remoção
void push_back(const T &t) { allocator.construct(&(*finish, t); if(++finish==start) pop_front(); } void pop_front() { allocator.destroy(&(*start)); ++start; }

CAP: 5 – ÁRVORES BINÁRIAS As estruturas em árvore é um dos tópicos mais importantes de EDA Embora não sejam componentes standard da biblioteca C++, a família dos contentores associativos suportam-se em árvores binárias de pesquisa balanceadas e o adaptador de contentores sequenciais priority_queue tem como modelo conceptual uma estrutura em árvore completa. Nas árvores cada nó pode ter um nº arbitrário de sucessores. Árvore: é a colecção de nós cuja posição e informação contida satisfazem um determinado conjunto de propriedades Nós – são os objectos constituintes das árvores Ramos – são as ligações entre os nós Raiz – é o nó de onde emergem, directa ou indirectamente, todos os ramos. Percurso – é um caminho possível entre 2 nós, satisfazendo uma dada condição Nível – de um nó é o número de ramos envolvidos no percurso entre a raiz e o nó. Altura – é o máximo nível (a raiz é o nível 0) Ascendente (ou pai) Descendentes directos (filhos) Folhas ou nós terminais Uma árvore diz-se ordenada, quando o posicionamento relativo dos descendentes de cada nó é significativo. Uma árvore binária é aquela em que cada nó tem no máximo dois descendentes directos. Uma árvore binária de pesquisa (ABP) é uma árvore binária ordenada, em que o valor contido em qualquer nó é maior ou igual que os valores contidos nos nós da sua subárvore esquerda e menor ou igual que os valores da sua subárvore direita. Uma árvore diz-se balanceada quando a diferença de alturas das duas subárvores de qualquer nó é menor que 1. Uma ABP quase balanceada (árvore red-black), no pior caso a altura de uma das subárvores nunca ultrapassa o dobro da outra, para todos os nós. Uma árvore diz-se perfeitamente balanceada se, relativamente a qualquer nó, a diferença entre o número de nós das suas subárvores for no máximo 1. Diz-se completa se estiver inteiramente preenchida, isto é, se todos os nós têm ambos os filhos, excepto no último nível que vai sendo preenchido da esquerda para a direita. Diz-se organizada em heap (monte) caso seja completa e todos os nós tiverem a propriedade do seu valor ser maior ou igual a qualquer dos seus filhos. 5.2. ABP O desempenho da pesquisa aproxima-se tanto mais da pesquisa dicotómica sobre vectores (ordem log N), quanto mais balanceadas estas estiverem. Os nós são constituídos por objectos de uma classe, com os atributos: - O valor a armazenar - Um apontador para a sua subárvore esquerda - Um apontador para a sua subárvore direita No caso de não existir alguma subárvore, o respectivo apontador toma o valor NULL. Associada a uma estrutura de nós em árvore define-se um template de classes Tree, gestoras dessa estrutura, cujo atributo fundamental é um apontador para o nó raiz, dispondo de métodos públicos (inserir nó, remover nó, aceder a todos os nós com um dado critério de percurso.) 5.2.1. Versão Básica de ABP Adoptando um nó dummy e acrescentando à estrutura dos nós um apontador para o seu ascendente, é possível definir (como prescreve o standard C++) um iterador bidireccional. Os exemplos são parecidos com o RBTree da biblioteca. #define SUPER Container<T,A> template<classT, class A = allocator<T>> class TreeBase : public SUPER { protected: //<<Tipos usados pela árvore>> struct Node;

typedef typename A::rebind<Node>::other ANode; typedef typename ANode::pointer NodePtr; typedef typename Anode::const_pointer ConstNodePtr; struct Node { T values; NodePtr Left; NodePtr right; NodePtr parent; }; //<<Declaração dos iteradores template<class NodePtr, class PTR, class REF> class IT; template<class NodePtr, class PTR, class REF> friend class IT; public: IMPORT_TYPE(reference); IMPORT_TYPE(const_reference); IMPORT_TYPE(pointer); IMPORT_TYPE(const_pointer); IMPORT_TYPE(size_type); IMPORT_TYPE(difference_type); typedef IT<NodePtr, pointr, reference> iterator; typedef IT<ConstNodePtr, const_pointer, const_reference> const_iterator; protected: //<<atributos>> A constr; //Allocator para construir objectos T ANode alloc; size_type sz; NodePtr dummy; NodePtr root; //Métodos auxiliares declarados na parte protegida //<<Métodos de instanciação e destruição de nós>> //Aloja nó sentinela e inicia membros apontadores a apontarem para ele próprio NodePtr newNode(); //Auxiliar de insert(). Aloja um Node e inicia value com e, left e right com NULL e parent com p NodePtr newNode(NodePtr p, const T &e); //Auxiliar de copy(). Aloja um Node e constrói value por cópia de p.value NodePtr newNode(ConstNodePtr p); //Destrói e desaloja o Node apontado por p void deleteNode(NodePtr p); //<<Métodos que os métodos públicos homónimos invocam>> //Template de métodos de percursos; Visit é um objecto função ou apontador para função; determina a acção a executar nos nós visitados template<class Visit> void preorder(NodePtr r, Visit) const; template<class Visit> void inorder(NodePtr r, Visit) const; template<class Visit> void postorder(NodePtr r, Visit) const; //<<Métodos auxiliares de inserção e remoção>> //Se MULTI for true executa o insertMulti(), se falso insertUni() template<bool MULTI> pair<iterator, bool> insertNode(const T&); //Insere um nó à esquerda de r. Retorna o nó inserido NodePtr insertLeft(NodePtr r, const T &e); NodePtr insertRight(NodePtr r, const T &e); //Afecta parent de descendente com ascendente static void setParent(NodePtr child, NodePtr father); //Auxiliar de erase(). o parent de x passa a parent de y void transferParent(NodePtr x, NodePtr y);

//Auxiliar de erase(), o nó x é substituído pelo nó y. O parent de x passa a parent de y. as subárvores direita e esquerda de x passam a subárvores direita e esquerda de y void transferNode(NodePtr x, NodePtr y); //Destrói a árvore ou subárvore com raiz Em r void clear(NodePtr r); //Constrói uma árvore por cópia da árvore com raiz em r. Retrorna apontador para raiz da cópia NodePtr copy(const TreeBase &x); //Retorna o nó mais à direita da subárvore r. template<class NP> static NP rightMost(NP r); template<class NP> static leftMost(NP r); //Auxiliar do método público isBalance(). static bool isBalanced(ConstNodePtr, int &); //Converter a árvore em lista; auxiliar do método balance() static void treeToList(NodePtr r, NodePtr &header); //Converter lista em árvore; auxiliar do método balance() static NodePtr listToTree(NodePtr &header, size_type n); //<<Conversão de Iteradores em apontadores e vice versa. Estes métodos auxiliares poderão ser necessa´rios às classes derivadas, que não sendo friend do template de classes IT, não têm acesso aos construtores nem ao atributo current. iterator getIterator(NodePtr p) {return p; } NodePtr getNode(iterator i) {return i.current; } //Interface Pública public: //<<Construtores e Destrutor TreeBase(const A &a = A()) : alloc(a), constr(a), root(NULL), dummy(newNode()), sz(0) {} TreeBase(const TreeBase &x); ~TreeBase(); //<<Sobrecarga do operador afectação TreeBase &operator=(const TreeBase &x); //<<Cópia do allocator usado na construção>> A get_allocator() const {return constr); } //<<Percursos sobre a árvore>> /*Executar a acção imposta pela função visit() sobre o atributo value dos sucessivos nós visitados, segundo cada um dos modos de percurso template<class Visit> void preorder(Visit visit) const {preorder(root, visit); } template<class Visit> void inorder(Visit visit) const {inorder(root, visit); } template<class Visit> void postorder(Visit visit) const {postorder(root, visit); } //<<Obtenção de Iteradores iterator begin() {return dummy->left; } iterator end() {return dummy; } //<<Acesso às dimensões size_type size() const {return sz; } bool empty() const {return size()==0; } //<<Acesso aos elementos reference front() {return *begin(); } reference back() {return *(--end()); } //<<Métodos de pesquisa, inserção e remoção>> //Pocura um nó cujo valor seja n. Se existir retorna um iterador para esse nó. Casso contrário retorna end(), que é o dummy. iterator find(const T &n); //Insere ordenadamente (???) o valor n, caso ainda não exista

pair<iterator, bool> insertUni(const T &n); //Insere ordenadamente o valor n, mesmo que já exista iterator insertMulti( const T &n); //Remove da árvore o nó cujo valor seja n, caso exista size_type erase(const T &n); //Remove da árvore o elemento apontado por i void erase(iterator i); //Remove todos os elementos void clear() //Promove o balanceamento da árvore void balance(); //Métodos auxiliares de debugging //Mostra ordenadamente os valores contidos na árvore void display() const; //Testa se a árvore está de facto ordenada bool isOrded() const; //Testa se a árvore está balanceada bool isBalanced() const; //Mostra a topologia da árvore usando espaçamentos horizontais por níveis void printOn(ostream &o) const; }; 5.2.1.1. Percursos prefixo, infixo e sufixo Percorrer uma árvore consiste em visitar todos os seus nós por uma determinada ordem, entendendo-se por visitar um nó realizar algum tipo de acção sobre o valor que lhe esteja associado (contar o nº total de objectos inseridos na árvore, mostrar no ecrã esses valores, etc.) - O percurso prefixo (preorder) visita o nó raiz, depois percorre a subárvore esquerda e, finalmente, percorre a subárvore direita; - Infixo: Esquerda, Raiz, Direita - Sufixo: Esquerda, Direita, Raiz Numa árvore de pesquisa ordenada de forma crescente tem relevância o percurso infixo, dado que será esse o percurso usado pelo iterador para percorrer por ordem crescente. Em qualquer dos tipos de percursos, acção a executar sobre cada nó é determinada pelo parâmetro template Visit (objecto função ou apontador para função) //Percurso preorder template<class T, class A> template<class Visit> void TreeBase<T,A>::preorder(NodePtr r, Visit visit) const { if(r==NULL) return; visit(r->value); preorder(r->left, visit); preorder(r->right, visit); Fazer percurso inorder e postorder. Método público display template<class T> void displayValue(const T &t) {cout << t << ‘ ‘;} template<class T, class A> void TreeBase<T,A>::display() const {inorder(displayValue<T>()); } 5.2.1.2. Pesquisa A pesquisa numa árvore balanceada tem eficiência O(log n), dado que o nº de testes a realizar é igual ao nível em que foi encontrado o valor passado como parâmetro. O critério de pesquisa é: - Caso a árvore a pesquisar esteja vazia, retorna-se o iterador end(); - Caso o valor procurado seja maior que o da raiz, continua-se a pesquisa na subárvore direita; - Menor, esquerda - Igual, retorna-se o iterador para a raiz Método público iterator find (const T &e) {return find(root, e) ; }

Método reursivo privado NodePtr find(NodePtr r, const T &e) { if(r==NULL) return end(); if(r->value < e) return find(r->right, e); if(r->value > e) return find(r->left, e); return r; } No entanto, dado que se trata de um método recursivo terminal, é facilmente convertível à forma iterativa, com melhor desempenho e sem precisar do método auxiliar: iterator find(const T &e) { NodePtr r=root; while(r!=NULL) { if(r->value < e) r=r->right; else if(e<r->value) r=r->left; else eturn r; } return end(); } No caso de existirem repetições, é conveniente retornar o iterador para o elemento mais antigo. Por esse facto iniciamos um apontador com dummy ecomeçando pela root executa-se: - caso o valor do nó visitado seja menor que o valor procurado, prossegue-se a iteração na subárvore direita - caso contrário, afecta-se aux com o apontador para esse nó e prossegue-se a iteração na subárvore esquerda - atingindo uma folha, testa-se o valor de auxiliar: - caso aux seja dummy ou se o valor procuardo for menor que o valor apontado por aux, conclui-se pela sua não existência e retorna-se o iterador para o past the end - caso contrário, retorna-se o iterador par o nó apontado por aux. Fazer com estas regras.

5.2.1.4. Inserção
O método público insertUni() deve retornar 2 valores: um bool a indicar se ocorreu ou não inserção e um iterador a apontar para o nó da árvore em que o elemento reside, quer tenha sido inserido, quer já existisse anteriormente. O modo normal como a biblioteca standard resolve o caso de um método ter de retornar 2 valores, consiste em usar um template de estruturas pair, instanciar dele uma estrutura template pair com os parâmetros adequados (do tipo dos valores a retornar) e declarar a função retornando uma instância dessa estrutura. template<class U, class V> struct pair { U first; V second; pair(): first(U()), second(V()) {} pair(const U &u, const V &v) : first(u), second(v) {} }; ex: Começa-se por comparar e com a raiz. Se menor que a raiz compara-se com raiz da subárvore esquerda, se maior, com a direita, se maior direita, se vazia, isere-se; e afecto o membro right do nó com o endereço do novo nó. pair<iterator, bool> insertUni(const T &e) { NodePtr r; //Apontador para o novo nó if root(==NULL) //Árvore vazia r=root=dummy->left=dummy->right=newNode(dummy, e); //inicia root com parent=dummy; left e right=NULL else { r=root; for(;;) if (e < r->value) if(r->left != NULL) r=r->left; else {r=insertLeft(r,e); break; } else if (r->value < e) if (r->right != NULL) r=r->right;

else {r=insertRight(r,e); break; } else return pair<iterator, bool>(r,false); } ++sz; return pair<iterator, bool>(r, true); } Os métodos auxiliares insertLeft() e insertRight() realizam a inserção, afectando o membro left ou right do nó passado como parâmetro com o endereço de um novo nó. Providenciam a actualização de dummy->left ou dummy->right se a inserção for realizada num dos extremos da árvore e retornam o apontador para o novo nó. Fazer com estas regras.

Método InsertMulti
Ex: começa-se por comparar 6 com a raiz. dado que 6 é igual a 6, compara-se com 8 na subárvore direita, seguidamente com 7 e ao tentar comparar com a raiz da subárvore esquerda do nó 7, constata-se que esta subárvore está vazia. Então insere-se como descendente esquerdo do 7. Fazer com estas regras.

5.2.1.5. Remoção
Há 2 métodos: um para remover objectos dado o valor e outro dado um iterador para o nó onde esse objecto se encontra. erase(T &e) remove todos os objectos com valor igual a e retornando o nº de objectos removidos. Começa por invocar o método find(), que retorna um iterador para o 1º nó a remover (caso exista) ou o iterador end() caso não exista. Se existir invoca repetidamente o método erase(i), incrementando o iterador, enquanto o valor apontado pelo iterador for igual ao valor a remover (ordenada por valores ???) template<class T, class A> TreeBase<T,A>size_type TreeBase<T,A>::erase(const T &e) { size_type count = 0; iterator p = find(e); if (p != end()) do {++count; erase(p++); } while (p!=end() && !(e<*p)); return count; } O método erase(iterator pos) providencia a remoção do nó referenciado por pos, tendo o cuidado de reconstituir as ligações dos nós envolventes por forma a que continuem a satisfazer os requisitos de ordenação da árvore. Função relativa do nó a remover, podemos distinguir 3 casos: - O nó a remover é uma folha - O nó a remover tem um único descendente - O nó a remover tem 2 descendentes No 1º caso desliga-se simplesmente o nó, afectando com NULL o apontador left ou right do seu parent. (e o left ou right do dummy???) No 2º caso, afecta-se o apontador left ou right do seu parent para passar a apontar para o seu descendente e actualiza-se o membro parent do seu descendente. Os métodos auxiliares transferParent e setParent tratam do caso (ver pg.306 e 307) No 3º caso, o modo de restabelecer a coerência exige uma actuação mais complexa: - Identificar o nó mais à direita da subárvore esquerda (apontado por previous no código) – é o maior do menores. - desliga-se esse nó da árvore, colocando a sua subárvore esquerda (que pode ser vazia) como descendente do seu pai - Insere-se esse nó na posição que se situava o nó a remover. Resumindo: as acções a realizar pelo método erase() são: - Providenciar a actualização dos membvros left e/ou right do nó dummy, caso o nó a remover corresponda ao menor (left most) e/ou ao maior (right most) da árvore global - Desligar o nó, tendo o cuidado de reconstituir as ligações dos nós envolventes por forma a que conmtinuem a satisfazer os requisitos de ordenação da árvore - Libertar a memória ocupada pelo nó.

template<class T, class A> void TreeBase<T,A>::erase(iterator i) { NodePtr r = i.current; if (r == dummy.left) //Actualizar left most dummy->left=(++iterator(i)).current; if(r == dummy->right) dummy->right=(--iterator(i)).current; if (r->left == NULL) transferParent(r, r->right); else if (r->right == NULL) transferParent(r, r->left); else { NodePtr previous = rightMost(r->left); //Procurar transferParent(previous, previous->left); //Desligar transferNode(r, previous); //substituir } --sz; deleteNode(r); //Libertar memória } 5.2.1.6/7/8/9 Construtor por Cópia / Operador afectação por cópia / Métodos auxiliares newNode() e deleteNode() / Destrutor 5.2.1.10. Balanceamento Já realçámos a necessidade de manter a árvore balanceada (repetidas inserções e remoções desbalanceiam), sob pena de degradar drasticamente o desempenho dos seus métodos.. O método que vamos analisar permite regenerar o balanceamento de uma árvore, com desempenho pouco eficiente O(n), facto que não o torna recomendável para aplicações genéricas. A solução para garantir a permanência do balanceamento das árvores, frente a repetidas inserções e remoções, sem penalizações gravosas de desempenho, será estudada mais adiante, nas árvores red-black. No entanto é didáctico e elegante pois manipulamos só apontadores, sem necessidade de realizar cópias. - Converte a árvore numa lista ordenada simplesmente ligada, invocando o método auxiliar treeToList() e, seguidamente, converte a lista numa árvore balanceada, invocando listToTree(). template<class T, class a> void TreeBase<T,A>::balance() { if (size() <=2) return; NodePtr header = NULL; treeToList(root, header); root=listToTree(header, size()); root->parent = dummy; } Conversão de uma árvore em lista este método providencia a concatenação de todos os nós da árvore numa lista ordenada, simplesmente ligada, usando o campo right dos nós da árvore como apontador next da lista. O algoritmo recursivo é: - Se a árvore estiver vazia, terminar o algoritmo; - Converter em lista a subárvore direita; - Inserir o nó raiz à cabeça da lista produzida; - Converter em lista a subárvore esquerda (ficando antes da já produzida). template<class T, classA> void TreeBase<T,A>:: treeToList(NodePtr r, NodePtr &header) { if(!r) return; treeToList(r->right, header); r->right = header; header = r; treeToList(r->left, header); }

Conversão em Árvore de n Elementos de uma Lista - Se o nº de nós da lista for zero, a árvore é vazia e termina o algoritmo - Converter em árvore a primeira metade dos nós; - Desligar o nó que ficou situado à cabeça da lista, que passará a constituir a raiz da árvore - Agregar à raiz como subárvore esquerda a árvore já obtida - Converter em árvore os restantes nós da lista (nº de nós da lista original menos metade menos um) e agregando-a à raiz como subárvore direita. ... 5.3. Árvores Binárias Organizadas em Heap Heap (monte) toma diferentes significados em informática. No presente contexto queremos referir a estrutura de dados representada como uma árvore binária completa, tal que, qualquer dos seus nós toma valor maior ou igual ao dos seus filhos, garantindo por esse facto que o nó de maior valor é a raiz. 5.3.1. Estrutura Heap Tem propriedades muito interessantes que a recomendam para suporte do adaptador dos contentores standard priority_queue e como base conceptual do algoritmo de ordenação heap_sort, aplicável a contentores sequenciais com acesso aleatório. Dado ser completa tem representação implícita em array (bloco contíguo de memória). 5.3.1.1. Representação de Árvores Completas em Array Numeramos os nós de uma árvore completa de cima para baixo e da esquerda para a direita, atribuindo à raiz o nº 0, a árvore tem uma representação implícita em array, inserindo cada um dos seus nós no índice correspondente à numeração que lhe foi atribuída. Desta correspondência resulta uma relação aritmética simples entre os índices ocupados pelos nós da árvore e os índices ocupados pelos seus filhos esquerdo e direito, Nomeadamente: - Um nó situado no índice k tem o seu filho esquerdo situado no índice 2*k+1 e os eu filho direito no 2*k+2 - Os filhos esquerdos situam-se nos índices ímpares e os direitos nops pares - Se um filho esquerdo(direito) estiver no índice k, o seu irmão direito(esquerdo) está no k+1(k-1) - O pai de um nó situado no índice k situa-se em (k-1)/2 As estruturas de dados representados em árvores completas podem ser alojadas em array ou em qualquer contentor sequencial, cujo iterador seja de acesso aleatório, como é o caso do vector e deque. Uma das consequências importantes que advém do facto de uma estrutura heap ser representada implicitamente num array, é não ser preciso que na sua implementação os nós da árvore disponham dos apontadores left, right e parent como na ABP. O nó, neste caso, é exclusivamente constituído pelo objecto T (valor). 5.3.1.2. Algoritmos genéricos standard das estruturas heap As setruturas heap revelam-se muito interessantes quendo se trate de aplicações que se pretenda, através de acções push_heap, inserir num contentor sequencial valores aleatórios e poder retirar desse contentor, através de uma acção pop_heap(), o elemento de maior valor, ou de menor valor, conforme o critério de comparação utilizado. Nas estruturas heap consegu-se isso com complexidade O(log n), como só as árvores balanceadas conseguem.

5.4 Adaptador Sequencial priority_queue Um adaptador de contentores sequenciais consiste num template de classes contentoras, que toma como parâmetros tipo não só o tipo de objectos que vai alojar, como também o tipo de contentor sequencial em que se suporta. Da biblioteca STL de componentes constam 3 tipos de adaptadores de contentores sequenciais: - stack (pilha) - queue (fila de espera) - priority_queue (fila de espera com prioridades) Os primeiros 2 tipos não utilizam estruturas em árvore e as suas definições, para além dos conceitos que os 3 partilham, resumem-se a uma aplicação dos temas já tratados no capítulo anterior. O 3º baseia-se numa estrutura em heap, e é interessante pô-lo em confronto com os outros 2 para podermos inferir as aplicações mais adequadas a cada. 5.4.1. Adaptadores de Contentores Caracterizam-se por disponibilizar um nº muito reduzido de métodos públicos, correspondentes às acções que lhe são típicas.

5.4.1.1. Class stack<T,Sequence>
Obedece estritamente a uma disciplina FILO e disponibiliza: - push() – para inserir objectos no topo da pilha - pop() – para retirar/remover objectos do topo da pilha - top() – retorna uma referência para o objecto situado no topo da pilha O parâmetro tipo ou segundo argumento do template pode ser o vector, deque ou list, já que todos dispôem, na sua interfgace, métodos que dão suporte directo às acções que são específicas dos contentores sequenciais, nomeadamente: - push_back() – acrescenta o elemento indicado como argumento, que é do tipo Sequence::value_type, no fim do contentor sequencial. - pop_back() – não toma argumentos e retira o elemento armazenado no fim do contentor. - back() – retorna uma referência para o elemento que se encontra no fim do contentor. O parâmetro tipo Sequence deve ser escolhido criteriosamente, conforme a aplicação a que se destina o stack, tendo em conta que: - O vector pode aumentar o espaço reservado, mas não permite reduzi-lo em run-time - O deque permite aumentar e reduzir em run-time e as acções são um pouco menos eficientes que as do vector - A list reserva e devolve espaço em run-time, mas apesar de terem complexidade constante nos métodos pretendidos, são mais lentos que os anteriores. A diferença básica dos contentores sequenciais e dos adaptadores é que os 1ºs implementam eles próprios a estrutura de dados que lhe é específica, enquanto os adaptadores toma como parâmetro o tipo de contentor e agregam como atributo um objecto desse tipo ao qual delegam as acções de acesso. template<class T, class Sequence = deque<T>> class stack { public: typedef typename Sequence::value_type value_type; typedef typename Sequence::size_type size_type; typedef typename Sequence::reference reference; typedef typename Sequence::const_reference const_reference; typedef Sequence container_type; //<<Atributo>> protected: Sequence c; //<<Interface Pública>> public: stack() : c() {} explicit stack(const Sequence &s) : c(s) {} bool empty() const {return c.empty();}

size_type size() const {return c.size();} reference top() {return c.back();} void push(const value_type &x) {c.push_back(x);} void pop() {c.pop_back();} friend bool operator==(const stack &x, const stack &y) {return x.c==y.c;} };

5.4.1.2. Classe queue<T,

Sequence>

Este adaptador obedece à disciplina FIFO e disponibiliza: push() – Insere objectos na fila de espera; pop() Remove objectos na cabeça da fila; front() – Retorna uma referência para o objecto que permanece à mais tempo na fila back() – Retorna uma referência para o objecto que foi mais recentemente inserido. Estas acções podem ser delegadas num contentor sequencial que disponha de um método pop_front() que dê suporte ao pop, o que exclui o vector, dado que este não permite remoções no início do domínio em tempo constante. A definição é semelhante, pelo que deixamos como exercício.

5.4.2. Definição do template priority_queue
Pode entender-se como um refinamento do queue e dispõe: push() – Insere objectos na fila de espera; pop() – Remove o objecto ao qual foi atribuído maior prioridade através do objecto função tipo Cmp que lhe seja passado como 3º parâmetro template. top() – Retorna uma referência para o objecto de maior prioridade. O “pequeno” pormenor que distingue o adaptador priority_queue do adaptador queue é o facto do standard impor complexidade O(log n) aos algoritmos de todos os seus métodos, o que implica que a representação do contentor sequencial em que se suporte seja organizado segundo uma disciplina heap. Este adaptador suporta-se, por omissão, num vector, e os seus métodos invocam os algoritmos push_heap() e pop_heap() já apresentados. template<class T, class Sequence = vector<T>, class Cmp=less<typename Sequence::value_type>> class priority_queue { public: typedef typename Sequence::value_type value_type; typedef typename Sequence::size_type size_type; typedef typename Sequence::reference reference; typedef typename Sequence::const_reference const_reference; typedef Sequence container_type; //<<Atributos>> protected: Sequence c; Cmp cmp; //<<Interface Pública>> public: explicit priority_queue(const Cmp &cp = Cmp(), const Sequence &pq=Sequence()) : cmp(cp), c(pq) {} bool empty() const {return c.empty();} size_type size() const {return c.size();} const value_type &top() const {return c.front();} void pop() {pop_heap(c.begin(), c.end(), cmp); c.pop_back(); } void push(const value_type &x) {c.push_back(x); push_heap(c.begin(), c.end(), cmp);} };

CAP.6 – ÁRVORES BALANCEADAS
6.1. Introdução As árvores binárias ordenadas por valor de chave de pesquisa são muito boas quanto ao desempenho das acções de pesquisa, remoção e inserção, mas exigem que se mantenham balanceadas, isto é, que a sua altura se mantenha da ordem do logaritmo do número de elementos que contém. Assim, temos que adoptar métodos de inserção e remoção que preservem os eu balanceamento. Em casos específicos, alternativamente, podemos manter controlo sobre a altura e sempre que esta exceda um determinado valor (como sugere Knuth) 5.log2n, balancear com um método como o que se apresentou no cap. anterior, que exige tempo de execução linear O(n). Em 1962 foram apresentadas as AVL que são ABP que garantem inserção e remoção com permanência de balanceamento. Em 1970 R.Bayer desenvolveu estrutura arborescente de páginas, com elevado nº de chaves de pesquisa por página. Multidescendentes, portanto e garantem inserção e remoção com manutenção de balanceamento. São as B-Tree ou árvores de Bayer. Depois desenvolveu uma variante “symmetric binary B-Tree”, que é B-Tree de ordem 3 (com 2 ou 3 descendentes por página) vocacionada para residir em memória central. Estas evoluíram para ordem 4 (2, 3 ou 4 descendentes), denominadas red-black. A generalidade das implementações da biblioteca standard do C++ adoptam as árvores red-black como suporte dos contentores associativos. Os algoritmos das árvores red-black, relativamente às ABP, só diferem quanto aos métodos de inserção e remoção. No entanto, dada a complexidade relativa, quer da implementação, quer da análise destes 2 métodos, vamos adoptar uma abordagem a 3 níveis: gráfica, pseudo-código e C++ É importante pedagogicamente pois é o método que deve ser abordado quando algoritmos são muito complexos (mais de 7 decisões segundo psicólogos). 6.2. Estruturas B-Tree – (árvores de Bayer) Desde os primórdios da informática que o objectivo de aceder com eficiência a bases de dados de grandes dimensões, com natureza persistente, ocupa lugar de destaque. dado o tempo de latência, impõe-se que os acessos a ficheiro não se façam individualmente por item, como nas árvores binárias, mas sim por páginas (com dezenas ou centenas de itens). Numa ABP mesmo que equilibrada, envolvendo um milhão de itens, uma acção de pesquisa pode requerer o teste de 20 nós (O(log2 n). Se aglomerarmos os itens em páginas de 100 itens, o nº máximo de acessos a páginas, para a mesma quantidade de itens, é apenas 3. Num regime de inserções e remoções frequentes, torna-se impossível garantir que todas as páginas tenham o mesmo nº de itens inseridos. O balanceamento refere-se a páginas. Torna-se necessário então providenciar um nº mínimo e máximo de itens por página, até por causa do espaço ocupado em disco. A ordem de uma B-Tree é o nº máximo de descendentes de cada página. Uma B-Tree de ordem N satisfaz: - Todas as páginas têm no máximo N descendentes - Todas as páginas, excepto a raiz, têm no mínimo N/2 descendentes - A raiz tem no mínimo 2 descendentes ( a menos que também seja folha) - As folhas situam-se todas ao mesmo nível e não têm descendentes - Uma página que não seja folha, com k descendentes, contém k-1 itens - Numa B-Tree de ordem N, o nº de itens contidos situam-se entre N-1 e (N-1)/2 inclusive. item é corporizado por uma estrutura contendo um membro com a chave de pesquisa e um membro com o valor associado à chave. Os valores inteiros aparecem por ordem crescente da esquerda para a direita, se imaginamos a BTree comprimida num único nível. 6.2.1.1. Algoritmo de Pesquisa Vamos considerar cada um dos valores inseridos na B-Tree como raiz de uma árvore, com uma subárvore esquerda, onde se situam valores inferiores a essa raiz, e com uma subárvore direita onde se situam valores que lhe são superiores.

A pesquisa de um valor numa B-Tree processa-se de modo semelhante à pesquisa em ABP: - Pesquisa-se primeiro dentro da página raiz - Caso o valor procurado não se encontre nessa página, prossegue-se a pesquisa na subárvore direita do maior dos menores valores relativamente ao valor procurado - Se o valor procurado for menor que todos os valores existentes na página, prossegue-se a pesquisa na subárvore esquerda do menor dos valores existentes nessa página. - Termina com insucesso quando se atinge uma página folha que não contenha o nº. Como as B-Tree são por natureza balanceadas, se providenciarmos dentro de cada página, uma procura também O(log n), ficamos com um total de eficiência de O(log n). O algoritmo de pesquisa, recursivamente, é: - Caso a B-Tree esteja vazia retorna false - Se a página raiz contém t, retorna true - Caso contrário: - Se o valor procurado for menor que todos os valores existentes na página, pesquisar recursivamente na subárvore esquerda do menor dos valores existentes nessa página; caso contrário, pesquisar recursivamente na subárvore direita do maior dos valores menores que t nela existentes. 6.2.1.2. Algoritmo de Inserção Supondo, para já, que não são permitidos valores repetidos. Para inserir procede-se primeiro à sua pesquisa. Caso encontrado, desiste-se da inserção e retorna-se informação desse facto. Caso contrário, insere-se ordenadamente esse valor na página folha onde terminou a pesquisa. Se não ficar sobrecarregada, a inserção fica consumada. Caso contrário, teremos que realizar um operação de split, criando uma nova página irmã à sua direita com o seguinte critério: - O valor central da folha sobrecarregada é inserido na página ascendente - Os valores que se encontravam à direita do valor central transferem-se para uma página irmã criada à sua direita. O split pode ser propagado, no pior dos casos até à raiz. As inserções envolvem sempre um par constituído pelo valor a inserir e pelo apontador para a subárvore direita que lhe fica associada ( a nova criada por split): ex: insert(Pair(7,NULL) --- quando não sabemos onde vai ficar insert(Pair(6,p6)) --- quando há propagação de split. Há ainda que ver que há mudança de página ascendente (parent) em páginas não directamente envolvidas na inserção. Se todas as páginas até à raiz tiverem que ser desmembradas, a árvore cresce em altura, mantendo no entanto o balanceamento. A criação da página raiz é pois a única circunstância que pode fazer aumentar a altura da árvore. Cresce das folhas para a raiz ao contrário das ABP. A inserção de valores por ordem crescente, que tornavam a ABP numa lista, aqui não se verifica. Recursivamente, o algoritmo de inserção é: - Caso a B-Tree esteja vazia (o apontador root igual a NULL), cria a página raiz com o elemento a inserir - Caso contrário, promove a sua inserção ordenada na página folha. Se a folha ficar sobrecarregada, realiza split dessa folha e invoca recursivamente o algoritmo de inserção sobre a página ascendente.

CAPÍTULO 7 – CONTENTORES ASSOCIATIVOS
TEORIA
O standard ANSI/ISO estabelece para os contentores associativos que estes devem garantir complexidade O(log n) para os métodos de pesquisa, inserção e remoção (ao contrário dos contentores sequenciais). Assim, têm de ser suportados em árvores binárias balanceadas (ou quase balanceadas). Os contentores associativos standard que vamos estudar suportam-se no template de classes RBTree, derivando desse template. As tabelas de hash, por vezes, são uma boa alternativa relativamente às árvores, como estruturas de suporte dos contentores associativos, pelo que vamos também definir essa extensão à biblioteca standard. As tabelas de hash são de utilização recomendável, por exemplo, nos casos em que o objectivo a atingir não é reduzir o nº de colisões de chaves ao mínimo, mas sim repartir por vários contentores parciais os elementos a que se pretende aceder com eficiência. Méritos e deméritos dos contentores sequenciais: VECTOR – M – Suporta iteradores de acesso aleatório; Complexidade O(1), constante, para inserção e remoção no fim do contentor. D– Complexidade linear para inserções e emoções no meio e início do domínio; Limitações quanto à evolução dinâmica: podem aumentar o espaço reservado mas não diminui posteriormente. DEQUE M– Suportam iteradores de acesso aleatório (menos eficientes que os de vector); Complexidade O(1) para inserções e remoções em ambos os extremos; Podem aumentar e diminuir o espaço reservado ao longo da evolução dinâmica. D– Complexidade linear O(n) para inserções e remoções no meio do domínio. LIST M– Complexidade O(1) para inserções e remoções em qualquer ponto. D– Suportam apenas iteradores bidireccionais; Complexidade linear na pesquisa; Para estruturas de grandes dimensões não é tolerável a complexidade O(n) para acções que sejam frequentemente invocadas. A biblioteca STL define 4 variantes de contentores associativos: MAP – contentor de elementos constituídos por pares (chave, dados), ordenados por chave, sem repetições; MULTIMAP – map que aceita repetições de chaves equivalentes; SET – contentor de elementos constituídos apenas pela chave, sem repetições; MULTISET – set que aceita múltiplas chaves equivalentes. São denominados associativos porque associam chaves a dados.

INTERFACE PÚBLICA
begin () Retorna um iterator ou const_iterator para o primeiro elemento. end() Retorna um iterator ou const_iterator para o past the end. swap(container) Trocar os elementos com o contentor indicado clear()

Remover todos os elementos size() Retorna o nº de elementos max_size() Retorna a dimensão máxima que pode atingri empty() Retorna true se o contentor estiver vazio // Métodos adicionais da interface pública: key_comp() Retorna o objecto função comparação de chaves usado na construção value_comp() Retorna o objecto função de comparação de valores usado na construção. insert(t) Insere o valor t. Retorna o pair<iterator,bool>, em que o primeiro membro aponta para um elemento com chave equivalente à de t ou para o elemento inserido, conforme o segundo seja false ou true. insert(p,t) Idêntico ao anterior, em que o iterador p indica onde deve começar a pesquisa. insert(i,j) Insere os elementos situados no domínio [i,,j[ definido pelos iteradores i e j. erase(k) Remove elementos com chave k e retorna o nº de elementos removidos erase(q) Remove elemento apontado por q find(k) Retorna iterador para elemento com chave equivalente a k, ou para o past the end. count(k) Retorna nº de elementos com chave k lower_bound(k) ; upper_bound(k) ; equal_range(k) Conjunto de tipos dependentes do objecto função comparação Cmp, que os contentores devem definir: key_type - tipo das chaves key_compare – tipo da comparação usada para ordenar chaves value_compare – tipo da comparação usada para valores. Construtor recebem como argumentos um objecto Cmp, que estabelece o modo de comparação dos elementos, e um allocator. Por omissão dos argumentos é usado o objecto função Cmp e allocator A, construídos com o construtor sem parâmetros dos tipos indicados nos parâmetros template.

TEMPLATE DE CLASSES MAP
template <class K, class T> struct FirstOfPair { const K &operator() (const pair<K,T> &p) const {return p.first;} }; #define SUPER RBTree<K, pair<K,T>, FirstOfPair<K,T>, Cmp, A> template <class K, class T, class Cmp = less<K>, class A = allocator <pair<K,T>> class map: public SUPER { public: IMPORT_TYPE(value_type); IMPORT_TYPE(iterator); typedef T mapped_type; map(const Cmp &c = Cmp(), cons A &a = A()) : SUPER(c,a) {} pair<iterator,bool> insert(const value_type &value) {return insertUni(value); }

T &operator[](const K &key) {return insert(value_type(key, T())).first->second; } }; #undef SUPER EXEMPLO DE COMO SE INSTANCIA E USA O TEMPLATE DE CLASSES map void main() { typedef map<string, unsigned> Table; Table table; string word; while (cin >> word) ++table[word]; table::iterator i; for (i=table.begin(); i != table.end(); ++i) cout << i->first << ‘ ‘ << i->second << endl; }

TEMPLATE DE CLASSES SET
É muito parecido com o map. Tentar fazer. set<K,Cmp,A>

TEMPLATE DE CLASSES MULTIMAP E MULTISET
A diferença é que o método acrescentado, à RBTree, insert() delega no método da RBTree insertMulti() da RBTree. Tentar fazer.

EXEMPLO DE APLICAÇÃO DE UM MULTIMAP
Mostra no console output as palavras lidas do console input, identificando as linhas e colunas em que ocorreram. void main() { typedef string Key; typedef pair<unsigned, unsigned> Position; typedef pair<Key, Position> Value; typedef multimap<Key, Position> Table; typedef Table::iterator Iterator; table table; string line; Key word; unsigned column; for (unsigned numLine=1; getline(cin, line); ++numLine) { //Stream de entrada associado a uma string C-style. istrstream is(line.c_str()); while (is >> word) { column=is.telg() – word.size() + 1; table.insert(Value(word, Position(numLine, column))); } } for (iterator i=table.begin(); i != table.end(); ++i) cout << i->first << “ – [ “ << i->second.first << ‘:’ << i->second.second << ‘]<< endl; } Notas sobre este programa: Ao extrair uma palavra de um istream (cin) não se consegue distinguir a linha em que estava, dado que o carácter ‘\n’ é consumido como separador de palavras. A solução proposta é ler uma linha, invocando a função global getline() e posteriormente instanciar um istrstream passando ao seu construtor, como parâmetro, a cadeia de caracteres lida.

TABELAS HASH (abertas)
TEORIA As tabelas de hash adoptam o critério de converter a chave de pesquisa, por uma relação aritmética, directamente no índice de um array onde se encontra o valor a ela associado. Em casos favoráveis consegue-se encontrar em tempo constante o valor pesquisado, ou seja, envolvendo um único teste. A correspondência entre o universo das chaves e o universo dos índices deve ser unívoca mas normalmente não é biunívoca. Assim, as colisões são normais, isto é, associado a cada índice do array a, não corresponde exclusivamente uma chave, mas sim uma lista de chaves, associadas aos respectivos dados. A pesquisa dos dados associados a uma chave exigirá, além da execução da função h(k), um acesso ao array a indexado por h(k) e o teste de comparação para confirmar se de facto a[h(k)] corresponde à chave procurada. 3 casos pode ocorrer: a[h(k)] é um apontador NULL ==> chave k não consta da tabela. a[h(k)] aponta par um nó da lista da qual consta a chave k ==> pesquisa com um só teste a[h(k)] aponta para um nó da lista que não corresponde à chave pesquisada ==> testes sequenciais ao longo da lista - O(n). O factor de carga é M/N. Quanto menor for o factor de carga, menor é a probabilidade de colisões, como convém (mas a relação não é linear). Caso as chaves de pesquisa sejam strings deve inicialmente providenciar-se a conversão da string num valor numérico e só depois efectuar a conversão desse valor em índices do array. Baseados numa estimativa do nº máximo de chaves a inserir na tabela, implementa-se um array de apontadores para listas simplesmente ligadas. Dos nós das listas constam pares (chave, valor) (mais apontador para next). A cada conjunto de chaves em colisão chama-se bucket. Deseja-se pois que os buckets não sejam muito grandes pois é no interior deles que se tem de pesquisar (sequencialmente) no caso de colisões de chaves. FUNÇÕES HASH Condições básicas: - Envolver operações aritméticas de rápida execução - Minimizar o nº de colisões Das características da função hash depende a eficiência dos métodos de inserção, remoção e pesquisa. Deve pois apresentar probabilidade uniforme para todos os elementos do contradomínio, mas isso também depende da distribuição das chaves de domínio. Não há função hash óptima para todas as aplicações. endereçamento directo – domínio dos valores de chaves = domínio dos índices do array. É função hash perfeita, porque biunívoca. Não é sempre possível devido ao desperdício de memória. O critério que preside ao estabelecimento de uma função hash é procurar uma relação entre a chave de pesquisa e um valor numérico, módulo size_t, o mais “disperso” possível. Então é melhor que N seja sempre nº primo. CRITÉRIOS DE OPTIMIZAÇÃO Dimensão do array: - Deve ser nº primo - Ser maior que o nº de chaves que é previsível inserir (factor de carga < 1) - Se é pretendido tabela dinâmica, tem de estar-se sempre a verificar factor de carga e, quando se aproximar de 1, aumentar o tamanho do array para o nº primo que seja o mais próximo do dobro do anterior. Esta operação é morosa mas imprescindível. Para poupar tempo interessa que na definição da tabela conste como constante um array de números primos previamente calculados, organizado por ordem crescente com progressão geométrica de razão 2, através da qual a dimensão real a adoptar para a tabela de hash se faça por mera consulta a esse array. Usa-se o crivo de Eratosthenes. Pode-se então construir uma classe Prime, que contém como atributo o índice i do array, e com os seguintes métodos: Prime(n) – construtor invocado com o valor estimado para a dimensão da tabela afecta i com o índice do número primo aproximado por excesso. set(n) – Afectai com o índice do nº primo aproximado por excesso a n

operator prime_type – operador de conversão para prime_type que retorna o nº primo correspondente ao índice i next() – incrementa i e retorna o nº primo correspondente. get() – auxiliar para retornar o nº primo correspondente ao índice i FUNÇÃO hash_string ...

TEMPLATE DE FUNÇÕES HASH
Do template de tabelas hash consta como parâmetro-tipo a classe template objecto função hash que se pretenda adoptar, tomando por omissão a classe template de uso genérioco mostrada a seguir: template <class Key> struct Hash { size_t operator() (unsigned long x) const {return size_t(x); } }; Este template de classes objecto função toma como parâmetro o tipo da chave, tem uma versão básica para os tipos integrais (convertíveis em unsigned long) que se limita a retornar o valor do parâmetro e especializações para tipo char* e string. Na especialização para string C-style ou string, o operador chamada a função põe em execução a função hash_string() de uso genérico (por exemplo, uma das que se estudaram anteriormente) template<> struct Hash<char*> { size_t operator() (const char *s) const {return hash_string(s); } }; template<> struct Hash<string> { size_t operator() (const string &s) const {return hash_string(s.c_str());} };

TEMPLATE DE CLASSES HashTable
---> Parâmetros tipo: K é a chave de pesquisa FH é a classe função de hash. Como valor por omissão é instanciada a classe template Hash<K> V é a classe dos valores KFromV é um tipo de objecto função que infere K a partir de V Equal é um tipo objecto função que estabelece o critério de equivalência das chaves de pesquisa A é o tipo de allocator adoptado #define SUPER Container<V,A> template<class K, class FH=Hash<K>, class V=K, class KFromV = Identity>K,V>, class Equal = equal_to<K>, class A = allocator<V> > class HashTable:public SUPER { public: //<<Tipos>> IMPORT_TYPE(pointer); IMPORT_TYPE(const_pointer); IMPORT_TYPE(reference); IMPORT_TYPE(const_reference); IMPORT_TYPE(size_type) IMPORT_TYPE(allocator_type); typedef K key_type; typedef Equal key_compare; typedef FH hasher;

A estrutura Node (nó) definida nested e privada de HashTable tem um atributo next, do tipo apontador para Node, e um atributo data do tipo dos objectos de que a tabela de hash é contentora. O tipo apontador para Node é obtido a partir do tipo pointer do allocator de objectos Node. O tipo do allocator de objectos Node é obtido a partir do tipo other do template de classes rebind do allocator A, instanciado para a classe template rebind<Node>. protected: typedef KFromV kfromv_type; struct Node; typedef typename A::rebind<Node>::other Anode; typedef typename ANode::pointer NodePtr; typedef typename Anode::const_pointer ConstNodePtr; struct Node { V data; NodePtr next; }; A tabela de buckets é implementada num vector de apontadores para o primeiro nó da lista simplesmente ligada (cabeça da lista) typedef typename A::rebind<NodePtr>::other AnodePtr; typedef vector<NodePtr, AnodePtr> Table; typedef typename Table::iterator Titerator typedef typename Table::iterator TconstIterator; //<<Iteradores>> template <class P, class R, class TI, class NP< > class IT; template <class P, class R, class TI, class NP< > friend class IT;

//<<Atributos>>
//Apontador para função ou objecto função hash FH fh; //Apontador para função ou objecto função que verifica se duas chaves são equivalentes Equal equal; //Apontador para função ou objecto função que infer K a partir de V KFromV kFromV; //Número de buckets da tabela. O nº de buckets da tabela será um nº primo Prime prime; A constr; //Allocator para construir objectos v ANode alloc; //Allocator para aloja e desalojar nós size_type sz; //Nº de elementos contidos na tabela //Vector de estruturas listas simplesmente ligada Table buckets; //<<Métodos auxiliares>> ... //<<Interface Pública>> public: typedef IT<pointer, reference, Titerator, NodePtr> iterator //<<Construtores, destrutor e operador afectação>> //O construtor tem por parâmetro o nº de entradas da tabela e como opcionais: a função hash, a função de equivalência de chaves, a função para extracção da chave e o allocator. HashTable(size_type n=0, const FH &f=FH(), const Equal &e=equal(), const KFromV &kv=kFromV(), const A &a=A()) : fh(f), equal(e), kFromV(kv), prime(n), constr(a), alloc(a), sz(0), buckets(prime, NodePtr(), a) {}; HashTable(size_t n, const FH &f, const Equal &e, const A &a) : fh(f), equal(e), prime(n), constr(a), alloc(a), sz(0), buckets(prime, NodePtr(), a) {} HashTable(const HashTable&); //Construtor por cópia ~HashTable() {clear(); } //Destrutor HashTable &operator=(const HashTable&); //Afectação //<<Observadores>> hasher hash_funct() const {return fh; }

key_compare key_comp() const {return equal; } allocator_type get_allocator() const {return constr; } //<<Métodos Públicos size_t size() const {return sz;} bool empty() {return size()==0;} size_type bucket_count() const {return buckets.size(); } void resize(size_type n) {if (n>prime) {prime.set(n); expand(n); }} //<<Pesquisa, Inserção e Remoção>> //Pesquisa por chave. Se existir na tabela um elemento com a chave passada como parâmetro retorna um iterador para o elemento ou o past the end caso contrário. iterator find(const K &k); //Insere um elemento na tabela caso não exista. No membro second retorna true se o elemento não existia e false caso contrário. No membro first retorna o iterador para o elemento inserido ou para o elemento existente pair<iterator, bool> insertUni(const V&); //Inserção de um elemento na tabela iterator insertMulti(const V&); //Remove da tabela todos os objectos cuja chave seja igual à passada como parâmetro. Retorna o número de elementos removidos size_type erase(const K&); void clear(); //Remove todos os nós da tabela //<<Acesso por iterador>> iterator begin() {return iterator(buckets.begin(), buckets.end()); } iterator end() {return iterator(buckets, bucket_count()); }

Implementação dos Métodos de Pesquisa, Inserção e Remoção
Limitam-se a calcular sobre qual dos buckets devem invocar o método auxiliar homónimo e construir o respectivo valor de retorno. O valor retornado por cada um dos métodos depende da operação que for invocada sobre o contentor. Nas acções de inserção e remoção, o contador de nº de elementos é actualizado, tendo em conta o resultado da operação sobre o bucket. No método find(), o iterador retornado é construído tendo em conta o resultado da pesquisa no bucket e a entrada na tabela correspondente à chave: Pesquisa iterator find(const K &k) { size_type i=bucketNum(k); return iterator(buckets, i, find(buckets[i], k); } Por sua vez, o método auxiliar find() num bucket percorre os elementos da lista e termina a iteração quando encontrar um objecto com chave equivalente à da pesquisa ou atingir o fim do bucket NodePtr find(NodePtr header, const K &k) { NodePtr cur=header; for (;cur && !equal(k, KFromV(cur->data)); cur=cur->next); return cur; } Inserção pair<iterator, bool> insertUni(const V &v) { expand(size()+1); size_type i = bucketNum(kFromV(v)); pair<NodePtr, bool> r = insert<flase>(buckets[i], v); return make_pair(iterator(buckets,i,r.first), r.second); } Tal como na ABP, definiu-se um template de métodos auxiliares insert() com parâmetro-valor do tipo bool que, quando instanciado com o valor true, executa inserção múltipla (permite inserir múltiplos elementos com chaves equivalentes) e quando instanciado com o valor false executa exclusivamente inserções simples. template <bool MULTI> pair<NodePtr, bool> insert(NodePtr &header, const V &v) {

NodePtr curr = find(header, kFromV(v)); if(curr) { if (MULTI) { curr->next = newNode(curr->next, v); ++sz; return make_pair(curr->next, true); } return make_pair(curr, false); } ++sz; return make_pair(header = newNode(header, v), true); } O método insertMulti() é muito semelhante ao insertUni, só com true. Remoção size_type erase(const K &k) { return erase(buckets[bucketNum(k)], k); Com o método auxiliar erase() explicitado abaixo. As remoções numa lista simplesmente ligada (sem nó dummy) implicam, para além da destruição dos nós a remover, afectar o membro next dos nós anteriores ou o header da lista, com o endereço do nó seguinte ao nó removido. Isto requer, tal como na inserção, que ao longo da iteração de pesquisa seja mantida informação actualizada do apontador para o nó anterior ao nó corrente: size_type erase(NodePtr &header, const K &key) { NodePtr prev = NULL, curr = header; size_type count = 0; while (curr) if (equal(key, kFromV(curr->data))) { curr = curr->next; if(prev) {deleteNode(prev->next); prev->next=curr; } else {deleteNode(header); header = curr; } ++count; } else if (count > 0) break; else {prev = curr; curr = curr->next; } sz-=count; return count; }

CONTENTORES ASSOCIATIVOS HASH
As definições dos templates de classes HashMap, HashSet, HashMultiMap e HashMultiSet, suportam-se no template de classes HashTable analisado anteriormente, reduzem-se ao seguinte: #define SUPER \ HashTable<K, FH ,pair<K,T>, FirstOfPair<K,T> ,Cmp, A> template <class K, class T, class FH=Hash<K>, class Cmp=equal_to<K>, class A=allocator<pair<K,T>> class HashMap: public SUPER { public: IMPORT_TYPE(iterator); IMPORT_TYPE(value_type); IMPORT_TYPE(size_tyep); typedef T Mapped_type; HashMap(size_type n=0, const FH &h=Fh(), const Cmp &c=Cmp(), const A &a=A()) : SUPER(n,h,c,a) {} pair<iterator,bool> insert(const value_type &value) {return insertUni(value); } T &operator[] (const K &k) {return insert(pair<K,T>(k,T())).first->second; }}; Os outros são semelhantes e podem ser vistos na página 521 do livro.