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. instanciações de objectos objectos temporários e locais. Os problemas são o aumento significativo de instanciações 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 po por có cópia String (c (char*) Construtor a partir de de um uma st string CC-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 const char *c_str *c_str(( ) Retorna Retorna a string string C-styl C-stylee 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&) a ppen d 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 Ca da obj objec ecto to da clas classe se Stri String ng terá terá como como atrib atribut utos os um ap apont ontado adorr pa para ra a repre represe senta ntaçã ção, 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 reserv reservaa de espaç espaçoo ado adopta ptamo moss dime dimens nsões ões mú múltltip ipla lass de DIM_MI DIM_MIN N (por (por po potên tênci cias as de 2), 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 repre represe senta ntaçã çãoo stri string ng C-st C-styl ylee aqu aquan ando do da exec execuç ução ão do cons constru trutor tor po porr cópi cópiaa e do op oper erado ador r 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é métod todos os são são do enc encarg argoo de Srep Srep.. A cada cada repr repres esent entaç ação ão de Stri String ng fica fica un univ ivoc ocam amen ente te 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 af afec ecta tado doss po porr cópi cópiaa freq freque uent ntem emen ente te ma mass nã nãoo são são de depo pois is alte altera rado dos. s. No Noss caso casoss como como concate con catenaç nação ão em string stringss ou operador operadores es aritmé aritmétic ticos os em cla classe ssess num numéri éricas cas o idioma idioma pouc poucoo beneficia pois os objectos são muitas vezes alterados. Entã Então, o, segui seguindo ndo o nos nosso so ex. ex. vaivai-se se def defin inir ir um umaa nov novaa clas classe se StrT StrTmp mp (str (strin ingg te temp mpor orár ária ia)) 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 alojamento dinâmico reside nos operadores operadores que retornam retornam objectos objectos por valor, criando criando localmente localmente buffers de resultados. Por Por exem exempl ploo a conc concate atenaç nação ão de ma mais is que 2 ob obje ject ctos os stri string ng impl implic icaa a cria criaçã çãoo de vári vários os 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
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 const unsigned 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 //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) &p) {assert (DIM!=size); data[size++]=p; data[size++]=p; } void pop () {assert(size>0); --size; } }; Ex. de declaração de objectos: Stack stk1; Stack stk2;
O códi código go dos mé métod todos os da ge gener neral alid idad adee das clas classe sess de conte contento ntore ress te tem m a pa parti rticu cula lari ridad dadee 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 class Xpto; typedef Xpto MyXpto; MyXpto ob1, ob2, ob3; - Ex. definição de um novo nome para a classe Stack typedef Stack CharStack; Stac Stackk def defin inid idoo ant antes es nã nãoo tem tem nat natur urez ezaa dinâm dinâmic ica. a. Ge Gera ralm lment entee te tem m send sendoo supor suportad tadaa por contentores sequenciais.
3.2.2. Templates de Funções Define uma família de funções. Ex: template void swap(T swap(T &a, &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(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 Array { T *v; *v; //Apon //Apontad tador or pa para ra a repr repres esen entaç tação ão unsigned di dim; //Capacidade da da re representação unsigned size; //Número de elementos contidos Array (const Array&); //Impossibilitar a construção por cópia void oerator= oerator=(const (const Array&) Array&) //Impos //Impossibil sibilitar itar afectação afectação public: explicit Array(unsigned d); //Iniciado com dim=d ~Array () {delete [] v; } T &operator[](unsigned n) {assert (n void sort(Array&; Template ostream &operator<<(ostream&, const Array&); Quan Qu ando do um me mebr broo de um temp templa late te de clas classe sess é de defifini nido do fo fora ra da sua sua clas classe se,, de deve ve ser ser explicitamente declarado como template. Ex: template Array::Array(unsigned d) {v=new t [dim = d]; sz=0, } O tem templ plat atee de fun funçõ ções es glob globai aiss def defin inee-se se com com sint sintax axee seme semelh lhant antee ao te temp mpla late te de fun funçõ ções es membro: ex. com bubble-sort
template void sort(Array &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> List {/*...+/} Isto significa que se podem instanciar classes template List, com um ou os dois argumentos template: List list1 List>double, AllocPool> list2 Quando declaramos declaramos todos os parâmetros por omissão. a lista lista de argumentos da classe classe templa template te pode ser vazia. Ex: template class Stack {/*...+/}; Stack<> *p; //OK, apontador para Stack Stack *q //Erro: Imprescindível 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 List {/* ... */ }; template class C = List> class map { struct pair { C key; C 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 //Chaves do tipo List e dados do tipo List Map //Chaves do tipo Stack e dados do tipo Stack 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 ambiguidades em expressões nas quais o compilador não tenha possibilidade de inferir se um dado nome de um memb me mbro ro de um argum argumen ento to tem templ plate ate,, corr corres espo ponde nde ou não a um tipo tipo.. type typenam namee espe especi cififica ca 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 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; typ typedef typ typen enaame A::di ::diffferen encce_type type dif diffe ferrence_ nce_ttype; t y pede f 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 int compare(const T2&) {/*...*/} // Template de estruturas nested template struct rebind {typedef X other; }; //... }; Também pode ser definido fora da classe: template template int X::compare(const T2 &s) {... se dentro tiver: template int compare(const T2&); 3.2.7. Especialização de Templates Podemos Podemos querer que uma dada classe template tenha comportamento comportamento diferente quando tiver um tipo de argumento específico. Seja por ex. um template vector: template class vector {/*...*/} no qual pretendemos que a classe template «vector tenha um comportamento diferenciado para economia de espaço de representação. Então temos de, além de definir definir o template primário, explicitar explicitar também a definição definição especializada especializada (sempre depois): template<> vector {/*...*/} 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 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 7 typedef float UserType; }; template<> struct UserTraits { typedef int UserType; }; Seja o template de funções associado: template typename UserTarits::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 esgota-se com a definição definição de classes classes de objectos 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çõ relações es de herança e usam usam polimorfismo, conc concei eito toss supo suport rtad ados os pel pelos os me meca cani nism smos os de derivação e de métodos virtuais do C++ 3.3.1. Relação de Herança
Numa da Numa dada da ap apli lica caçã ção o co comp mple lexa xa o pr prog ogra rama mado dorr de deve ve en enco cont ntra rarr cl clas asse sess co com m propriedades comuns, comuns, de modo a que a definição 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, privado, oculto na classe B. public: void set10() {x=10;} void show() {cout << x;} }; class B: public 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 antecedida por 2 pontos e separados por vírgulas, vírgulas, segue-se a lista lista de iniciação, iniciação, de que constam 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 constr con strutor utor com parâme parâmetros tros (excep (excepção ção ao con constr struto utorr por cópia), cópia), o con constr strutor utor impli implicit citamen amente te 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 int yy=10) {x=xx; y=yy; 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, B(v3, v4), d(v5) {} D() {d=5;} //zz é iniciado 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 preservar o encapsulamen encapsulamento to dos membros especificados especificados como private, private, também não são acessíveis aos métodos das suas classe derivadas. Os espe especif cifica icados dos com comoo protect protected ed man mantêm têm-se -se ina inaces cessív síveis eis às enti entidade dadess exte externas rnas mas são acessíveis directamente pelos métodos das classes derivadas. Ex: class B { int aa; //Zona apenas apenas acessíve acessívell pelos pelos métodos métodos de B prot protec ecte ted: d: // //Zo Zona na aces acessí síve vell ao aoss de deri riva vado doss de B int bb; public: //Zona pública int cc; }; class D : public B { public: int dd; void f() { aa=0; //Erro. membro membro aa é privado da classe B. bb=1; //OK cc=2; / /o K } }; void main () { B b; D d; b. b.aa aa= =0; b.bb=1; b.cc=2; b.dd=3; d.aa=4; d.bb=5; d.cc=6 d.dd=7; }
//E //Erro. rro. aa é pri privvado ado de de B (não (não po pode de ser ser ace acedi dido do de main) ain) //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 existe na classe classe e poderá ser acedido indirectamente 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 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::ge d.B::getn() tn();; //OK. //OK. n aced acedido ido po porr B através através do do seu mé método todo getn( getn()) cout << d.getn(); //OK b.display(); //OK d.display(); //OK B *ptB=&d; //Coerção implícita de apontadores pt ptB B ---> di displa play(); //I //Invoca di display( ay() de 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: D::(char*) oc oculta B: B::h(int). d.w(); //Ok. invoca D::w(). d.B::h(7); //Ok. In Invoca B: B::h(int) d.B::w(); //Ok. Invoca B::w() d.h(“Hellow”) d.h(“Hellow”);; //Ok. Invoca D::h(cha D::h(char*). r*). }
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()”; “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 cout << pt pt--->f >f() () << endl endl;; //E //Escre screve ve:: “D:: “D::f( f()” )”.. cout cout << pt pt--->g >g() () << endl endl;; //E //Escre screve ve “B.. “B..g( g()” )”.. } Na redefinição (overrride) (overrride) de um método declarado como virtual, virtual, é preeciso 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 Allo Alloca cator tor – para para isol isolar ar os conte contento ntores res do det detal alhe he dos mo model delos os de aloj alojam amen ento to de me memó móri riaa utilizados e para separar as acções de alojamento das de iniciação de objectos. Resumindo: 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. métodos públicos públicos a As normas ANSI/ISO não impõem implementação específica mas sim os métodos 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 eleme elementos ntos contidos têm um único elemento sucessor sucessor e um único antecessor. permitem 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[ a[n] n] Estr Estrut utur uraa 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. vector or em an anel el – vari varian ante te de fila fila de espe espera ra com com capa capaci cida dade de cons consta tant ntee ring buffer buffer – vect preesta preestabel beleci ecida da FIFO. FIFO. Obj Object ectos os ins inseri eridos dos no fim da fila fila e removi removidos dos do início início,, com temp tempoo 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. 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: Big-Oh: t(n) é O(log n). 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) t( n) é O (n lo log g n)) n)) - tem tempo po 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 fim do array array,, ma mass qu quand andoo nã nãoo há ma mais is espaç espaçoo dob dobra ra dinam dinamic icam ament entee este, este, de fo form rmaa 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 permitem percorrer o conjunto 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]; &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) ++i) *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 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 desempenho O(n). Se lhe forem passados passados iteradores iteradores de acesso aleatório 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 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 exemplo quando um algoritmo 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 invocação promove comportamentos comportamentos diferentes da forma abreviada, abreviada, pois limita-se 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 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 *operator new (size_t, (size_t, void *p) {return {return p; } e limita-se limita-se a retornar o próprio 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 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 templat templatee de contento contentorr envolve sempre a definição 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 template> 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 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)i) {return (*begin() (*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 allo alloca cato tor; r; //al //allo loca cato tor< r T> por por omis omissã sãoo 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 vector::vector(const a &a1) : allocator(a1), start(), finish(), endOfStorage() {} template vector::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 vector::iterator vector::uninitializedFill(iterator dest, size_type n, const T &x) { for(; n; --n, ++dest) allocator.construct(dest, x); return dest; } template vector::vector(const vector &x) : allocator(x.allocator) { start = allocator.allocate(x.size()); finish = uninitializedCopy(x.begin(), x.end(), start); endOfStorage = finish; } template vector::iterator vectro::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 vector::~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 vector::iterator vector::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()) (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 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 vector::iterator vector::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 void vector::clear() {destroy(begin(), end()); finish = start; } 4.5.1.8. Método swap() template void vector::swap(vector &vx) { if (this != &X) if (allocator (allocator == x. allocator) allocator) { ::swap(start, x.start); ::swap(finish, x.finish); ::swap(endOfStorage, x.endOfStorage); } else ::swap(*this, x); template inline void swap(U &a, &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 template> class list: public SUPER { public: IMP IM PORT_TYPE(size_ e_ty type pe)); IMP IM PORT_TYPE(diff ffeeren encce_type type)); IMP IM PORT_TYPE(refer ference) nce);; IMP IM PORT_TYPE(con onsst_ t_rref efeeren encce); 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::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 Nod odeePtr he heaad; //A //Aponta ntado dorr pa parra o nó sentin tinela (dum umm my) 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 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() size() const {return sz;} sz;} bool empty() {return sz==0;} const A &get_allocator() const {return constr;} //Acesso aos elementos reference front() {return *begin(); } reference back() {return *(--end()); *(--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 void insert(iterator p, InputIter first, InputIter last); void push_front(const T &e) {insert (begin(), (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 list::NodePtr list::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 inline list::list(const list::list(const A &a1) : constr(a1), alloc(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 list::NodePtr listnewNode(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 list::iterator list::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 template void list::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 void list::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 list::iterator list::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 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 template > class deque : public SUPER { public: IMPOR IMPORT_ T_TY TYPE PE(p (poi oint nter) er);; IMPORT IMPORT_T _TYP YPE( E(co cons nst_p t_poi ointe nter; r; IMPORT_TYPE IMPORT_TYPE(refere (reference); nce); IMPORT_TYPE IMPORT_TYPE(const_ (const_referenc reference); e); IMPORT_TYPE IMPORT_TYPE(siz (size_type); e_type); IMPORT_TYPE IMPORT_TYPE(differ (difference_ty ence_type); pe); private: //<> static const size_type DIM_MAP_MIN = 5; static const size_type DIM_BLOCK=512/sizeof(T); //<> typedef typename A::rebind::other AMap; typedef typename AMap::pointer MapPointer; template class IT {...}; public: //<> typedef IT iteartor; //<> private: A blockAlloc; AMap mapAlloc; MapPointer map; size_type mapSize; iterator start, finish; //<> public: //<> //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); //<> //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 template > class RingBuffer : public SUPER { public: IMPOR IMPORT_ T_TY TYPE PE(p (poi oint nter) er);; IMPORT IMPORT_T _TYP YPE( E(co cons nst_p t_poi ointe nter); r); IMPORT_TYPE IMPORT_TYPE(refere (reference); nce); IMPORT_TYPE IMPORT_TYPE(const_ (const_referenc reference); e); IMPORT_TYPE(size_type); //<> typedef CircCount difference_type; private: template class IT {/*...*/}
Interface Pública
public: //<> typedef IT iterator; typedef IT const_iterator; //<> RingBuffer(const A &a=A()); RingBuffer(const RingBuffer&); ~RingBuffer(); RingBuffer &operator=(const RingBuffer &); //<> //Acesso às dimensões size_type size() const {return finish-start;} size_type max_size() max_size() const {return DIM;} bool empty() const {return start==finish; start==finish; } const A &get_allocator() const {return allocator;} //Acesso por iterador iterator begin() const {return start;} 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];} //<> void push_back(const T&); void pop_front(); //<> void clear() {destroy(start, finish); finish); finish=start} finish=start} private:
//<>
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 Embor Emboraa nã nãoo seja sejam m comp compon onent entes es sta stand ndard ard da bibl biblio iotec tecaa C++, C++, a fam famílília ia do doss conte contento ntores res associ associati ativos vos sup suporta ortam-s m-see em árvores árvores bin binári árias as de pes pesqui quisa sa bal balanc anceada eadass e o ada adaptad ptador or 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 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 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 template> class TreeBase : public SUPER { protected: //<> struct Node;
typedef typename A::rebind::other ANode; typedef typename ANode::pointer NodePtr; typedef typename Anode::const_pointer ConstNodePtr; struct Node { T values; NodePtr Left; NodePtr right; NodePtr parent; }; //< class IT; template friend class IT; public: IMPORT_TYPE IMPORT_TYPE(refere (reference); nce); IMPORT_TYPE IMPORT_TYPE(const_ (const_referenc reference); e); IMPOR IMPORT_ T_TY TYPE PE(p (poi oint nter) er);; IMPORT IMPORT_T _TYP YPE( E(co cons nst_p t_poi ointe nter); r); IMPORT_TYPE IMPORT_TYPE(siz (size_type); e_type); IMPORT_TYPE IMPORT_TYPE(differ (difference_ty ence_type); pe); typedef IT iterator; typedef IT const_iterator; protected: //<> A constr; //Allocator para construir objectos T ANode alloc; size_type sz; NodePtr dummy; NodePtr root; //Métodos auxiliares declarados na parte protegida //<> //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); //<> //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 void preorder(NodePtr r, Visit) const; template void inorder(NodePtr r, Visit) const; template void postorder(NodePtr r, Visit) const; //<> //Se MULTI for true executa o insertMulti(), se falso insertUni() template pair 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 static NP rightMost(NP r); template 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); //<
//Interface Pública public: //<> A get_allocator() const {return constr); } //<> /*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 void preorder(Visit visit) const {preorder(root, visit); } template void inorder(Visit visit) const {inorder(root, visit); } template void postorder(Visit visit) const {postorder(root, visit); } //<left; } iterator end() {return dummy; } //<> //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 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 Perc Percorr orrer er um umaa árvo árvore re cons consis iste te em visi visitar tar todo todoss os seus seus nós por um umaa de deter termi minad nadaa ordem 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 (preo (preorde rder) r) visi visita ta o nó raiz, raiz, dep depoi oiss perco percorr rree a subár subárvo vore re esqu esquerd erdaa 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 template void TreeBase::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 void displayValue(const T &t) {cout << t << ‘ ‘;} template void TreeBase::display() const {inorder(displayValue()); }
5.2.1.2. Pesquisa A pesquisa numa árvore balanceada balanceada tem eficiência 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 recursivo terminal, terminal, é facilmente facilmente convertível convertível à forma iterativa, c om 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(evalue) 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 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 insertUni(const T &e) { NodePtr r; //Apontador para o novo nó if root(==NULL) //Árvore vazia r=ro r=root ot=d =dum ummy my-> ->lleft= eft=du dumm mmyy-> >righ right= t=ne newN wNod ode( e(du dum mmy my,, e); // //iinici niciaa roo oott com 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(r,false);
} ++sz; return pair(r, true); } Os métodos auxiliares insertLeft() e insertRight() realizam a inserção, afectando o membro left ou righ rightt do nó pa pass ssad adoo como como pa parâ râme metr troo com com o en ende dere reço ço de um no novo vo nó nó.. Prov Provid iden enci ciam am 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 itera iterado dorr en end( d()) caso caso nã nãoo exis exista ta.. Se exis existitirr invo invoca ca repet repetid idam amen ente te o mé métod todoo eras erase( e(i) i),, incrementando o iterador, enquanto o valor apontado pelo iterador for igual ao valor a remover (ordenada por valores ???) template TreeBasesize_type TreeBase::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 void TreeBase::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é métod todoo que vamo vamoss ana analilisa sarr permi permite te regen regener erar ar o ba bala lanc ncea eame mento nto de um umaa árvo árvore re,, com com desem des empen penho ho pou pouco co efic eficie iente nte O(n) O(n),, fact factoo qu quee não o to torn rnaa recom recomend endáv ável el para para ap aplilica caçõ ções es genéric gené ricas. as. A sol solução ução para para garanti garantirr a permanê permanênci nciaa do bal balanc anceam eamento ento das árvores árvores,, frente 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 void TreeBase::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 void TreeBase:: 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 contento res standard 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
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 stack { public: typedef type def typ typenam enamee Sequen Sequence:: ce::val value_ ue_type type val value_t ue_type ype;; type typede deff type typena name me Sequ Sequen ence ce:: ::si size ze_t _typ ypee size size_t _typ ype; e; type typede deff typen typenam amee Seque Sequenc nce: e::r :ref efer eren ence ce refe refere renc nce; e; typed typ edef ef typen typenam amee Seque Sequenc nce:: e::co cons nst_r t_refe eferen rence ce cons const_ t_ref refer erenc ence; e; t y pede f Sequence container_type; //<> protected: Sequence c; //<> public: stack() : c() {} explicit stack(const Sequence &s) : c(s) {} bool empty() cons constt {ret {retur urnn c.emp c.empty ty() ();} ;}
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
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 Cmp=less> class priority_queue { public: typedef type def typ typenam enamee Sequen Sequence:: ce::val value_ ue_type type val value_t ue_type ype;; type typede deff type typena name me Sequ Sequen ence ce:: ::si size ze_t _typ ypee size size_t _typ ype; e; type typede deff typen typenam amee Seque Sequenc nce: e::r :ref efer eren ence ce refe refere renc nce; e; typed typ edef ef typen typenam amee Seque Sequenc nce:: e::co cons nst_r t_refe eferen rence ce cons const_ t_ref refer erenc ence; e; t y pede f Sequence container_type; //<> protected: Sequence c; Cmp cmp; //<> 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 R.Bayer desenvolveu estrutura estrutura arborescente de páginas, com elevado nº de chaves de pesqui pes quisa sa po porr pág págin ina. a. Mul Multitide desc scen enden dentes tes,, porta portanto nto e ga garan rantem tem inse inserç rção ão e remo remoçã çãoo com 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, balanceadas, se providenciar providenciarmos mos 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 - Ca Caso so cont contrá rári rio, o, prom promov ovee a sua sua inse inserç rção ão orde ordena nada da na pá pági gina na fo folh lha. a. Se a fo folh lhaa fica ficar r sobrecarregada, realiza split dessa folha e invoca recursivamente o algoritmo de inserção sobre a página ascendente.
CAPÍTULO 7 – CONTENTORES ASSOCIATIVOS 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 supo suporte rte dos con content tentore oress associ associati ativos vos,, pel peloo que vam vamos os tam também bém defi definir nir essa essa ext extens ensão ão à biblioteca standard. As tabelas de hash são de utilização recomendável, por exemplo, nos casos em que o objectivo a atingi atingirr não é reduz reduzir ir o nº de coli colisõ sões es de chav chaves es ao míni mínimo mo,, ma mass sim sim repar repartitirr por vários 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.
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, 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 Construtor recebem como argumentos um objecto objecto Cmp, que estabelece o modo de comparação comparação dos elementos, e um allocator. Por omissão dos argumentos é usado o objecto função Cmp e allocator allocator A, construídos construídos com o construtor construtor sem parâmetros parâmetros dos tipos indicados indicados nos parâmetros parâmetros template.
template struct FirstOfPair { const K &operator() (const pair &p) const {return p.first;} }; #define SUPER RBTree, FirstOfPair, Cmp, A> template , class A = allocator > 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 insert(const value_type &value) {return insertUni(value); }
T &operator[](const K &key) {return insert(value_type(key, T())).first->second; }
}; #undef SUPER
void main() { typedef map 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; }
É muito parecido com o map. Tentar fazer.
set
A diferença é que o método acrescentado, à RBTree, insert() delega no método da RBTree insertMulti() da RBTree. Tentar fazer.
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 Position; typedef pair Value; typedef multimap Table; typedef Table::iterator Iterator; table table; string line; Key word; unsigned column; for (unsigned numLine=1; numLine=1; getline(cin, line); line); ++numLine) { //Stream de entrada associado a uma string C-style. istrstream is(line.c_str()); while (is (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. palavras. A solução solução proposta é ler uma linha, linha, invocando invocando a função global getline() e posteriorme posteriormente nte instanciar um istrstream 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 a[h (k)] )] ap apont ontaa pa para ra um nó da lista lista qu quee nã nãoo corre corresp spon onde de à chav chavee pes pesqu quis isada ada ==> ==> tes testes tes 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 pe pesq squi uisa sa.. De Deve ve po pois is ap apre rese sent ntar ar prob probab abililid idad adee un unififor orme me pa para ra to todo doss os elem elemen ento toss 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 aproximar de 1, aumenta aumentarr o tamanho do array para o nº primo que seja o mais próximo 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 núm eros primos primos previa previament mentee cal calcul culados ados,, organiz organizado ado por ordem ordem cresce crescente nte com progres progressão sã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
op oper erat ator or prim prime_ e_ty type pe – op oper erad ador or de conv conver ersã sãoo pa para ra prim prime_ e_ty type pe qu quee reto retorn rnaa o nº prim primoo 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 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 { size_t operator() (const char *s) const {return hash_string(s); } }; template<> struct Hash { 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 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 template, class V=K, class KFromV = Identity>K,V>, Ide ntity>K,V>, class Equal = equal_to, class A = allocator > class HashTable:public SUPER { public: //<> IMPOR IMPORT_ T_TY TYPE PE(p (poi oint nter) er);; IMPORT IMPORT_T _TYP YPE( E(co cons nst_p t_poi ointe nter); r); IMPORT_TYPE IMPORT_TYPE(refere (reference); nce); IMPORT_TYPE IMPORT_TYPE(const_ (const_referenc reference); e); IMPORT IMPORT_TY _TYPE( PE(siz size_ty e_type) pe) IMPORT IMPORT_TYP _TYPE(a E(allo llocat cator_t or_type) ype);; t y pede f 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. protected: typedef KFromV kfromv_type; struct Node; typedef typename A::rebind::other Anode; typedef typename ANode::pointer NodePtr; typed typ edef ef typ typena ename me Anode Anode::c ::con onst_ st_po poin inter ter Co Cons nstNo tNodeP dePtr; tr; 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::other AnodePtr; typedef vector Table; typedef typename Table::iterator Titerator typedef typename Table::iterator TconstIterator; //<> template class IT; template friend class IT;
//<>
//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; //<> ... //<> public: typedef IT iterator //<> //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_ HashTable(size_type type n=0, const FH &f=FH(), &f=FH(), const Equal &e=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 //Destrutor ~HashTable() {clear(); } HashTable &operator=(const HashTable&); //Afectação //<> hasher hash_funct() const {return fh; }
key_compare key_comp() const nst {retur turn equ quaal; } allocator_type get_allocator() const {return constr; } //<prime) {prime.set(n); expand(n); }} //<> //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. contrário. No membro first retorna o iterador iterador para o elemen elemento to inserido ou para o elemento existente pair 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 //<> 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 insertUni(const V &v) { expand(size()+1); size_type i = bucketNum(kFromV(v)); pair r = insert(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 pair 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 defi definiç nições ões dos tem templa plates tes de cla classe ssess HashMap HashMap,, HashSe HashSet, t, HashMul HashMultiMa tiMapp e HashMul HashMultiS tiSet, et, suportam-se no template de classes HashTable analisado anteriormente, reduzem-se ao seguinte: #define SUPER \ HashTable, FirstOfPair ,Cmp, A> temp templa late te K>,, clas classs Cmp= Cmp=eq equa ual_ l_to to, >, clas classs A=allocator> class HashMap: public SUPER { public: IMPORT_TYPE(iterator); IMPORT_TYPE(value_type); IMPORT_TYPE(size_tyep); typedef T Mapped_type; HashMap(size_t HashMap(size_type ype n=0, const FH &h=Fh(), &h=Fh(), const Cmp &c=Cmp(), &c=Cmp(), const A &a=A()) &a=A()) : SUPER(n,h,c,a) {} pair insert(const value_type &value) {return insertUni(value); } T &operator[] (const K &k) {return insert(pair(k,T())).first->second; }}; Os outros são semelhantes e podem ser vistos na página 521 do livro.