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