Na primeira parte e segunda parte deste artigo, vimos a definição de listas ligadas simples, métodos para inserir, remover e listar nós e um exemplo com uma lista de nós usando uma “struct” para definir cada nó. Agora, vamos implementar uma lista ligada simples com classes e com algumas abordagens um pouco diferentes, relativamente à gestão dos nós, das que vimos nos exemplos anteriores. No fim, poderá encontrar um link para fazer o download de todos os ficheiros com comentários. Nos troços de código aqui exemplificados, não estão contemplados os “include“.
Nesta implementação, os nós são inseridos de forma ordenada e não é permitida a duplicação de valores.
Vamos encontrar 3 classes:
- No (nó)
- Lista
- Iterador
A classe “No” é a mais simples e está reduzida apenas a um ficheiro de cabeçalho (em “No.h”). Vamos ver a definição da classe:
class No { public: No(const int v = 0, No *no = NULL) : valor(v), seguinte(no) {} ~No() {} int valor; No *seguinte; };
No construtor da classe (linha 3), podemos passar, como parâmetros, um valor para o nó e o apontador do próximo nó. Por omissão, o valor é zero e o apontador para o nó seguinte é NULL.
Vejamos agora a definição da classe “Lista” (em “Lista.h”):
class Lista { private: No *cabeca; public: Lista(); ~Lista(); bool vazia(); bool insere(int); bool remove(int); void limpar(); Iterador *obtemIterador(); };
Repare que fazemos aqui uma referência a uma classe “Iterador” que irá servir para listar os nós. Repare também que a cabeça está declarada como “private“, pelo que só poderemos aceder aos nós através do Iterador, como iremos ver mais à frente.
Temos nesta classe, os seguintes métodos e funções:
- vazia() – devolve “verdade” se a lista não tiver nós (lista vazia) ou “falso” se a lista tiver nós.
- insere(int) – Insere um nó, com um número inteiro, na lista de forma ordenada. Devolve “verdade” se o nó foi inserido com sucesso ou “falso” se o número já se encontra armazenado.
- remove(int) – Remove o nó que contem o número inteiro passado como parâmetro. Devolve “verdade” se o nó foi removido com sucesso ou “falso” se o valor não foi encontrado na lista ou esta estiver vazia.
- limpar() – Remove todos os nós da lista.
- obtemIterador() – Devolve um iterador para podermos aceder aos nós da lista, começando pelo nó apontado pela cabeça (primeiro nó).
Implementação da classe (em “Lista.cpp”):
Lista::Lista() { cabeca = new No(); } Lista::~Lista() { if (!vazia()) { limpar(); } delete cabeca; } bool Lista::vazia() { return (cabeca->seguinte==NULL); } bool Lista::insere(int v) { No *pesquisa = cabeca; while (pesquisa->seguinte!=NULL) { No *teste = pesquisa->seguinte; if (teste->valor==v) { return false; } else { if (teste->valor > v) { break; } } pesquisa = pesquisa->seguinte; } No *novo = new No(v, pesquisa->seguinte); pesquisa->seguinte = novo; return true; } bool Lista::remove(int v) { if (vazia()) { return false; } No *pesquisa = cabeca; while (pesquisa->seguinte!=NULL) { No *teste = pesquisa->seguinte; if (teste->valor==v) { pesquisa->seguinte = teste->seguinte; delete teste; return true; } pesquisa = pesquisa->seguinte; } return false; } void Lista::limpar() { while (cabeca->seguinte!=NULL) { No *remove = cabeca->seguinte; cabeca->seguinte = remove->seguinte; std::cout << "A apagar o nó com o valor: " << remove->valor << std::endl; delete remove; } } Iterador *Lista::obtemIterador() { Iterador *i = new Iterador(cabeca); return i; }
Vejamos agora algumas considerações relevantes e algumas diferenças:
- Repare que, no construtor da classe, definimos a cabeça como um nó que, por omissão, terá o seu valor a zero e o apontador para o nó seguinte a NULL (linha 2). Na implementação anterior (partes 1 e 2 deste artigo), a cabeça era definida como um apontador directo ao primeiro nó. Agora, será o apontador “seguinte” do “nó cabeça” que irá apontar para o primeiro nó. Desta forma, simplificamos os métodos de inserção e remoção de nós, pois não iremos precisar de testar se estas operações estão a ser feitas à cabeça ou não, uma vez que a cabeça será sempre vista como um hipotético “primeiro” nó, cujo valor será sempre ignorado.
- Um pormenor do destrutor da classe (linha 5), em vez de uma função recursiva, vamos usar um método diferente para limpar os nós da lista, como iremos ver em seguida. Como a cabeça foi declarada como um objecto da classe “No”, temos que a destruir como a instrução “delete” (linha 9).
- Para testar se a lista está vazia, já não o fazemos testando se a cabeça aponta para NULL, mas sim se o seu apontador “seguinte” é NULL (linha 13).
- Para inserir um novo nó, e como já foi referido, bastará implementar o código como se a inserção fosse feita em qualquer parte da lista. É testado se o valor já se encontra na lista (linha 20), retornando “falso” caso o valor seja encontrado, e procurando um nó cujo valor seja superior ao que se pretende inserir (linha 24), saindo do ciclo caso seja encontrado. No fim, na linha 30, o novo nó será inserido antes do nó que tem um valor maior do que o estamos a inserir ou à cauda, caso o novo valor seja o maior que existe na lista (o ciclo “while” termina sem terem sido encontrados valores superiores). Este processo de inserção é análogo ao que vimos na primeira parte deste artigo.
- A remoção de um nó é feita de forma análoga à remoção de um nó em qualquer parte da lista, ou à cauda, conforme vimos na segunda parte deste artigo, em que vamos testar sempre um nó à frente (linha 41).
- Para limpar a lista, em vez de usarmos um método recursivo, vamos fazendo sucessivas remoções à cabeça (a partir do apontador “seguinte” do “nó cabeça”) até esta ficar vazia (ciclo “while” na linha 53). O “cout” na linha 56, serve apenas para o leitor ter a noção dos nós que são removidos.
- Por fim, temos a função para obter o iterador da lista, que devolve um iterador iniciado com o “nó cabeça” (linha 62). Vamos ver esta classe já a seguir.
Definição da classe “Iterador” (em “Iterador.h”):
class Iterador { private: No *no; public: Iterador(No *no); ~Iterador(); No *obterNo(); };
Aqui temos apenas a função “obterNo()” que devolve o nó que está a ser iterado.
Implementação da classe (em “Iterador.cpp”):
Iterador::Iterador(No *no) { this->no = no; } Iterador::~Iterador() {} No *Iterador::obterNo() { if (no==NULL) { return NULL; } no = no->seguinte; return no; }
Repare que, no construtor, copiamos o apontador recebido (que será a cabeça, caso estejamos a receber este iterador a partir da lista) para o objecto “no” local (linha 2).
Na função “obterNo()”, devemos testar se o iterador já chegou ao fim (linha 8), quando o “no” é NULL. Normalmente, pode-se implementar uma excepção para estes casos, mas aqui limitamos-nos a devolver NULL, o mesmo que acontece quando chegamos ao fim da lista. Na linha 11, avançamos sempre o iterador para o próximo nó antes de o devolver. Desta forma, quando este é criado e recebe o “nó cabeça”, irá devolver o primeiro nó e, quando chegar ao fim, irá devolver NULL. Podemos então usar este iterador, para listar todos os nós da lista, num ciclo até que este devolva NULL.
O exemplo que se segue, tirado do ficheiro “main.cpp”, ilustra este caso:
Iterador *i = lista->obtemIterador(); No *no; while ((no=i->obterNo())!=NULL) { cout << "Valor: " << no->valor << endl; } delete i;
Não vou abordar mais o código do ficheiro “main.cpp”, pois a sua implementação é bastante trivial, no que diz respeito ao uso da classe “Lista”.
Pode fazer o download do ficheiro “classe_lista.zip“ que contem todos os ficheiros das classes e o ficheiro “main.cpp” com comentários. A codificação de caracteres está em UTF-8 e os fins de linha em modo OS X / UNIX.
Observações:
- A implementação de listas, pilhas, filas e outras estruturas semelhantes, existe na biblioteca STL (Standard Template Library) do C++. Porém, para estudo, é sempre boa ideia começar pelas suas próprias implementações para melhor compreende o funcionamento destas estruturas.
- Este exemplo está limitado a valores inteiros. Uma lista pode ser implementada para ser usada de forma genérica com o uso de “templates“. Oportunamente, irei falar sobre classes com “templates“.
Este artigo foi escrito de acordo com a antiga grafia.