Mudanças entre as edições de "PRG29003: Introdução a Listas"

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar
 
(38 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 31: Linha 31:
 
= A lista da STL =
 
= A lista da STL =
  
A STL apresenta duas estruturas de dados relacionadas com armazenamento de sequências de dados, os quais podem ser acessados randomicamente:
+
A STL apresenta a estrutura de dados [http://www.cplusplus.com/reference/list/list/ list] para armazenamento de sequências de dados, os quais podem ser acessados randomicamente. Em uma lista, dados podem ser adicionados e removidos de qualquer posição com eficiência, porém são acessados de forma iterativa (sempre a partir do início ou final da lista). Em ''list'', cada dado ocupa uma área de memória sob medida, e para formar uma sequência essas áreas de memória são encadeadas (ligadas). Como consequência, os dados não ficam contíguos em memória. Por exemplo, supondo que tenha sido criada uma lista chamada ''numeros'', e nela tenham sido adicionados os números 10, 20, 30 e 40 (nessa ordem), o armazenamento desses números em memória poderia ser este:
* [http://www.cplusplus.com/reference/vector/vector vector]: um vetor dinâmico em que dados podem ser adicionados e removidos de qualquer posição (porém com certo custo computacional), e também acessados diretamente (referenciados por suas posições) com eficiência
 
* [http://www.cplusplus.com/reference/list/list/ list]: uma lista em que dados podem ser adicionados e removidos de qualquer posição com eficiência, porém acessados de forma iterativa (sempre a partir do início ou final da lista)
 
  
  
Ambas estruturas de dados podem ser usadas para armazenar conjuntos de dados em sequência. As diferenças entre elas dizem respeito a como os dados estão de fato armazenados na memória do programa, e isso influencia a eficiência das operações dessas estruturas de dados.
+
<br>[[imagem:prg2-Lista.png]]<br>''Uma lista com alguns dados''
  
  
Em ''vector'', os dados estão sempre contíguos em memória. Um ''vector'' usa uma área de memória capaz de guardar a quantidade de dados armazenados, estando os dados gravados sequencialmente ali dentro. A figura a seguir mostra como um vector usa memória para armazenar dados.<br>[[imagem:Prg2-Vector.png]]<br>''Um vector com alguns dados armazenados''
+
Em geral, ''list'' é adequada quando a quantidade de dados a serem armazenados é variável e desconhecida, e quando dados precisam ser inseridos e removidos de qualquer posição. Os pontos listados a seguir buscam esclarecer melhor o que está em jogo.
 
+
* Não é necessária uma área de memória contígua: como dados são armazenados dinamicamente, eles podem ocupar áreas de memória arbitrárias, e não há nenhuma relação entre a localização dos dados em memória e sua ordem na lista.
 
 
Em ''list'', cada dado ocupa uma área de memória sob medida, e para formar uma sequência essas áreas de memória são encadeadas (ligadas). Como consequência, os dados não ficam contíguos. <br>[[imagem:prg2-Lista.png]]<br>''Uma lista com alguns dados''
 
 
 
 
 
 
 
<!--Do ponto de vista de uma estrutura de dados usada em programas, uma lista encadeada se aparenta com estes exemplos:
 
 
 
[[imagem:prg2-2016-2-Lista1.jpg]]
 
 
 
 
 
Uma lista possui algumas características:
 
* Os dados são armazenados dinamicamente, por isso a quantidade total de memória usada para a lista depende da quantidade de dados nela armazenados (compare isso com um vetor ou matriz).
 
* A lista não precisa ocupar uma área de memória contígua: como dados são armazenados dinamicamente, eles podem ocupar áreas de memória arbitrárias, e não há nenhuma relação entre a localização dos dados em memória e sua ordem na lista (novamente compare isso com um vetor ou matriz).
 
 
* Não é possível indexar os dados, por isso para acessar um dado deve-se obrigatoriamente procurá-lo a partir do início ou fim da lista, seguindo cada sucessor ou antecessor até chegar àquele procurado.
 
* Não é possível indexar os dados, por isso para acessar um dado deve-se obrigatoriamente procurá-lo a partir do início ou fim da lista, seguindo cada sucessor ou antecessor até chegar àquele procurado.
* Para adicionar um dado, basta modificar a referência ao sucessor do dado que deve antecedê-lo na lista. Assim, não é necessário ''"empurrar"'' os dados seguintes para frente (como seria o caso de um vetor).
+
* Acrescentar um dado implica modificar a referência ao sucessor do dado que deve antecedê-lo na lista. Assim, não é necessário ''"empurrar"'' os dados seguintes para frente.
* Para remover um dado é a mesma coisa: basta modificar a referência de seu antecessor. Assim, não é necessário ''"deslocar pra trás"'' os dados seguintes (como seria o caso de um vetor).
+
* Para remover um dado é a mesma coisa: basta modificar a referência de seu antecessor. Assim, não é necessário ''"deslocar pra trás"'' os dados seguintes.
-->
 
 
 
 
 
Essas estruturas de dados possuem algumas operações em comum, como mostrado na tabela a seguir.
 
  
{| border=1
 
!Descrição
 
!vector
 
!list
 
|-
 
|Adicionar um dado ao final ||[http://www.cplusplus.com/reference/vector/vector/push_back/ push_back] || [http://www.cplusplus.com/reference/list/list/push_back/ push_back]
 
|-
 
|Adicionar um dado no início || ?? || [http://www.cplusplus.com/reference/list/list/push_front/ push_front]
 
|-
 
|Insere um dado em uma determinada posição ||[http://www.cplusplus.com/reference/vector/vector/insert/ insert] || [http://www.cplusplus.com/reference/list/list/insert/ insert]
 
|-
 
|Remove um ou mais dados a partir de uma determinada posição ||[http://www.cplusplus.com/reference/vector/vector/erase/ erase] || [http://www.cplusplus.com/reference/list/list/erase/ erase]
 
|-
 
|Acessa o dado que está no início ||[http://www.cplusplus.com/reference/vector/vector/front/ front] ||[http://www.cplusplus.com/reference/list/list/front/ front]
 
|-
 
|Acessa o dado que está no final ||[http://www.cplusplus.com/reference/vector/vector/back/ back]|| [http://www.cplusplus.com/reference/list/list/back/ back]
 
|-
 
|Obtém a quantidade de dados armazenados ||[http://www.cplusplus.com/reference/vector/vector/size/ size] ||[http://www.cplusplus.com/reference/list/list/size size]
 
|-
 
| Remove todos os dados (esvazia)||[http://www.cplusplus.com/reference/vector/vector/clear/ clear] || [http://www.cplusplus.com/reference/list/list/clear/ clear]
 
|}
 
 
 
Abaixo seguem exemplos de uso de algumas operações de ''vector'' e ''list'':
 
 
{| border=1
 
!vector
 
!list
 
|-
 
|<syntaxhighlight lang=c++>
 
#include <cstdlib>
 
#include <vector>
 
#include <iostream>
 
#include <string>
 
  
using namespace std;
+
As operações elementares de ''list'' são:
 +
* [http://www.cplusplus.com/reference/list/list/push_back/ push_back]: Adicionar um dado ao final
 +
* [http://www.cplusplus.com/reference/list/list/push_front/ push_front]: Adicionar um dado no início
 +
* [http://www.cplusplus.com/reference/list/list/pop_front/ pop_front]: Remover um dado do início
 +
* [http://www.cplusplus.com/reference/list/list/pop_back/ pop_back]: Remover um dado do final
 +
* [http://www.cplusplus.com/reference/list/list/insert/ insert]: Inserir um dado em uma determinada posição
 +
* [http://www.cplusplus.com/reference/list/list/erase/ erase]: Remover um ou mais dados a partir de uma determinada posição
 +
* [http://www.cplusplus.com/reference/list/list/front/ front]: Acessar o dado que está no início
 +
* [http://www.cplusplus.com/reference/list/list/back/ back]: Acessar o dado que está no final
 +
* [http://www.cplusplus.com/reference/list/list/size/ size]: Obter a quantidade de dados armazenados
 +
* [http://www.cplusplus.com/reference/list/list/clear/ clear]: Remover todos os dados (esvaziar)
  
void mostra_vetor(vector<string> & v) {
 
    // itera o vetor
 
    for (auto & dado: v) {
 
      cout << dado << ",";
 
    }
 
    cout << endl;
 
}
 
 
int main(int argc, char** argv) {
 
    // cria um vector de string
 
    vector<string> nomes;
 
   
 
    // anexa três dados ao final do vector
 
    nomes.push_back("manuel");
 
    nomes.push_back("maria");
 
    nomes.push_back("bilica");
 
   
 
    // mostra comprimento e conteúdo do vector
 
    cout << "Comprimento: " << nomes.size() << ", dados: ";
 
    mostra_vetor(nomes);
 
  
    // insere dado no início do vector (ineficiente ...)
+
Abaixo segue um exemplo de uso de algumas operações de ''list'':
    nomes.insert(nomes.begin(), "maneca");
 
    cout << "Comprimento: " << nomes.size() << ", dados: ";
 
    mostra_vetor(nomes);
 
  
    // remove dado do início do vector (ineficiente...)
+
<syntaxhighlight lang=c++>#include <cstdlib>
    nomes.erase(nomes.begin());
 
    cout << "Comprimento: " << nomes.size() << ", dados: ";
 
    mostra_vetor(nomes);
 
   
 
    // ao final, vector é automaticamente destruído, e a memória utilizada
 
    // é liberada
 
    return 0;
 
}</syntaxhighlight> || <syntaxhighlight lang=c++>#include <cstdlib>
 
 
#include <list>
 
#include <list>
 
#include <iostream>
 
#include <iostream>
Linha 164: Linha 90:
 
     nomes.push_front("maneca");
 
     nomes.push_front("maneca");
 
     cout << "Comprimento: " << nomes.size() << ", dados: ";
 
     cout << "Comprimento: " << nomes.size() << ", dados: ";
     mostra_lista2(nomes);
+
     mostra_lista(nomes);
  
 
     // remove dado do início da lista
 
     // remove dado do início da lista
Linha 175: Linha 101:
 
     return 0;
 
     return 0;
 
}</syntaxhighlight>
 
}</syntaxhighlight>
|}
 
  
  
 
Ao usar essa nova estrutura de dados, existem algumas novidades em comparação com a fila e a pilha. A primeira delas diz respeito à iteração da lista. Tanto a fila quanto a pilha possibilitavam acessar apenas os dados em suas extremidades. A lista é muito mais flexível, possibilitando acessar dados em qualquer posição. No entanto, devido à forma como os dados ficam armazenados dentro da lista, para acessá-los é necessário usar um ''iterador''.  
 
Ao usar essa nova estrutura de dados, existem algumas novidades em comparação com a fila e a pilha. A primeira delas diz respeito à iteração da lista. Tanto a fila quanto a pilha possibilitavam acessar apenas os dados em suas extremidades. A lista é muito mais flexível, possibilitando acessar dados em qualquer posição. No entanto, devido à forma como os dados ficam armazenados dentro da lista, para acessá-los é necessário usar um ''iterador''.  
  
== Atividade ==
+
== Iteradores ==
 
 
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_Numeros_Inteiros Lista de números inteiros]
 
 
 
== Um resumo sobre a notação para complexidade de algoritmos ==
 
 
 
[[imagem:Prg2-Big-o-table.jpg]]
 
''Figura obtida [https://apirobot.me/posts/lets-talk-about-data-structures-in-python deste artigo sobre estruturas de dados em Python]''
 
 
 
= Operações quer percorrem a lista =
 
 
 
 
* [https://wiki.sj.ifsc.edu.br/index.php/Introdu%C3%A7%C3%A3o_C%2B%2B#Exce.C3.A7.C3.B5es Exceções: avisos de erros ou situações inesperadas em um programa]
 
* [https://wiki.sj.ifsc.edu.br/index.php/Introdu%C3%A7%C3%A3o_C%2B%2B#Exce.C3.A7.C3.B5es Exceções: avisos de erros ou situações inesperadas em um programa]
  
 
+
Quando se necessitam acessar em sequência todos (ou uma boa parte) dos dados de uma lista, a melhor forma é por meio da operação de iteração. A lista é capaz de ser iterada por meio de um '''iterador'''. Um ''iterador'' é um objeto que se assemelha a um ponteiro, e que possibilita acessar um dado da lista, além de avançar para o dado seguinte ou retroceder para o dado anterior. As operações [http://www.cplusplus.com/reference/list/list/begin/ begin] e [http://www.cplusplus.com/reference/list/list/end/ end] da lista retornam, respectivamente, iteradores para o início ou fim da lista. O exemplo a seguir mostra como usá-los:
A lista encadeada da Prglib ofere outras operações além daquelas para adicionar, obter e remover dados. Duas outras operações são de grande utilidade:
 
* '''iteração''': possibilita obter sucessivamente de forma eficiente os dados da lista;
 
* '''procura''': possibilita procurar dados dentro da lista
 
 
 
== Iteração ==
 
 
 
Quando se necessitam acessar em sequência todos (ou uma boa parte) dos dados de uma lista, a melhor forma é por meio da operação de iteração. A lista é capaz de ser iterada por meio dos métodos ''inicia'', ''proximo'' e ''fim''. Esses métodos são usados em conjunto para acessar cada dado da lista sucessivamente, a partir do início da lista. O exemplo a seguir mostra como usá-los:
 
  
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
 
#include <iostream>
 
#include <iostream>
#include <prglib.h>
+
#include <list>
  
 
using namespace std;
 
using namespace std;
using prglib::lista;
 
  
 
int main() {
 
int main() {
   lista<int> numeros;
+
   list<int> numeros;
  
   numeros.anexa(34);
+
   numeros.push_back(34);
   numeros.anexa(7);
+
   numeros.push_back(7);
   numeros.anexa(21);
+
   numeros.push_back(21);
   numeros.anexa(8);
+
   numeros.push_back(8);
   numeros.anexa(12);
+
   numeros.push_back(12);
   numeros.anexa(17);
+
   numeros.push_back(17);
  
   // inicia a iteração
+
   // Itera do início ao fim da lista
   numeros.inicia();
+
   // A variável "it" é o iterador, que será usado para acessar os dados da lista
 +
  // Note como ao final de cada repetição do laço o iterador é incrementado ... isso
 +
  // faz com que se avance para o próximo dado da lista
  
   // enquanto não chegar ao fim da lista, continua a iteração
+
   for (auto it = numeros.begin(); it != numeros.end(); it++) {  
  while (! numeros.fim()) {
+
     // acessa o dado atual da iteração: ele é referenciado pelo iterador,
     // obtém o próximo dado da iteração  
+
     // como se este fosse um ponteiro
     int algo = numeros.proximo();
 
  
     cout << "Numero: " << algo << endl;
+
     cout << "Numero: " << *it << endl;
 
   }
 
   }
 
}
 
}
Linha 233: Linha 141:
  
  
Essa forma de percorrer os dados de uma lista é muito mais eficiente do que acessar os dados a partir de suas posições (usando o método ''obtem'' ou o operador ''[]''). Para ter uma ideia, percorrer uma lista com 10 dados é 10 vezes mais rápido com iteração. Se a lista tiver 100 dados, a iteração é 100 vezes mais rápida. Se a lista tiver 1000 dados, a uteração é 1000 vezes mais rápida, e assim por diante. O tempo que se leva para percorrer todos os dados com iteração é proporcional à quantidade de dados, porém se for usado o método ''obtem'' (ou operador ''[]''), o tempo necessário é proporcional ao quadrado da quantidade de dados.
+
Para fins de simplicidade, existe uma sintaxe na linguagem para iterar sequências de dados. Veja o exemplo anterior com essa forma de iterar:
 
 
A iteração pode ser feita também em sentido contrário se forem usados os métodos ''iniciaPeloFim'', ''anterior'', e ''inicio'':
 
  
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
 
#include <iostream>
 
#include <iostream>
#include <prglib.h>
+
#include <list>
  
 
using namespace std;
 
using namespace std;
using prglib::lista;
 
  
 
int main() {
 
int main() {
   lista<int> numeros;
+
   list<int> numeros;
  
   numeros.anexa(34);
+
   numeros.push_back(34);
   numeros.anexa(7);
+
   numeros.push_back(7);
   numeros.anexa(21);
+
   numeros.push_back(21);
   numeros.anexa(8);
+
   numeros.push_back(8);
   numeros.anexa(12);
+
   numeros.push_back(12);
   numeros.anexa(17);
+
   numeros.push_back(17);
  
   // inicia a iteração pelo fim da lista
+
   // Itera do início ao fim da lista
   numeros.iniciaPeloFim();
+
   // O iterador é usado implicitamente. Por isso, no laço a variável de controle
 +
  // acessa diretamente o dado atual da iteração (no caso, a variável "x").
  
   // enquanto não chegar ao início da lista, continua a iteração
+
   for (auto & x: numeros) {  
  while (not numeros.inicio()) {
+
     // "x" contém o dado atual da iteração
     // obtém o próximo dado da iteração  
+
     cout << "Numero: " << x << endl;
    int algo = numeros.anterior();
 
 
 
     cout << "Numero: " << algo << endl;
 
 
   }
 
   }
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== Procura por dados ==
 
 
Uma outra operações da lista útil e genérica diz respeito à busca de todos os dados que atendam algum critério de comparação, retornando uma outra lista com os dados encontrados.
 
 
A procura de um dado é uma operação que retorna todos os dados da lista de acordo com uma comparação de igualdade. Assim, os dados armazenados '''precisam ser comparáveis''' para que isso possa ser feito. Em outras palavras, o tipo de dados armazenado deve definir o operador de igualdade (==). Essa operação de procura está declarada assim na classe ''lista'' da ''prglib'':
 
 
<syntaxhighlight lang=c>
 
  // Retorna uma referência a um dado que seja equivalente a "algo"
 
  T& procura(const T &algo) const;
 
 
  // Procura todos os dados equivalentes a "algo", e os
 
  // anexa a "lista". Retorna uma referência à própria lista "result" que foi passada como parâmetro.
 
  lista<T> & procuraMuitos(const T &algo, lista<T> & result) const;
 
</syntaxhighlight>
 
  
Existem dois métodos para procura de dados na lista:
+
Deve-se observar que isso é específico de C++: não há algo parecido na linguagem C ! Porém, essa forma simplificada de iterar sequências aparece em outras linguagens (ex: [https://towardsdatascience.com/python-basics-iteration-and-looping-6ca63b30835c Python]).
* ''procura'': procura a primeira ocorrência de um dado que seja igual a ''algo'', retornando uma referência ao dado encontrado. Caso não o encontre, dispara uma [[Introdução_C++#Exce.C3.A7.C3.B5es|exceção]].
 
* ''procuraMuitos'': procura todos os dados que sejam iguais a ''algo'', e os armazena na lista ''result''. Caso nada encontre, não modifica a lista ''result'' e termina sem avisar qualquer erro.
 
  
 +
=== Iteração reversa ===
  
O método ''procura'' pode ser testado com uma lista de inteiros. O programa de teste a seguir demonstra o uso de ''procura'' (obs: o programa depende da lista ter implementado o [[PRG2-2016-1#Exerc.C3.ADcio:_apresenta.C3.A7.C3.A3o_do_conte.C3.BAdo_de_uma_lista|método ''escrevaSe'']]):
+
A iteração pode ser feita também em sentido contrário, se for usado um iterador reverso. Os métodos [http://www.cplusplus.com/reference/list/list/rbegin/ rbegin] e [http://www.cplusplus.com/reference/list/list/rend/ rend] de ''list'' retornam, respectivamente, iteradores reversos para o fim e início da lista:
  
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
 
#include <iostream>
 
#include <iostream>
#include <prglib.h>
+
#include <list>
  
 
using namespace std;
 
using namespace std;
using prglib::lista;
 
  
 
int main() {
 
int main() {
   lista<int> l;
+
   list<int> numeros;
  
   l.anexa(5);
+
   numeros.push_back(34);
   l.anexa(1);
+
   numeros.push_back(7);
   l.anexa(5);
+
   numeros.push_back(21);
   l.anexa(5);
+
   numeros.push_back(8);
  l.anexa(1);
+
   numeros.push_back(12);
   l.anexa(8);
+
   numeros.push_back(17);
   l.anexa(2);
 
  
   // "procura" dispara uma exceção se não encontrar o dado, então
+
   // Itera do início ao fim da lista
   // deve-se capturar essa exceção para tratá-la.
+
  // A variável "it" é o iterador, que será usado para acessar os dados da lista
   try {
+
   // Note como ao final de cada repetição do laço o iterador é incrementado ... isso
    int res = l.procura(5);
+
  // faz com que se retorceda para o próximo dado da lista
    cout << "Encontrou o valor " << res << " na lista" << endl;
+
 
  } catch (...) {
+
   for (auto it = numeros.rbegin(); it != numeros.rend(); it++) {  
     cout << "NÃO encontrou o valor 5 na lista" << endl;   
+
     // acessa o dado atual da iteração: ele é referenciado pelo iterador,
  }
+
    // como se este fosse um ponteiro
  
  try {
+
     cout << "Numero: " << *it << endl;
    int res = l.procura(9);
 
     cout << "Encontrou o valor " << res << " na lista" << endl;
 
  } catch (...) {
 
    cout << "NÃO encontrou o valor 9 na lista" << endl;  
 
 
   }
 
   }
 
  lista<int> lr;
 
 
  l.procuraMuitos(5, lr);
 
  cout << "Encontrou " << lr.comprimento() << " valores iguais a 5" << endl;
 
  lr.escrevaSe(cout, ",");
 
  cout << endl;
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Comparação de igualdade ===
 
  
As operações de ''procura'' dependem da comparação de igualdade dos dados contidos numa lista. Isso significa que deve existir o operador ''=='' para o tipo desses dados. Esse operador existe na linguagem para tipos básicos, tais como ''int'', ''float'', ''char'' e outros. Mas o mesmo não vale para tipos de dados definidos pelo programador, ou mesmo classes. Nesse caso, o '''programador deve definir o operador ''=='' ''', pois somente ele sabe como valores desses tipos devem ser comparados. Observe-se também que a comparação dos dados não é um problema da ''lista'', que apenas usa a comparação. Em suma, cada tipo e dados ou classe deve saber como comparar seus valores ou objetos.
+
A sintaxe para iteração simplificada não se aplica à iteração reversa !
  
A linguagem C++ possibilita definir como um determinado operador deve funcionar. Isso aplicado ao operador ''=='' resolve o problema da comparação de um tipo de dados definido pelo programador. A implementação de um operador pode ser feita de duas maneiras:
+
== Atividade ==
# '''Incluindo-o ao tipo struct ou à classe''': <syntaxhighlight lang=c>
+
 
struct Registro {
+
'''Objetivo:''' escrever um programa que mescle as informações contidas em diferentes arquivos, eliminando informações repetidas
  // atributos do tipo Registro
+
* ''Descrição'': existe um conjunto de arquivos, os quais contêm dados sobre alunos de uma escola. Cada arquivo corresponde a uma disciplina de algum curso dessa escola.  Cada linha desses arquivos tem este formato:
 +
 
 +
matrícula aluno
  
  bool operator==(const Registro & outro) const;
+
... sendo ''matrícula'' um número com 8 dígitos, e ''aluno'' o nome completo de um aluno.<br><br>Há a necessidade de obter uma relação dos alunos matriculados nessas disciplinas, sendo que cada aluno deve aparecer uma única vez.
};
 
</syntaxhighlight>
 
# '''Criando uma função''': <syntaxhighlight lang=c>
 
// compara "este" com "outro"
 
bool operator==(const Registro & este, const Registro & outro) const {
 
  // implementação da comparação
 
}
 
</syntaxhighlight>
 
  
  
Ambas as formas de implementar um operador são válidas e resolvem o problema. Uma observação diz respeito à implementação com uma função, a qual tem precedência sobre a implementação  dentro da struct ou classe. Isso significa que mesmo que já exista o operador em questão definido dentro de uma struct ou classe, ele pode ser substituído por outra implementação desse operador em uma função. A isso se chama [[Introdu%C3%A7%C3%A3o_C%2B%2B#Sobrecarga_de_operador|sobrecarga de operador]].
+
Para se familiarizar com o uso de listas, resolva primeiro estes exercícios:
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_Numeros_Inteiros Lista de números inteiros]
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_CopiaLista Copiar uma lista usando iteração]
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_Junta Junta strings contidas em uma lista]
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_FiltraValores Filtra valores de uma lista]
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_Contadores Lista de contadores]
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_PalavrasRepetidas Palavras repetidas]
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_ContadorPalavras Contador de palavras]
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_Separa A função separa usando lista]
 +
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_CompactadorIPv6 Compactador de endereço IPv6]
  
== Atividade ==
+
== Um resumo sobre a notação para complexidade de algoritmos ==
  
Faça estes exercícios que envolvem iteração e/ou procura por dados:
+
[[imagem:Prg2-Big-o-table.jpg]]
# [https://moodle.sj.ifsc.edu.br/mod/vpl/view.php?id=5594 Copiar uma lista usando iteração]
+
<br>''Figura obtida [https://apirobot.me/posts/lets-talk-about-data-structures-in-python deste artigo sobre estruturas de dados em Python]''
# [https://moodle.sj.ifsc.edu.br/mod/vpl/view.php?id=5600 Filtra valores de uma lista]
 
# [https://moodle.sj.ifsc.edu.br/mod/vpl/view.php?id=5599 Lista de contadores]
 
# [https://moodle.sj.ifsc.edu.br/mod/vpl/view.php?id=5601 Palavras repetidas]
 
# [https://moodle.sj.ifsc.edu.br/mod/vpl/view.php?id=5602 Contador de palavras]
 
  
 
= Operações que reorganizam a lista =
 
= Operações que reorganizam a lista =
Linha 372: Linha 245:
 
== Ordenamento da lista ==
 
== Ordenamento da lista ==
  
A lista possui o método ''ordena'', que ordena seus dados. O ordenamento é feito por um [https://en.wikipedia.org/wiki/Merge_sort algoritmo] com razoável eficiência (ele tem custo de tempo computacional ''O(n log n)''), e isso é importante porque esse tipo de operação tem custo computacional considerável (pode ser proporcional ao quadrado da quantidade de dados se não for bem feito). O único requisito para ordenar uma lista é que os dados armazenados possuam uma relação de precedência. Em outras palavras, que possam ser comparados com operador ''<'' (menor que). O exemplo a seguir mostra o ordenamento de uma lista:
+
A lista possui o método ''sort'', que ordena seus dados. O ordenamento é feito por um [https://en.wikipedia.org/wiki/Merge_sort algoritmo] com razoável eficiência (ele tem custo de tempo computacional ''O(n log n)''), e isso é importante porque esse tipo de operação tem custo computacional considerável (pode ser proporcional ao quadrado da quantidade de dados se não for bem feito). O único requisito para ordenar uma lista é que os dados armazenados possuam uma relação de precedência. Em outras palavras, que possam ser comparados com operador ''<'' (menor que). O exemplo a seguir mostra o ordenamento de uma lista:
  
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
 
#include <iostream>
 
#include <iostream>
#include <prglib.h>
+
#include <list>
  
 
using namespace std;
 
using namespace std;
using prglib::lista;
 
  
 
int main() {
 
int main() {
   lista<int> numeros;
+
   list<int> numeros;
  
   numeros.anexa(34);
+
   numeros.push_back(34);
   numeros.anexa(7);
+
   numeros.push_back(7);
   numeros.anexa(21);
+
   numeros.push_back(21);
   numeros.anexa(8);
+
   numeros.push_back(8);
   numeros.anexa(12);
+
   numeros.push_back(12);
   numeros.anexa(17);
+
   numeros.push_back(17);
  
 
   // ordena a lista
 
   // ordena a lista
   numeros.ordena();
+
   numeros.sort();
  
   numeros.escrevaSe(cout);
+
   // mostra o conteúdo da lista
 +
  for (auto & x: numeros) {
 +
    cout << x << endl;
 +
  }
  
 
   cout << endl;
 
   cout << endl;
Linha 400: Linha 275:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Como o método ''ordena'' depende da existência do operador ''<'' para o tipo dos dados armazenados, as [[PRG29003:_Introdu%C3%A7%C3%A3o_a_Listas#Compara.C3.A7.C3.A3o_de_igualdade|mesmas considerações feitas quanto à operação de igualdade]] se aplicam aqui. A próxima subseção mostra um exemplo de ordenamento de uma lista que contém valores de um tipo definido pelo programador.
+
Como o método ''sort'' depende da existência do operador ''<'' para o tipo dos dados armazenados, as [[PRG29003:_Introdu%C3%A7%C3%A3o_a_Listas#Compara.C3.A7.C3.A3o_de_igualdade|mesmas considerações feitas quanto à operação de igualdade]] se aplicam aqui. A próxima subseção mostra um exemplo de ordenamento de uma lista que contém valores de um tipo definido pelo programador.
  
 
=== Ordenamento de uma lista com valores de um tipo definido pelo programador ===
 
=== Ordenamento de uma lista com valores de um tipo definido pelo programador ===
  
Quando um programador define um novo tipo ou classe, seus operadores relacionais ainda não existem. Quer dizer, não se conseguem comparar valores desse novo tipo com operadores tais como <, >, <=, >= e ==, [http://www.cplusplus.com/doc/tutorial/operators/ entre outros]. O ordenamento da lista depende do operador <, portanto uma lista que contenha valores do novo tipo não pode ser ordenada a menos que esse operador seja declarado e implementado.
+
A operação ''sort'' depende da comparação de precedência dos dados contidos numa lista. Isso pode ser feito usando o operador ''<'' para o tipo desses dados (caso exista), ou uma função de comparação específica para essa finalidade. O operador ''<'' existe na linguagem para tipos básicos, tais como ''int'', ''float'', ''char'' e outros. Mas o mesmo não vale para tipos de dados definidos pelo programador, ou mesmo classes. Nesse caso, o '''programador deve definir o operador ''<'' ''', pois somente ele sabe como valores desses tipos devem ser comparados. Observe-se também que a comparação dos dados não é um problema da ''lista'', que apenas usa a comparação. Em suma, cada tipo e dados ou classe deve saber como comparar seus valores ou objetos, ou deve existir uma função para fazer essa comparação.
  
O exemplo a seguir mostra um programa que cria uma lista com valores de um novo tipo de dados, e a ordena.
+
O caso mais simples envolve escrever uma função que compare valores do tipo de dados em questão. O exemplo a seguir mostra como essa função deve ser declarada, e depois como ela pode ser utilizada para fazer o ordenamento.
  
{{collapse top|Exemplo com o operador< como um método do novo tipo de dados}}
 
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
// o novo tipo de dados: as coordenadas de um vetor bidimensional
+
// compara duas strings de acordo com seus comprimentos
struct Vetor {
+
bool comp_comprimento(cons string & s1, const string & s2) {
  double x, y;
+
  return s1.size() < s2.size();
 
+
}
  Vetor(double X, double Y) {
 
    x = X;
 
    y = Y;
 
  }
 
 
 
  // calcula o módulo do vetor
 
  double modulo() const {
 
    return sqrt(x*x + y*y);
 
  }
 
 
 
  // operador < para Vetor: compara os módulos deste vetor com o outro vetor
 
  // retorna true se módulo deste vetor for menor que módulo de v2
 
  bool operador<(const Vetor & v2) {
 
    return modulo() < m2.modulo();
 
  }
 
};
 
 
 
  
 
int main() {
 
int main() {
   lista<Vetor> lv;
+
   list<string> l;
 
 
  // acrescenta alguns vetores à lista
 
  lv.anexa(Vetor(2,2));
 
  lv.anexa(Vetor(1,3));
 
  lv.anexa(Vetor(3,2));
 
  lv.anexa(Vetor(0,5));
 
  lv.anexa(Vetor(3,3));
 
  
   // ordena a lista
+
   l.push_back("banana");
   lv.ordena();
+
  l.push_back("caju");
 +
  l.push_back("laranja");
 +
   l.push_back("cajamanga");
  
   // mostra os vetores ordenados
+
   // ordena lista de string de acordo com comprimentos das string
   lv.inicia();
+
   l.sort(comp_comprimento);
  while (! lv.fim()) {
 
    Vetor & v = lv.proximo();
 
  
     cout << "x=" << v.x << ", " << y=" << v.y << endl;
+
  // mostra conteúdo da lista na tela
 +
  for (auto & w: l) {
 +
     cout << w << endl;
 
   }
 
   }
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
{{collapse bottom}}
 
  
{{collapse top|Exemplo com o operador< como uma função}}
 
<syntaxhighlight lang=c>
 
// o novo tipo de dados: as coordenadas de um vetor bidimensional
 
struct Vetor {
 
  double x, y;
 
  
  Vetor(double X, double Y) {
+
A linguagem C++ possibilita definir como um determinado operador deve funcionar. Isso aplicado ao operador ''<'' resolve o problema da comparação de um tipo de dados definido pelo programador. A implementação de um operador pode ser feita de duas maneiras:
    x = X;
+
# '''Incluindo-o ao tipo struct ou à classe''': <syntaxhighlight lang=c>
    y = Y;
+
struct Registro {
   }
+
   // atributos do tipo Registro
  
   // calcula o módulo do vetor
+
   bool operator<(const Registro & outro) const;
  double modulo() const {
 
    return sqrt(x*x + y*y);
 
  }
 
 
 
};
 
};
 +
</syntaxhighlight>
 +
# '''Criando uma função''': <syntaxhighlight lang=c>
 +
// compara "este" com "outro"
 +
bool operator<(const Registro & este, const Registro & outro) const {
 +
  // implementação da comparação: verifica se "este" < "outro"
 +
}
 +
</syntaxhighlight>
  
// operador < para Vetor: compara os módulos dos vetores v1 e v2
 
// retorna true se módulo do vetor v1 for menor que módulo de v2
 
bool operator<(const Vetor & v1, const Vetor & v2) {
 
    return v1.modulo() < v2.modulo();
 
}
 
  
int main() {
+
Ambas as formas de implementar um operador são válidas e resolvem o problema. Uma observação diz respeito à implementação com uma função, a qual tem precedência sobre a implementação  dentro da struct ou classe. Isso significa que mesmo que já exista o operador em questão definido dentro de uma struct ou classe, ele pode ser substituído por outra implementação desse operador em uma função. A isso se chama [[Introdu%C3%A7%C3%A3o_C%2B%2B#Sobrecarga_de_operador|sobrecarga de operador]].
  lista<Vetor> lv;
 
 
 
  // acrescenta alguns vetores à lista
 
  lv.anexa(Vetor(2,2));
 
  lv.anexa(Vetor(1,3));
 
  lv.anexa(Vetor(3,2));
 
  lv.anexa(Vetor(0,5));
 
  lv.anexa(Vetor(3,3));
 
  
  // ordena a lista
 
  lv.ordena();
 
  
  // mostra os vetores ordenados
+
O exemplo a seguir mostra um programa que cria uma lista com valores de um novo tipo de dados, e a ordena.
  lv.inicia();
 
  while (! lv.fim()) {
 
    Vetor & v = lv.proximo();
 
  
    cout << "x=" << v.x << ", " << y=" << v.y << endl;
+
{{collapse top|Exemplo com o operador< como um método do novo tipo de dados}}
  }
 
}
 
</syntaxhighlight>
 
{{collapse bottom}}
 
{{collapse top|Outro exemplo visto em aula}}
 
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
 
#include <iostream>
 
#include <iostream>
 
#include <string>
 
#include <string>
#include <prglib.h>
+
#include <list>
 
   
 
   
 
using namespace std;
 
using namespace std;
using prglib::lista;
 
  
 
// Tipo Alguem: representa uma pessoa com seu nome e idade  
 
// Tipo Alguem: representa uma pessoa com seu nome e idade  
Linha 536: Linha 361:
 
};
 
};
  
// sobrecarga do operador << para stream de saída (ostream)
+
int main() {
// com ele se consegue escrever na stream um valor do
+
  list<Alguem> l;
// tipo Alguem
+
 
 +
  Alguem joao("Joao", 20);
 +
  l.push_back(joao);
 +
 
 +
  Alguem x1("Amanda", 15);
 +
  l.push_back(x1);
 +
 
 +
  Alguem x2("Gabriel", 19);
 +
  l.push_back(x2);
 +
 
 +
  l.push_back(Alguem("Isadora", 18));
 +
  l.push_back(Alguem("Gustavo", 18));
 +
 
 +
  // ordena a lista
 +
  l.sort();
 +
 +
  // mostra a lista ...
 +
  for (auto & pessoa: l) {
 +
    cout << pessoa.nome << ": " << pessoa.idade << endl;
 +
  }
 +
 
 +
  cout << endl;
 +
}
 +
</syntaxhighlight>
 +
{{collapse bottom}}
 +
{{collapse top|Exemplo do operador< como uma função}}
 +
<syntaxhighlight lang=c>
 +
#include <iostream>
 +
#include <string>
 +
#include <list>
 +
 +
using namespace std;
 +
 
 +
// Tipo Alguem: representa uma pessoa com seu nome e idade
 +
struct Alguem {
 +
    string nome;
 +
    int idade;
 +
   
 +
    Alguem() {
 +
        idade = 0;       
 +
    }
 +
   
 +
    Alguem(const string & umNome, int age) {
 +
        nome = umNome;
 +
        idade = age;
 +
    }       
 +
};
  
ostream & operator<<(ostream & out, const Alguem & x) {
+
// sobrecarga do operador< para o tipo Alguem
     out << x.nome << ": " << x.idade;
+
bool operator<(const Alguem & este, const Alguem & outro) {
     return out;
+
     if (este.idade == outro.idade) {
 +
        return este.nome < outro.nome;
 +
    }
 +
     return este.idade < outro.idade;
 
}
 
}
 +
  
 
int main() {
 
int main() {
   lista<Alguem> l;
+
   list<Alguem> l;
 
    
 
    
 
   Alguem joao("Joao", 20);
 
   Alguem joao("Joao", 20);
   Alguem ninguem;
+
   l.push_back(joao);
 
    
 
    
 
   Alguem x1("Amanda", 15);
 
   Alguem x1("Amanda", 15);
   l.anexa(x1);
+
   l.push_back(x1);
 
    
 
    
 
   Alguem x2("Gabriel", 19);
 
   Alguem x2("Gabriel", 19);
   l.anexa(x2);
+
   l.push_back(x2);
 
    
 
    
   l.anexa(Alguem("Isadora", 18));
+
   l.push_back(Alguem("Isadora", 18));
   l.anexa(Alguem("Gustavo", 18));
+
   l.push_back(Alguem("Gustavo", 18));
 
    
 
    
 
   // ordena a lista
 
   // ordena a lista
   l.ordena();
+
   l.sort();
 
   
 
   
   // isto só funciona porque agora existe o operador <<
+
   // mostra a lista ...
   // que sabe apresentar o tipo Alguem
+
  for (auto & pessoa: l) {
   l.escrevaSe(cout);
+
    cout << pessoa.nome << ": " << pessoa.idade << endl;
 
+
   }
 +
    
 
   cout << endl;
 
   cout << endl;
 
}
 
}
Linha 572: Linha 448:
 
{{collapse bottom}}
 
{{collapse bottom}}
  
== Embaralhamento ==
+
<!--== Embaralhamento ==
  
 
O embaralhamento envolve misturar eficientemente os dados da lista de forma aleatória. O algoritmo envolvido tem custo de tempo computacional ''O(n log n)''. Esse método está declarado assim na classe lista:
 
O embaralhamento envolve misturar eficientemente os dados da lista de forma aleatória. O algoritmo envolvido tem custo de tempo computacional ''O(n log n)''. Esse método está declarado assim na classe lista:
Linha 608: Linha 484:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
-->
  
 
== Inversão ==
 
== Inversão ==
  
A inversão da lista envolve inverter a ordem dos dados nela armazenados: o primeiro se torna o último, o segundo o penúltimo, e assim por diante. O algoritmo envolvido tem custo de tempo computacional ''O(n)''. Esse método está declarado assim na classe lista:
+
A inversão da lista, implementada pela operação [http://www.cplusplus.com/reference/list/list/reverse/ reverse], envolve inverter a ordem dos dados nela armazenados: o primeiro se torna o último, o segundo o penúltimo, e assim por diante. O algoritmo envolvido tem custo de tempo computacional ''O(n)''.
 
 
<syntaxhighlight lang=c>
 
  // inverte a ordem dos dados de uma lista
 
  void inverte();
 
</syntaxhighlight>
 
  
 
Seu uso é direto, e não há dependência a qualquer operador do tipo dos dados armazenados. Um exemplo de uso é este:
 
Seu uso é direto, e não há dependência a qualquer operador do tipo dos dados armazenados. Um exemplo de uso é este:
Linha 622: Linha 494:
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
 
#include <iostream>
 
#include <iostream>
#include <prglib.h>
+
#include <list>
  
 
using namespace std;
 
using namespace std;
using prglib::lista;
 
  
 
int main() {
 
int main() {
   lista<int> numeros;
+
   list<int> numeros;
  
   numeros.anexa(34);
+
   numeros.push_back(34);
   numeros.anexa(7);
+
   numeros.push_back(7);
   numeros.anexa(21);
+
   numeros.push_back(21);
   numeros.anexa(8);
+
   numeros.push_back(8);
   numeros.anexa(12);
+
   numeros.push_back(12);
   numeros.anexa(17);
+
   numeros.push_back(17);
  
 
   // ordena a lista
 
   // ordena a lista
   numeros.ordena();
+
   numeros.sort();
  
 
   // ... e agora a inverte, para obter um ordenamento decrescente
 
   // ... e agora a inverte, para obter um ordenamento decrescente
   numeros.inverte();
+
   numeros.reverse();
  
   numeros.escrevaSe(cout);
+
   // apresenta a lista
 +
  for (auto & x: numeros) {
 +
    cout << x << endl;
 +
  }
  
  cout << endl;
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
Linha 652: Linha 525:
  
 
Faça estes exercícios que envolvem ordenamento:
 
Faça estes exercícios que envolvem ordenamento:
# [https://moodle.sj.ifsc.edu.br/mod/vpl/view.php?id=5603 Lista ordenada de números inteiros]
+
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_ListaOrdenadaInteiros Lista ordenada de números inteiros]
# [https://moodle.sj.ifsc.edu.br/mod/vpl/view.php?id=5605 Ordenar linhas de um arquivo]
+
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_OrdenarLinhasArquivo Ordenar linhas de um arquivo]
# [https://moodle.sj.ifsc.edu.br/mod/vpl/view.php?id=5607 Ordenar linhas de um arquivo de acordo com comprimentos das linhas]
+
# [https://github.com/IFSC-Engtelecom-Prg2/Lista_OrdenarLinhasPorComprimento Ordenar linhas de um arquivo de acordo com comprimentos das linhas]
  
 +
<!--
 
{{collapse top | Uma solução para o exercício 3}}
 
{{collapse top | Uma solução para o exercício 3}}
 
<syntaxhighlight lang=c>
 
<syntaxhighlight lang=c>
 
#include <fstream>
 
#include <fstream>
 
#include <iostream>
 
#include <iostream>
#include <prglib.h>
+
#include <list>
using prglib::lista;
 
  
 
using namespace std;
 
using namespace std;
Linha 691: Linha 564:
  
 
     // Cria uma lista de Linha, para ser possível ordenar pelo comprimento de string
 
     // Cria uma lista de Linha, para ser possível ordenar pelo comprimento de string
     lista<Linha> l;
+
     list<Linha> l;
  
 
     string x;
 
     string x;
 
      
 
      
 
     // para cada linha lida e guardada na variável "x"
 
     // para cada linha lida e guardada na variável "x"
     while (getline(arq, x)) l.anexa(x);
+
     while (getline(arq, x)) l.push_back(x);
  
 
     // ordena a lista: a comparação entre os valores armazenados será feita usando o  
 
     // ordena a lista: a comparação entre os valores armazenados será feita usando o  
 
     // operador < do tipo Linha
 
     // operador < do tipo Linha
     l.ordena();
+
     l.sort();
 
      
 
      
 
     // Usa iteração para apresentar o conteúdo da lista na tela
 
     // Usa iteração para apresentar o conteúdo da lista na tela
     l.inicia();
+
     for (auto & linha: l) {       
    while (! l.fim()) {
 
        Linha & linha = l.proximo();
 
       
 
 
         cout << linha.s << endl;
 
         cout << linha.s << endl;
 
     }
 
     }
Linha 714: Linha 584:
 
</syntaxhighlight>
 
</syntaxhighlight>
 
{{collapse bottom}}
 
{{collapse bottom}}
{{collapse top | Outra solução para o exercício 3 ... ver o uso da operação escrevaSe da lista (depende do operador << para o tipo Linha)}}
+
-->
<syntaxhighlight lang=c>
+
 
#include <fstream>
+
== Curiosidade: contando quantas vezes cada valor existe em uma lista ==
#include <iostream>
+
 
#include <prglib.h>
+
Em um projeto sobre estatísticas sobre filmes e atores, ralizado em 2019.2, o requisito ''"listar os atores que mais atuaram, por ordem decrescente de atuações em filmes"'' poderia ser resolvido de mais de uma maneira. Uma abordagem é criar uma lista contendo todos os nomes de atores que atuaram nos filmes, de forma que, se um ator trabalhou em três filmes, seu nome apareceria três vezes nessa lista. Basicamente isso implica listar os atores de cada filme, e acrescentá-los a essa nova lista. Ao final, bastaria contar quantas vezes cada ator aparece na lista.
using prglib::lista;
+
 
 +
Ao menos dois algoritmos podem ser pensados para esse problema:
 +
# Para cada ator da lista, iterar a lista para contar quantas vezes seu nome aparece. Há que cuidar para realizar essa procura somente para a primeira vez em que cada ator é avaliado.
 +
# Ordenar a lista de atores, e então iterá-la. Os nomes de atores ficarão contíguos, o que facilita contá-los. Se o próximo ator da iteração for diferente do anterior, então registra-se a contagem do ator anterior e reinicia-se o contador.
  
using namespace std;
 
  
// Este novo tipo de dados foi criado para possibilitar comparar
+
Qual dos dois algoritmos é melhor, do ponto de vista de custo computacional (tempo para que concluam) ? O gráfico abaixo responde essa questão !
// strings pelos seus comprimentos.
 
// Note que o tipo Linha apenas encapsula uma string (campo string s),
 
// e define o operador <
 
  
struct Linha {
+
[[imagem:PRG2-Conta_repetidos.png]]
  // a string encapsulada
+
<br>''Custo computacional dos algoritmos para contar valores repetidos em uma lista, para listas de números inteiros''
  string s;
 
  
  Linha() {}
+
= Uma alternativa à lista: vetor dinâmico =
  Linha(const string & algo) {
 
    s = algo;
 
  }   
 
};
 
  
// o operador < compara as strings s1 e s2 (s1 < s2)
+
A STL apresenta uma outra estrutura de dados linear chamada [http://www.cplusplus.com/reference/vector/vector vector]. Essa estrutura se apŕesenta como um vetor dinâmico, em que dados são armazenados de forma parecida com um vetor, porém cuja área de armazenamento em memória pode ser expandida automaticamente. Em ''vector'', os dados estão sempre contíguos em memória. Um ''vector'' usa uma área de memória capaz de guardar a quantidade de dados armazenados, estando os dados gravados sequencialmente ali dentro. A figura a seguir mostra como um vector usa memória para armazenar dados.
// a comparação é feita pelos comprimentos das strings encapsuladas
 
bool operator<(const Linha & s1, const Linha & s2) {
 
  return s1.s.size() < s2.s.size();
 
}
 
  
// define como funciona o operador << (escrita formatada) para o tipo Linha
+
[[imagem:Prg2-Vector.png]]
ostream& operator<<(ostream & out, const Linha & linha) {
+
<br>''Um vector com alguns dados armazenados''
  out << linha.s;
 
  return out;
 
}
 
  
int main(int argc, char * argv[]) {
 
    // implemente aqui seu programa
 
    ifstream arq(argv[1]);
 
  
    // Cria uma lista de Linha, para ser possível ordenar pelo comprimento de string
+
Em geral, ''vector'' é adequado quando os dados são armazenados e removidos do final da área de armazenamento, e também quando se precisam acessá-los diretamente (por suas posições) e/ou aleatoriamente. Com ''list'' é o contrário, pois ela é adequada quando dados precisam ser inseridos e removidos de qualquer posição. Cabe ao programador escolher a estrutura mais adequada em cada situação. Os pontos listados a seguir buscam esclarecer melhor o que está em jogo.
    lista<Linha> l;
+
* Com ''list'', não é necessária uma área de memória contígua: como dados são armazenados dinamicamente, eles podem ocupar áreas de memória arbitrárias, e não há nenhuma relação entre a localização dos dados em memória e sua ordem na lista. No caso de ''vector'', a localização dos dados em memória está diretamente ligada a suas posições na área de armazenamento.
 +
* Com ''list'' não é possível indexar os dados, por isso para acessar um dado deve-se obrigatoriamente procurá-lo a partir do início ou fim da lista, seguindo cada sucessor ou antecessor até chegar àquele procurado.
 +
* Com ''vector'' podem-se indexar os dados, acessando-os diretamente por suas posições, uma vez que suas localizações em memória podem ser calculadas em função de suas posições.
 +
* Com ''list'', acrescentar uma dado implica modificar a referência ao sucessor do dado que deve antecedê-lo na lista. Assim, não é necessário ''"empurrar"'' os dados seguintes para frente (como seria o caso quando se usa ''vector'').
 +
* Para remover um dado é a mesma coisa: basta modificar a referência de seu antecessor. Assim, não é necessário ''"deslocar pra trás"'' os dados seguintes (como seria o caso de ''vector'').
  
    string x;
 
   
 
    // para cada linha lida e guardada na variável "x"
 
    while (getline(arq, x)) l.anexa(x);
 
  
    // ordena a lista: a comparação entre os valores armazenados será feita usando o  
+
As operações que podem ser feitas em ''vector'' são parecidas com as operações de ''list''.
    // operador < do tipo Linha
+
* [http://www.cplusplus.com/reference/vector/vector/push_back/ push_back]: Adicionar um dado ao final
    l.ordena();
+
* [http://www.cplusplus.com/reference/vector/vector/insert/ insert]: Inserir um dado em uma determinada posição
   
+
* [http://www.cplusplus.com/reference/vector/vector/erase/ erase]: Remover um ou mais dados a partir de uma determinada posição
    l.escrevaSe(cout);
+
* [http://www.cplusplus.com/reference/vector/vector/front/ front]: Acessar o dado que está no início
    cout << endl;
+
* [http://www.cplusplus.com/reference/vector/vector/operator%91%93/ operador <nowiki>[]</nowiki>]: Acessar um dado em uma posição qualquer
 +
* [http://www.cplusplus.com/reference/vector/vector/back/ back]: Acessar o dado que está no final
 +
* [http://www.cplusplus.com/reference/vector/vector/size/ size]: Obter a quantidade de dados armazenados
 +
* [http://www.cplusplus.com/reference/vector/vector/clear/ clear]: Remover todos os dados (esvaziar)
  
    return 0;
 
}
 
</syntaxhighlight>
 
{{collapse bottom}}
 
  
 +
Abaixo segue um exemplo de uso de algumas operações de ''vector'':
  
<!-- === Exemplo de modelagem ===
+
<syntaxhighlight lang=c++>
 +
#include <cstdlib>
 +
#include <vector>
 +
#include <iostream>
 +
#include <string>
  
Uma pessoa possui um arquivo de texto com telefones de amigos, porém a relação de telefones está desorganizada. Por isso imaginou-se que um programa poderia ajudá-la a organizar essa lista de telefones, ordenando-a pelos nomes dos amigos.
+
using namespace std;
* [http://tele.sj.ifsc.edu.br/~msobral/prg2/teles.zip Arquivo com os telefones]
 
  
 +
void mostra_vetor(vector<string> & v) {
 +
    // itera o vetor
 +
    for (auto & dado: v) {
 +
      cout << dado << ",";
 +
    }
 +
    cout << endl;
 +
}
  
Projete esse programa, identificando classes que representem os elementos da lista de telefones. Com isso:
+
int main(int argc, char** argv) {
# Declare cada uma das classes identificadas
+
    // cria um vector de string
# Implemente os métodos dessas classes
+
    vector<string> nomes;
# Escreva o programa de forma a usar objetos dessas classes
+
   
-->
+
    // anexa três dados ao final do vector
 +
    nomes.push_back("manuel");
 +
    nomes.push_back("maria");
 +
    nomes.push_back("bilica");
 +
   
 +
    // mostra comprimento e conteúdo do vector
 +
    cout << "Comprimento: " << nomes.size() << ", dados: ";
 +
    mostra_vetor(nomes);
  
== Curiosidade: contando quantas vezes cada valor existe em uma lista ==
+
    // Acessa um dado por sua posição
 +
    for (int i=0; i < v.size(); i++) {
 +
      cout << "Dado na posição " << i << ": " << nomes[i] << endl;
 +
    }
  
No projeto 2, o requisito ''"listar os atores que mais atuaram, por ordem decrescente de atuações em filmes"'' poderia ser resolvido de mais de uma maneira. Uma abordagem é criar uma lista contendo todos os nomes de atores que atuaram nos filmes, de forma que, se um ator trabalhou em três filmes, seu nome apareceria três vezes nessa lista. Basicamente isso implica listar os atores de cada filme, e acrescentá-los a essa nova lista. Ao final, bastaria contar quantas vezes cada ator aparece na lista.
+
    // remove dado do final do vector
 +
    nomes.pop_back();
 +
    cout << "Comprimento: " << nomes.size() << ", dados: ";
 +
    mostra_vetor(nomes);
 +
   
 +
    // ao final, vector é automaticamente destruído, e a memória utilizada
 +
    // é liberada
 +
    return 0;
 +
}</syntaxhighlight>
  
Ao menos dois algoritmos podem ser pensados para esse problema:
 
# Para cada ator da lista, usar a operação ''procuraMuitos'' da lista para contar quantas vezes seu nome aparece. O comprimento da lista resultante dessa procura é a quantidade de vezes em que seu nome aparece. Há que cuidar para realizar essa procura somente para a primeira vez em que cada ator é avaliado.
 
# Ordenar a lista de atores, e então iterá-la. Os nomes de atores ficarão contíguos, o que facilita contá-los. Se o próximo ator da iteração for diferente do anterior, então registra-se a contagem do ator anterior e reinicia-se o contador.
 
  
 +
Nem tudo que se pode fazer com ''list'' está disponível em ''vector'':
 +
* '''Ordenamento''': não há uma operação de ordenamento para ''vector'', porém é possível ordená-los por meio de um [http://www.cplusplus.com/reference/algorithm/sort/ algoritmo] existente na STL.
 +
* '''Embaralhamento:''' não existe uma operação para embaralhar os dados em um ''vector'', mas também há um [http://www.cplusplus.com/reference/algorithm/random_shuffle/ algoritmo para essa finalidade] na STL.
 +
* '''Reversão''': não há operação para inverter as ordens dos dados, o que também depende de um [http://www.cplusplus.com/reference/algorithm/reverse/ algoritmo] da STL.
 +
* ... e algumas outras operações de ''list'' !
  
Qual dos dois algoritmos é melhor, do ponto de vista de custo computacional (tempo para concluirem) ? O gráfico abaixo responde essa questão !
 
  
[[imagem:PRG2-Conta_repetidos.png]]
+
Por fim, assim como em''list'', iteradores são úteis para acessar dados e percorrer ''vector''.
<br>''Custo computacional dos algoritmos para ocntar valores repetidos em uma lista, para listas de números inteiros''
 

Edição atual tal como às 14h56min de 21 de maio de 2020

[Próxima aula]



Uma lista encadeada é uma estrutura de dados em que os dados são armazenados dinamicamente em memória, de forma a se organizarem em uma sequência. Qualquer dado em uma lista pode ser acessado, independente de sua posição, assim como pode ser adicionados ou removidos de uma posição qualquer. Além disso, a ordem dos dados em uma lista pode ser modificada de diferentes maneiras (ordenamento, embaralhamento, inversão, ...). Tudo isso graças à forma com que uma lista encadeia os dados, em que cada dado armazenado possui referências tanto a seu sucessor quanto seu antecessor. Pode-se fazer um paralelo com listas reais, que aparecem em diversas situações do dia-a-dia, como estas:


Prg29003-Playlist.jpg
Uma playlist
Prg29003-Lista-tarefas.png
Uma lista de tarefas


Do ponto de vista computacional, podem-se citar estas aplicações de listas:

  • Armazenar um conjunto de dados cuja quantidade não pode ser conhecida de antemão. Exemplos são resultados de consultas a bancos de dados, listagens de arquivos de um diretório, resultados de separação de string em substrings.
  • Armazenar dados cuja ordem em memória é modificada frequentemente. Exemplos são listas de processos em execução mantidas por sistemas operacionais, listas de mensagens a serem transmitidas cuja ordem depende de suas prioridades, listas de tarefas a serem realizadas por um simulador, listas de reprodução em tocadores de músicas.

Praticamente todas as linguagens de programação usadas atualmente possuem sua própria implementação de lista. Seguem alguns exemplos:

A lista da STL

A STL apresenta a estrutura de dados list para armazenamento de sequências de dados, os quais podem ser acessados randomicamente. Em uma lista, dados podem ser adicionados e removidos de qualquer posição com eficiência, porém são acessados de forma iterativa (sempre a partir do início ou final da lista). Em list, cada dado ocupa uma área de memória sob medida, e para formar uma sequência essas áreas de memória são encadeadas (ligadas). Como consequência, os dados não ficam contíguos em memória. Por exemplo, supondo que tenha sido criada uma lista chamada numeros, e nela tenham sido adicionados os números 10, 20, 30 e 40 (nessa ordem), o armazenamento desses números em memória poderia ser este:



Prg2-Lista.png
Uma lista com alguns dados


Em geral, list é adequada quando a quantidade de dados a serem armazenados é variável e desconhecida, e quando dados precisam ser inseridos e removidos de qualquer posição. Os pontos listados a seguir buscam esclarecer melhor o que está em jogo.

  • Não é necessária uma área de memória contígua: como dados são armazenados dinamicamente, eles podem ocupar áreas de memória arbitrárias, e não há nenhuma relação entre a localização dos dados em memória e sua ordem na lista.
  • Não é possível indexar os dados, por isso para acessar um dado deve-se obrigatoriamente procurá-lo a partir do início ou fim da lista, seguindo cada sucessor ou antecessor até chegar àquele procurado.
  • Acrescentar um dado implica modificar a referência ao sucessor do dado que deve antecedê-lo na lista. Assim, não é necessário "empurrar" os dados seguintes para frente.
  • Para remover um dado é a mesma coisa: basta modificar a referência de seu antecessor. Assim, não é necessário "deslocar pra trás" os dados seguintes.


As operações elementares de list são:

  • push_back: Adicionar um dado ao final
  • push_front: Adicionar um dado no início
  • pop_front: Remover um dado do início
  • pop_back: Remover um dado do final
  • insert: Inserir um dado em uma determinada posição
  • erase: Remover um ou mais dados a partir de uma determinada posição
  • front: Acessar o dado que está no início
  • back: Acessar o dado que está no final
  • size: Obter a quantidade de dados armazenados
  • clear: Remover todos os dados (esvaziar)


Abaixo segue um exemplo de uso de algumas operações de list:

#include <cstdlib>
#include <list>
#include <iostream>
#include <string>

using namespace std;

void mostra_lista(list<string> & lista) {
    // itera a lista
    for (auto & dado: lista) {
      cout << dado << ",";
    }
    cout << endl;
}

int main(int argc, char** argv) {
    // cria uma lista de string
    list<string> nomes;
    
    // anexa três dados ao final da lista
    nomes.push_back("manuel");
    nomes.push_back("maria");
    nomes.push_back("bilica");
    
    // mostra comprimento e conteúdo da lista
    cout << "Comprimento: " << nomes.size() << ", dados: ";
    mostra_lista(nomes);

    // insere dado no início da lista
    nomes.push_front("maneca");
    cout << "Comprimento: " << nomes.size() << ", dados: ";
    mostra_lista(nomes);

    // remove dado do início da lista
    nomes.pop_front();
    cout << "Comprimento: " << nomes.size() << ", dados: ";
    mostra_lista(nomes);
    
    // ao final, lista é automaticamente destruída, e a memória utilizada
    // é liberada
    return 0;
}


Ao usar essa nova estrutura de dados, existem algumas novidades em comparação com a fila e a pilha. A primeira delas diz respeito à iteração da lista. Tanto a fila quanto a pilha possibilitavam acessar apenas os dados em suas extremidades. A lista é muito mais flexível, possibilitando acessar dados em qualquer posição. No entanto, devido à forma como os dados ficam armazenados dentro da lista, para acessá-los é necessário usar um iterador.

Iteradores

Quando se necessitam acessar em sequência todos (ou uma boa parte) dos dados de uma lista, a melhor forma é por meio da operação de iteração. A lista é capaz de ser iterada por meio de um iterador. Um iterador é um objeto que se assemelha a um ponteiro, e que possibilita acessar um dado da lista, além de avançar para o dado seguinte ou retroceder para o dado anterior. As operações begin e end da lista retornam, respectivamente, iteradores para o início ou fim da lista. O exemplo a seguir mostra como usá-los:

#include <iostream>
#include <list>

using namespace std;

int main() {
  list<int> numeros;

  numeros.push_back(34);
  numeros.push_back(7);
  numeros.push_back(21);
  numeros.push_back(8);
  numeros.push_back(12);
  numeros.push_back(17);

  // Itera do início ao fim da lista
  // A variável "it" é o iterador, que será usado para acessar os dados da lista
  // Note como ao final de cada repetição do laço o iterador é incrementado ... isso
  // faz com que se avance para o próximo dado da lista

  for (auto it = numeros.begin(); it != numeros.end(); it++) { 
    // acessa o dado atual da iteração: ele é referenciado pelo iterador,
    // como se este fosse um ponteiro

    cout << "Numero: " << *it << endl;
  }
}


Para fins de simplicidade, existe uma sintaxe na linguagem para iterar sequências de dados. Veja o exemplo anterior com essa forma de iterar:

#include <iostream>
#include <list>

using namespace std;

int main() {
  list<int> numeros;

  numeros.push_back(34);
  numeros.push_back(7);
  numeros.push_back(21);
  numeros.push_back(8);
  numeros.push_back(12);
  numeros.push_back(17);

  // Itera do início ao fim da lista
  // O iterador é usado implicitamente. Por isso, no laço a variável de controle 
  // acessa diretamente o dado atual da iteração (no caso, a variável "x").

  for (auto & x: numeros) { 
    // "x" contém o dado atual da iteração
    cout << "Numero: " << x << endl;
  }
}


Deve-se observar que isso é específico de C++: não há algo parecido na linguagem C ! Porém, essa forma simplificada de iterar sequências aparece em outras linguagens (ex: Python).

Iteração reversa

A iteração pode ser feita também em sentido contrário, se for usado um iterador reverso. Os métodos rbegin e rend de list retornam, respectivamente, iteradores reversos para o fim e início da lista:

#include <iostream>
#include <list>

using namespace std;

int main() {
  list<int> numeros;

  numeros.push_back(34);
  numeros.push_back(7);
  numeros.push_back(21);
  numeros.push_back(8);
  numeros.push_back(12);
  numeros.push_back(17);

  // Itera do início ao fim da lista
  // A variável "it" é o iterador, que será usado para acessar os dados da lista
  // Note como ao final de cada repetição do laço o iterador é incrementado ... isso
  // faz com que se retorceda para o próximo dado da lista

  for (auto it = numeros.rbegin(); it != numeros.rend(); it++) { 
    // acessa o dado atual da iteração: ele é referenciado pelo iterador,
    // como se este fosse um ponteiro

    cout << "Numero: " << *it << endl;
  }
}


A sintaxe para iteração simplificada não se aplica à iteração reversa !

Atividade

Objetivo: escrever um programa que mescle as informações contidas em diferentes arquivos, eliminando informações repetidas

  • Descrição: existe um conjunto de arquivos, os quais contêm dados sobre alunos de uma escola. Cada arquivo corresponde a uma disciplina de algum curso dessa escola. Cada linha desses arquivos tem este formato:
matrícula aluno

... sendo matrícula um número com 8 dígitos, e aluno o nome completo de um aluno.

Há a necessidade de obter uma relação dos alunos matriculados nessas disciplinas, sendo que cada aluno deve aparecer uma única vez.


Para se familiarizar com o uso de listas, resolva primeiro estes exercícios:

  1. Lista de números inteiros
  2. Copiar uma lista usando iteração
  3. Junta strings contidas em uma lista
  4. Filtra valores de uma lista
  5. Lista de contadores
  6. Palavras repetidas
  7. Contador de palavras
  8. A função separa usando lista
  9. Compactador de endereço IPv6

Um resumo sobre a notação para complexidade de algoritmos

Prg2-Big-o-table.jpg
Figura obtida deste artigo sobre estruturas de dados em Python

Operações que reorganizam a lista

Três operações disponíveis na lista reorganizam a ordem dados dados armazenados:

  • ordenamento: ordena os dados de forma eficiente
  • embaralhamento: mistura os dados aleatoriamente
  • inversão: inverte a ordem dos dados

Ordenamento da lista

A lista possui o método sort, que ordena seus dados. O ordenamento é feito por um algoritmo com razoável eficiência (ele tem custo de tempo computacional O(n log n)), e isso é importante porque esse tipo de operação tem custo computacional considerável (pode ser proporcional ao quadrado da quantidade de dados se não for bem feito). O único requisito para ordenar uma lista é que os dados armazenados possuam uma relação de precedência. Em outras palavras, que possam ser comparados com operador < (menor que). O exemplo a seguir mostra o ordenamento de uma lista:

#include <iostream>
#include <list>

using namespace std;

int main() {
  list<int> numeros;

  numeros.push_back(34);
  numeros.push_back(7);
  numeros.push_back(21);
  numeros.push_back(8);
  numeros.push_back(12);
  numeros.push_back(17);

  // ordena a lista
  numeros.sort();

  // mostra o conteúdo da lista
  for (auto & x: numeros) {
    cout << x << endl;
  }

  cout << endl;
}

Como o método sort depende da existência do operador < para o tipo dos dados armazenados, as mesmas considerações feitas quanto à operação de igualdade se aplicam aqui. A próxima subseção mostra um exemplo de ordenamento de uma lista que contém valores de um tipo definido pelo programador.

Ordenamento de uma lista com valores de um tipo definido pelo programador

A operação sort depende da comparação de precedência dos dados contidos numa lista. Isso pode ser feito usando o operador < para o tipo desses dados (caso exista), ou uma função de comparação específica para essa finalidade. O operador < existe na linguagem para tipos básicos, tais como int, float, char e outros. Mas o mesmo não vale para tipos de dados definidos pelo programador, ou mesmo classes. Nesse caso, o programador deve definir o operador < , pois somente ele sabe como valores desses tipos devem ser comparados. Observe-se também que a comparação dos dados não é um problema da lista, que apenas usa a comparação. Em suma, cada tipo e dados ou classe deve saber como comparar seus valores ou objetos, ou deve existir uma função para fazer essa comparação.

O caso mais simples envolve escrever uma função que compare valores do tipo de dados em questão. O exemplo a seguir mostra como essa função deve ser declarada, e depois como ela pode ser utilizada para fazer o ordenamento.

// compara duas strings de acordo com seus comprimentos
bool comp_comprimento(cons string & s1, const string & s2) {
  return s1.size() < s2.size();
}

int main() {
  list<string> l;

  l.push_back("banana");
  l.push_back("caju");
  l.push_back("laranja");
  l.push_back("cajamanga");

  // ordena lista de string de acordo com comprimentos das string
  l.sort(comp_comprimento);

  // mostra conteúdo da lista na tela
  for (auto & w: l) {
    cout << w << endl;
  }
}


A linguagem C++ possibilita definir como um determinado operador deve funcionar. Isso aplicado ao operador < resolve o problema da comparação de um tipo de dados definido pelo programador. A implementação de um operador pode ser feita de duas maneiras:

  1. Incluindo-o ao tipo struct ou à classe:
    struct Registro {
      // atributos do tipo Registro
    
      bool operator<(const Registro & outro) const;
    };
    
  2. Criando uma função:
    // compara "este" com "outro"
    bool operator<(const Registro & este, const Registro & outro) const {
      // implementação da comparação: verifica se "este" < "outro"
    }
    


Ambas as formas de implementar um operador são válidas e resolvem o problema. Uma observação diz respeito à implementação com uma função, a qual tem precedência sobre a implementação dentro da struct ou classe. Isso significa que mesmo que já exista o operador em questão definido dentro de uma struct ou classe, ele pode ser substituído por outra implementação desse operador em uma função. A isso se chama sobrecarga de operador.


O exemplo a seguir mostra um programa que cria uma lista com valores de um novo tipo de dados, e a ordena.

Exemplo com o operador< como um método do novo tipo de dados
#include <iostream>
#include <string>
#include <list>
 
using namespace std;

// Tipo Alguem: representa uma pessoa com seu nome e idade 
struct Alguem {
    string nome;
    int idade;
    
    Alguem() {
        idade = 0;        
    }
    
    Alguem(const string & umNome, int age) {
        nome = umNome;
        idade = age;
    }
    
    // sobrecarga do operador< para o tipo Alguem
    bool operator<(const Alguem & o) {
        if (idade == o.idade) {
            return nome < o.nome;
        }
        return idade < o.idade;
    }
    
};

int main() {
  list<Alguem> l;
  
  Alguem joao("Joao", 20);
  l.push_back(joao);
  
  Alguem x1("Amanda", 15);
  l.push_back(x1);
  
  Alguem x2("Gabriel", 19);
  l.push_back(x2);
  
  l.push_back(Alguem("Isadora", 18));
  l.push_back(Alguem("Gustavo", 18));
  
  // ordena a lista
  l.sort();
 
  // mostra a lista ...
  for (auto & pessoa: l) {
    cout << pessoa.nome << ": " << pessoa.idade << endl;
  }
  
  cout << endl;
}
Exemplo do operador< como uma função
#include <iostream>
#include <string>
#include <list>
 
using namespace std;

// Tipo Alguem: representa uma pessoa com seu nome e idade 
struct Alguem {
    string nome;
    int idade;
    
    Alguem() {
        idade = 0;        
    }
    
    Alguem(const string & umNome, int age) {
        nome = umNome;
        idade = age;
    }        
};

// sobrecarga do operador< para o tipo Alguem
bool operator<(const Alguem & este, const Alguem & outro) {
    if (este.idade == outro.idade) {
        return este.nome < outro.nome;
    }
    return este.idade < outro.idade;
}


int main() {
  list<Alguem> l;
  
  Alguem joao("Joao", 20);
  l.push_back(joao);
  
  Alguem x1("Amanda", 15);
  l.push_back(x1);
  
  Alguem x2("Gabriel", 19);
  l.push_back(x2);
  
  l.push_back(Alguem("Isadora", 18));
  l.push_back(Alguem("Gustavo", 18));
  
  // ordena a lista
  l.sort();
 
  // mostra a lista ...
  for (auto & pessoa: l) {
    cout << pessoa.nome << ": " << pessoa.idade << endl;
  }
  
  cout << endl;
}


Inversão

A inversão da lista, implementada pela operação reverse, envolve inverter a ordem dos dados nela armazenados: o primeiro se torna o último, o segundo o penúltimo, e assim por diante. O algoritmo envolvido tem custo de tempo computacional O(n).

Seu uso é direto, e não há dependência a qualquer operador do tipo dos dados armazenados. Um exemplo de uso é este:

#include <iostream>
#include <list>

using namespace std;

int main() {
  list<int> numeros;

  numeros.push_back(34);
  numeros.push_back(7);
  numeros.push_back(21);
  numeros.push_back(8);
  numeros.push_back(12);
  numeros.push_back(17);

  // ordena a lista
  numeros.sort();

  // ... e agora a inverte, para obter um ordenamento decrescente
  numeros.reverse();

  // apresenta a lista
  for (auto & x: numeros) {
    cout << x << endl;
  }

}

Atividade

Faça estes exercícios que envolvem ordenamento:

  1. Lista ordenada de números inteiros
  2. Ordenar linhas de um arquivo
  3. Ordenar linhas de um arquivo de acordo com comprimentos das linhas


Curiosidade: contando quantas vezes cada valor existe em uma lista

Em um projeto sobre estatísticas sobre filmes e atores, ralizado em 2019.2, o requisito "listar os atores que mais atuaram, por ordem decrescente de atuações em filmes" poderia ser resolvido de mais de uma maneira. Uma abordagem é criar uma lista contendo todos os nomes de atores que atuaram nos filmes, de forma que, se um ator trabalhou em três filmes, seu nome apareceria três vezes nessa lista. Basicamente isso implica listar os atores de cada filme, e acrescentá-los a essa nova lista. Ao final, bastaria contar quantas vezes cada ator aparece na lista.

Ao menos dois algoritmos podem ser pensados para esse problema:

  1. Para cada ator da lista, iterar a lista para contar quantas vezes seu nome aparece. Há que cuidar para realizar essa procura somente para a primeira vez em que cada ator é avaliado.
  2. Ordenar a lista de atores, e então iterá-la. Os nomes de atores ficarão contíguos, o que facilita contá-los. Se o próximo ator da iteração for diferente do anterior, então registra-se a contagem do ator anterior e reinicia-se o contador.


Qual dos dois algoritmos é melhor, do ponto de vista de custo computacional (tempo para que concluam) ? O gráfico abaixo responde essa questão !

PRG2-Conta repetidos.png
Custo computacional dos algoritmos para contar valores repetidos em uma lista, para listas de números inteiros

Uma alternativa à lista: vetor dinâmico

A STL apresenta uma outra estrutura de dados linear chamada vector. Essa estrutura se apŕesenta como um vetor dinâmico, em que dados são armazenados de forma parecida com um vetor, porém cuja área de armazenamento em memória pode ser expandida automaticamente. Em vector, os dados estão sempre contíguos em memória. Um vector usa uma área de memória capaz de guardar a quantidade de dados armazenados, estando os dados gravados sequencialmente ali dentro. A figura a seguir mostra como um vector usa memória para armazenar dados.

Prg2-Vector.png
Um vector com alguns dados armazenados


Em geral, vector é adequado quando os dados são armazenados e removidos do final da área de armazenamento, e também quando se precisam acessá-los diretamente (por suas posições) e/ou aleatoriamente. Com list é o contrário, pois ela é adequada quando dados precisam ser inseridos e removidos de qualquer posição. Cabe ao programador escolher a estrutura mais adequada em cada situação. Os pontos listados a seguir buscam esclarecer melhor o que está em jogo.

  • Com list, não é necessária uma área de memória contígua: como dados são armazenados dinamicamente, eles podem ocupar áreas de memória arbitrárias, e não há nenhuma relação entre a localização dos dados em memória e sua ordem na lista. No caso de vector, a localização dos dados em memória está diretamente ligada a suas posições na área de armazenamento.
  • Com list não é possível indexar os dados, por isso para acessar um dado deve-se obrigatoriamente procurá-lo a partir do início ou fim da lista, seguindo cada sucessor ou antecessor até chegar àquele procurado.
  • Com vector podem-se indexar os dados, acessando-os diretamente por suas posições, uma vez que suas localizações em memória podem ser calculadas em função de suas posições.
  • Com list, acrescentar uma dado implica modificar a referência ao sucessor do dado que deve antecedê-lo na lista. Assim, não é necessário "empurrar" os dados seguintes para frente (como seria o caso quando se usa vector).
  • Para remover um dado é a mesma coisa: basta modificar a referência de seu antecessor. Assim, não é necessário "deslocar pra trás" os dados seguintes (como seria o caso de vector).


As operações que podem ser feitas em vector são parecidas com as operações de list.

  • push_back: Adicionar um dado ao final
  • insert: Inserir um dado em uma determinada posição
  • erase: Remover um ou mais dados a partir de uma determinada posição
  • front: Acessar o dado que está no início
  • operador []: Acessar um dado em uma posição qualquer
  • back: Acessar o dado que está no final
  • size: Obter a quantidade de dados armazenados
  • clear: Remover todos os dados (esvaziar)


Abaixo segue um exemplo de uso de algumas operações de vector:

#include <cstdlib>
#include <vector>
#include <iostream>
#include <string>

using namespace std;

void mostra_vetor(vector<string> & v) {
    // itera o vetor
    for (auto & dado: v) {
      cout << dado << ",";
    }
    cout << endl;
}

int main(int argc, char** argv) {
    // cria um vector de string
    vector<string> nomes;
    
    // anexa três dados ao final do vector
    nomes.push_back("manuel");
    nomes.push_back("maria");
    nomes.push_back("bilica");
    
    // mostra comprimento e conteúdo do vector
    cout << "Comprimento: " << nomes.size() << ", dados: ";
    mostra_vetor(nomes);

    // Acessa um dado por sua posição
    for (int i=0; i < v.size(); i++) {
      cout << "Dado na posição " << i << ": " << nomes[i] << endl;
    }

    // remove dado do final do vector
    nomes.pop_back();
    cout << "Comprimento: " << nomes.size() << ", dados: ";
    mostra_vetor(nomes);
    
    // ao final, vector é automaticamente destruído, e a memória utilizada
    // é liberada
    return 0;
}


Nem tudo que se pode fazer com list está disponível em vector:

  • Ordenamento: não há uma operação de ordenamento para vector, porém é possível ordená-los por meio de um algoritmo existente na STL.
  • Embaralhamento: não existe uma operação para embaralhar os dados em um vector, mas também há um algoritmo para essa finalidade na STL.
  • Reversão: não há operação para inverter as ordens dos dados, o que também depende de um algoritmo da STL.
  • ... e algumas outras operações de list !


Por fim, assim como emlist, iteradores são úteis para acessar dados e percorrer vector.