1 Objetivos 2 Parte I: Conceitos de Lista - DIMAp
Transcrição
1 Objetivos 2 Parte I: Conceitos de Lista - DIMAp
Universidade Federal do Rio Grande do Norte Departamento de Informática e Matemática Aplicada – DIMAp Prática de Algoritmos e Estrutura de Dados I – DIM0426 Umberto Souza da Costa - 25/09/2007 – Quarta Lista de Exercı́cios – Implementação e uso de Listas Encadeadas 1 Objetivos O objetivo desta aula é estudar as estruturas de dados lista encadeada simples e listas circulares enfatizando sua implementação através de classes em C++. Nesta aula também exploraremos uma aplicação que deve fazer uso de uma lista circular em sua solução. Espera-se que assim seja possı́vel perceber a importância das listas encadeadas como estrutura de dados na solução de problemas práticos. A lista encadeada pode ser utilizada, por exemplo, na implementação de pilhas, filas e deques, bem como na solução de alguns problemas computacionais que requerem tal estrutura. Os programas e documentações devem entregues até a próxima aula prática (veja a Seção 4 para mais detalhes). 2 Parte I: Conceitos de Lista Encadeada Simples Uma Lista Encadeada é uma estrutura de dados do tipo container, ou seja, serve para armazenar elementos em uma certa ordem. A lista encadeada oferece operações de acesso geral, tais como inserção, remoção e busca arbitrária. Uma das caracterı́sticas mais importantes de uma lista encadeada é seu caráter dinâmico, que permite armazenar um número de elementos limitado apenas pela memória disponı́vel. Uma lista encadeada consiste de uma seqüência linear de nós dinamicamente alocados, que são encadeados (ou conectados) através de ponteiros (ou apontadores). Versões mais elaboradas de listas encadeadas utilizam nós com ponteiros para os nós sucessor e antecessor (listas duplamente encadeadas) e outras fazem com que o último nó aponte para o primeiro (listas circulares). No nosso caso começaremos trabalhando com lista encadeada simples, cujo último nó aponta para NULL (aterramento). Uma estrutura básica representando essa idéia é demonstrada abaixo: template< typename T > class ListNode { public: T elemento; ListNode *prox; }; Para facilitar a implementações dos algoritmos de manipulação de uma lista encadeada simples, é possı́vel fazer uso de um nó especial chamado de nó-cabeça. Este nó especial tem as seguintes caracterı́sticas: ⋆ É o nó apontado pelo ponteiro de inı́cio de lista (ptLista); 4a Lista: Implementação de Lista Encadeada Simples UFRN/DIM0426/2007.2 ⋆ Nunca é removido; ⋆ Não contém valor válido. Porém, em algumas ocasiões pode conter informações relacionadas a lista, como, por exemplo, a quantidade de nós ou um ponteiro para o último elemento da lista. Desta forma o primeiro nó de uma lista (nó-cabeça) é acessı́vel via ponteiro especial (ptLista) e a partir dele é possı́vel realizar o acesso seqüêncial à todos os elementos da lista. A Figura 1 apresenta alguns exemplos esquemáticos de listas. ptLista ptLista A Nó cabeça B D Nó cabeça (a) (b) Figura 1: Exemplo de (a) uma lista encadeada com nó-cabeça vazia; e (b) uma lista encadeada simples com nó-cabeça e 3 elementos, aterrada em D. 2.1 Busca Uma das operações mais importantes sobre lista é a busca, que consiste em localizar um determinado elemento na lista. A importância da busca deve-se ao fato de que ela é invocada pelas operações de inserção (descrita na Seção 2.2) e remoção (descrita na Seção 2.3). A operação de busca consiste em percorrer a lista seqüencialmente a partir do primeiro elemento válido até o final da mesma, procurando pela primeira1 ocorrência do elemento solicitado. Uma das formas mais comum de implementar a busca consiste fazer com que ela retorne dois apontadores: um para a primeira ocorrência na lista do elemento procurado (pont) e outro para o nó imediatamente anterior ao elemento procurado (ante). O apontador para o nó imediatamente anterior será utilizado nas operações de inserção e remoção. Se o elemento solicitado não estiver na lista então os apontadores pont deve apontar para um valor nulo (i.e. NULL). Um possı́vel pseudo-código para o algoritmo de busca encontra-se no Código 1. 2.2 Inserção O processo de inserção em uma lista deve ser bem planejado para evitar que a lista se “quebre” ou a inserção seja feita em local inapropriado. A inserção pode ser implementado de várias maneiras, adicionando-se o elemento: ⋆ Ao final da lista; ⋆ No inı́cio da lista; ⋆ De forma a preservar uma ordem pré-existente da lista; ⋆ Logo após um ponteiro que aponta para um nó válido da lista. 1 Note que é possı́vel que uma mesma lista possua mais de uma instância do elemento solicitado. Página 2 4a Lista: Implementação de Lista Encadeada Simples UFRN/DIM0426/2007.2 Código 1 Pseudo-código do algoritmo de busca em lista encadeada simples. 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: 17: Funç~ ao buscaLES( x: Tipo Chave; Ref ante↑, pont↑: Tipo ListNode ) : Neutro | Var pt↑: ListNode; % pt: ponteiro de percurso | ante := ptLista; % aponta p/ nó-cabeça | pont := λ; % assumimos que chave n~ ao está na lista | pt := ptLista↑.prox; % pt aponta para o 1o nó válido | Enquanto ( pt 6= λ ) Faça | | Se ( pt ↑.chave < x ) Ent~ ao % avança ponteiro anterior | | | ante := pt; | | | pt := pt↑.prox; % atualiza ponteiro de percurso | | Sen~ ao Se ( pt ↑.chave = x ) Ent~ ao | | | | | | | pont := pt; % apontar p/ o nó encontrado | | | Fim-Se | | | pt := λ; % forçar saı́da | | Fim-Se Fim-Enquanto | Fim-Funç~ ao. A última opção é a mais versátil, pois basta fazer o ponteiro indicador de inserção apontar para posição desejada para que a inserção ocorra de forma correta. Neste caso cabe ao programador certificar-se que o ponto de inserção manterá as caracterı́sticas desejadas da lista (por exemplo, manter os elementos ordenados). O Código 2 apresenta um pseudo-código para a inserção feita após um nó indicando ao algoritmo. Se o elemento a ser inserido já estiver na lista o algoritmo não faz nada. Código 2 Pseudo-código do algoritmo de inserção em lista encadeada simples. 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: Funç~ ao insereLES( x: Tipo Chave; novoValor: Tipo Objeto ) : Neutro | Var pt↑, pont↑, ante↑: ListNode; % ponteiros p/ auxiliar inserç~ ao | buscaLES( x, ante, pont ); % achar posiç~ ao para inserç~ ao | Se ( pont = λ ) Ent~ ao % elemento n~ ao está na lista! inserir... | | Alocar( pt ); % solicitar nó | | pt↑.info := novoValor; % inicializar nó | | pt↑.chave := x; % inicializar chave | | pt↑.prox := ante↑.prox; % ligar novo nó à lista | | ante↑.prox := pt; % acertar lista | Fim-Se Fim-Funç~ ao. A Figura 2 apresenta o processo de inserção de um novo nó segundo o algoritmo apresentado no Código 2. 2.3 Remoção Outra operação importante em uma lista encadeada é a remoção. Para remover um elemento é preciso, primeiramente, encontrar o elemento que se deseja remover, mantendo um apontador para o mesmo. O procedimento de remoção é facilitado se tivermos um apontador para a posição imediatamente anterior ao nó que se deseja remover. Uma vez Página 3 4a Lista: Implementação de Lista Encadeada Simples ptLista UFRN/DIM0426/2007.2 ptLista A B D pont = λ ante λ A B D λ D λ Nó cabeça Nó cabeça C λ pt (a) ptLista (b) pont = λ ante A B ptLista D λ Nó cabeça pont = λ ante A B Nó cabeça C C pt pt (c) (d) Figura 2: Exemplo de inserção de um novo nó em uma lista. Em (a) temos a lista encadeada original; (b) o novo nó (conteúdo C) é alocado e apontado por pt; (c) o novo nó é ligado a lista através de seu campo prox, e; (d) o encadeamento da lista é ajustado de forma a incluir o novo nó. que os dois apontadores – para o nó a ser removido e para o nó antecessor imediato – estão configurados a remoção pode ser efetuada de forma simples. Portanto, mais uma vez o procedimento de busca deve ser utilizado. O processo supracitado é ilustrado pela Figura 3 e o algoritmo correspondente pode ser encontrado no Código 3. Código 3 Pseudo-código do algoritmo de remoção em lista encadeada simples. 01: 02: 03: 04: 05: 06: 07: 08: 09: 10: 11: 2.4 Funç~ ao removeLES( x: Tipo Chave ) : Tipo Objeto | Var pont↑, ante↑: ListNode; % ponteiros p/ auxiliar remoç~ ao | valRecuperado: Tipo Objeto; | buscaLES( x, ante, pont ); % achar posiç~ ao para remoç~ ao | Se ( pont 6= λ ) Ent~ ao % elemento foi encontrado! remover... | | ante↑.prox := pont↑.prox; % bypass : acertar lista... | | valRecuperado := pont↑.info; % guardar valor do nó removido | | Liberar( pont ); % liberar memória do nó | Fim-Se | Retornar valRecuperado; Fim-Funç~ ao. Tarefas Implementar um lista encadeada simples com nó cabeça para armazenar um caractere, cuja chave deve ser o próprio caractere. Esta lista deve ser mantida em ordem não decrescente de acordo com a chave. Página 4 4a Lista: Implementação de Lista Encadeada Simples ptLista UFRN/DIM0426/2007.2 ptLista A B C D λ Nó cabeça ante pont A B (a) ptLista C D λ C D λ Nó cabeça ante pont A B (b) ptLista C D λ Nó cabeça ante pont A Nó cabeça (c) (d) Figura 3: Exemplo de remoção de um nó de uma lista. Em (a) temos a lista encadeada original; (b) o novo a ser removido, apontado por pont, é localizado (conteúdo B) e seu antecessor é apontado por ante; (c) o novo a ser removido é contornado (bypass), e; (d) o nó marcado é desalocado. Neste exercı́cio você não deve se preocupar em criar uma classe completa (com construtor, destrutor, etc.), mais uma classe simples (à semelhança de um struct), com membros públicos, conforme exemplo fornecido na Seção 2. Construa um programa de teste que, através de um menu de opções, permita a realização de inserção, busca, remoção e listagem de elementos da lista. Porém, antes de começar você deve tomar uma decisão de implementação: tratar a lista (ptLista) como uma variável global acessı́vel dentro das várias funções de manipulação; ou tratar a lista como uma variável local (ao main) e passar o ponteiro para lista como um argumento para cada uma das funções listadas na Seção 2. 3 3.1 Parte II: Lista Circular e o Problema de Josephus Listas Circulares Uma lista encadeada simples com nó cabeça é uma estrutura de dados importante e pode ter seu desempenho melhorado ainda mais. Isso pode ser feito tornando-a circular, ou seja, ligando-se o último elemento da lista ao primeiro, conforme ilustrado na Figura 4. Com esse configuração é possı́vel percorrer uma lista continuamente, passando por todos os elementos e retornando ao primeiro naturalmente. Assim sua tarefa inicial consiste em criar a classe circular de forma que ela realize um percorrimento circular sobre uma lista. Tente usar o máximo do princı́pio de herança, ou seja, não é necessário alterar a classe pai e a classe filha deve sobrecarregar apenas os métodos que tiverem comportamento diferentes da classe original. Sinta-se livre para criar novos métodos se necessário. Na próxima seção será descrito um problema cuja solução naturalmente requer o uso de uma lista circular. 3.2 O Problema de Josephus (Roleta Romana) Um grupo de S soldados é circundado por uma força inimiga esmagadora. Não há esperança de vitória e só existe um cavalo disponı́vel para escapar! Os soldados entram num Página 5 4a Lista: Implementação de Lista Encadeada Simples UFRN/DIM0426/2007.2 Head <null> A B C D Figura 4: Exemplo de uma lista encadeada simples circular com 4 elementos. acordo para determinar qual deles deverá escapar para trazer ajuda. Para tanto eles irão utilizar o método da roleta romana para selecionar o soldado sortudo. O procedimento funciona da seguinte forma: 1. Os soldados se dispõe em cı́rculo formando uma certa seqüência fixa. 2. Um número n > 0 é sorteado. Um dos nomes dos soldados também é sorteado. 3. Começando-se pelo soldado cujo nome foi sorteado, eles iniciam uma contagem seqüencial ao longo do cı́rculo em sentido horário. Quando a contagem alcança n-ésimo soldado, ele é retirado do cı́rculo (e portanto eliminado da escolha) e a contagem reinicia com o soldado seguinte. 4. O processo continua de maneira que, toda vez que n é alcançado, outro soldado é eliminado do cı́rculo (lembre-se, todo soldado retirado do cı́rculo não entra mais na contagem). 5. O último soldado que restar deverá montar o cavalo e escapar. Considerando como entrada um número n > 0, a ordem dos soldados no cı́rculo e o soldado a partir do qual a contagem se inicia, o objetivo é determinar a seqüência na qual os soldados são eliminados do cı́rculo e o soldado que escapará. A entrada para o programa é uma seqüência de nomes dos soldados até que <CTRL>+d seja pressionado. A ordem de entrada dos nomes corresponde a ordem da lista, ou seja, o primeiro nome fornecido é o primeiro da lista, o segundo nome informado será o segundo da lista e assim por diante. O programa deve imprimir os nomes na seqüência em que os mesmos são eliminados e o nome do soldado que escapará. Por exemplo, suponha que n = 3 e que existam 5 soldados romanos chamados A, B, C, D e E. Digamos que A tenha sido o nome sorteado para iniciar o procedimento de eliminação. Então contamos três soldados a partir de A de forma que C é o soldado eliminado. Em seguida, começamos em D e contamos D, E e novamente A, para que A seja eliminado a seguir. Depois B, D e E é o eliminado da vez; e finalmente, B, D e B, de modo que D seja o soldado a escapar. Veja logo abaixo como poderia ser e interface do programa executado para o exemplo acima: $ ./josephus Entre com n (> 0): 3 Nome dos soldados (digite <CTRL>+d para encerrar): Página 6 4a Lista: Implementação de Lista Encadeada Simples UFRN/DIM0426/2007.2 A B C D E ------------------------------------------------Ordem de saı́da: C A E B Soldado sorteado: D 3.3 Tarefas 1. Crie a classe que implemente o percorrimento circular sobre uma lista. Sinta-se livre para criar novos métodos se necessário. 2. Utilize a lista encadeada circular do exercı́cio anterior para resolver o problema de Josephus descrito na Seção 3.2. 4 Implementação, Entrega e Avaliação O trabalho deve ser desenvolvido em duplas, tentando, dentro do possı́vel, dividir as tarefas igualmente entre os componentes. Porém os componentes devem ser capazes de explicar qualquer trecho de código do programa, mesmo que o código tenha sido desenvolvido pelo outro membro da equipe. Os programas completos e sua documentação html (gerada pelo Doxygen) devem ser entregues até a próxima aula prática, sem erros de compilação e testado. A entrega da códigos-fonte e seus arquivos relacionados deverá ser feita por email para [email protected], até o prazo indicado. O não cumprimento das instruções de envio poderá acarretar em penalização por pontos. – FIM – Página 7