Aula 6b - Listas Circulares
Nesta aula vemos uma segunda variação sobre as listas encadeadas: a lista circular, em que o último nodo, em vez de apontar para NULL, aponta de volta para o primeiro — fechando a lista em um "círculo".
1. A ideia central
Em uma lista circular (a partir da simplesmente encadeada da Aula 4), o último nodo aponta para o primeiro em vez de para NULL:
l.ini
|
v
+-------+ +-------+ +-------+
| Dados | --> | Dados | --> | Dados | --+
+-------+ +-------+ +-------+ |
^ |
+------------------------------------+
Não existe mais um nodo cujo prox seja NULL — todos os nodos apontam para algum outro nodo da lista, inclusive o último, que aponta de volta para o início. Isso muda a forma como percorremos a lista: não podemos mais usar aux != NULL como condição de parada, pois esse caso nunca ocorre.
Cuidado. Percorrer uma lista circular com a mesma condição de parada de uma lista comum (while (aux != NULL)) resulta em um laço infinito — o ponteiro nunca chega a NULL, pois sempre há um próximo nodo.
|
2. Percorrendo uma lista circular
A forma usual de percorrer uma lista circular é comparar o ponteiro auxiliar com o próprio início da lista, parando quando voltarmos a ele. Como a comparação só faz sentido depois de avançar pelo menos uma vez, usamos um do-while em vez de um while:
void imprime(ListaCirc *l) { Nodo *aux; if (l->ini == NULL) // lista vazia: nada a imprimir return; aux = l->ini; do { printf("%d - %s - %.2f\n", aux->dado.cod, aux->dado.nome, aux->dado.preco); aux = aux->prox; } while (aux != l->ini); }
Note o teste explícito de lista vazia antes do do-while: como o corpo de um do-while sempre executa pelo menos uma vez, seria necessário garantir que l->ini não é NULL antes de acessar aux->dado.
3. Inserção em uma lista circular
A principal diferença em relação à Aula 4 é que, ao inserir um novo nodo, o último nodo da lista (e não NULL) precisa apontar para o novo início — ou, de forma equivalente, o novo nodo precisa apontar de volta para o início da lista:
int insereInicio(ListaCirc *l, Produto prod) { Nodo *novo = (Nodo*) malloc(sizeof(Nodo)); Nodo *ultimo; if (novo == NULL) return 0; novo->dado = prod; if (l->ini == NULL) { // lista vazia: o novo nodo aponta para si mesmo novo->prox = novo; } else { ultimo = l->ini; while (ultimo->prox != l->ini) // percorre até achar quem aponta para o início ultimo = ultimo->prox; novo->prox = l->ini; ultimo->prox = novo; } l->ini = novo; return 1; }
Observação. Assim como na lista simplesmente encadeada da Aula 4, localizar o último nodo aqui custa O(n). Assim como discutimos na Aula 6a, guardar também um ponteiro para o último nodo na struct da lista resolveria esse problema — a mesma ideia se aplica a uma lista circular.
|
4. Aplicações
Listas circulares são úteis sempre que o conceito de "início" e "fim" não faz muito sentido, e o que importa é poder percorrer indefinidamente os elementos, voltando ao começo depois do último:
- Escalonamento round-robin: um sistema operacional distribuindo tempo de processador entre processos, sempre voltando ao primeiro processo da fila após passar pelo último;
- Buffers circulares: estruturas que reaproveitam posições já utilizadas, comuns em sistemas de streaming e comunicação;
- Jogos por turno: alternar entre jogadores de forma cíclica (jogador 1, 2, 3, ..., voltando ao jogador 1).
Resumo
|
Exercícios
-
Por que a condição
while (aux != NULL), usada para percorrer listas na Aula 4, nunca é verdadeira em uma lista circular? -
Por que a função
imprimedesta aula verifical->ini == NULLantes dodo-while, em vez de deixar essa verificação apenas na condição de parada do laço? - Cite um cenário (diferente dos vistos em aula) em que uma lista circular seria uma escolha natural, e explique por quê.
-
Implemente uma função
int tamanho(ListaCirc *l)que retorne o número de nodos de uma lista circular, tomando cuidado para não entrar em laço infinito.
Sugestões de Respostas dos Exercícios
Exercício 1
Porque, em uma lista circular, não existe nenhum nodo cujo prox seja NULL — mesmo o último nodo aponta de volta para o início da lista. Como a condição aux != NULL nunca se torna falsa, um laço que dependa dela nunca termina.
Exercício 2
Porque o corpo de um do-while sempre é executado pelo menos uma vez, antes mesmo de a condição ser testada. Se a lista estivesse vazia (l->ini == NULL) e não houvesse essa verificação antes, a primeira iteração tentaria acessar aux->dado com aux valendo NULL, o que é um erro.
Exercício 3
Um exemplo é um carrossel de imagens (slideshow) em um site: ao chegar na última imagem e clicar em "próxima", o usuário espera voltar para a primeira, e não receber um erro de "fim da lista". Representar as imagens como uma lista circular torna essa navegação natural, sem precisar de tratamento especial para o caso de "chegar ao fim".
Exercício 4
int tamanho(ListaCirc *l) { Nodo *aux; int contador = 0; if (l->ini == NULL) return 0; aux = l->ini; do { contador++; aux = aux->prox; } while (aux != l->ini); return contador; }
O padrão é o mesmo de imprime: trata o caso da lista vazia separadamente, e usa do-while com aux != l->ini como condição de parada, evitando o laço infinito discutido no Exercício 1.