Aula 9b - Filas
Nesta aula estudamos o TAD Fila, a segunda estrutura especializada desta unidade. Assim como a pilha (Aula 9a), a fila restringe onde inserções e remoções podem ocorrer — mas em vez de restringir tudo a uma única ponta, ela usa duas pontas diferentes: uma para inserir, outra para remover.
1. O TAD Fila
Uma fila (queue) é uma lista em que inserções ocorrem sempre em uma extremidade (o final, ou rear) e remoções sempre na outra (a frente, ou front). Isso faz com que o primeiro elemento inserido seja sempre o primeiro a ser removido — comportamento conhecido pela sigla FIFO (First-In, First-Out).
frente final | | v v +-------+ +-------+ +-------+ +-------+ | A | --> | B | --> | C | --> | D | +-------+ +-------+ +-------+ +-------+ (sai daqui) (entra aqui)
A analogia mais comum é a de uma fila de banco ou de fila de impressão: quem chega primeiro é atendido primeiro; novas chegadas entram sempre no final da fila.
Operações
| Operação | Descrição |
|---|---|
| enfileirar (enqueue) | Insere um novo elemento no final da fila. |
| desenfileirar (dequeue) | Remove e retorna o elemento da frente da fila. |
| frente (front) | Retorna o elemento da frente, sem removê-lo. |
| estaVazia | Indica se a fila não possui elementos. |
| tamanho | Retorna o número de elementos na fila. |
2. Implementação por lista encadeada
Assim como fizemos com a pilha (Aula 9a), implementamos a fila principal desta disciplina sobre uma lista encadeada, sem limite de capacidade. Diferente da pilha, porém, a fila precisa de acesso rápido às duas pontas — a mesma ideia de ListaDupla (Aula 6a), mas com nodos simplesmente encadeados:
typedef struct { int cod; char nome[50]; float preco; } Produto; typedef struct str_Nodo Nodo; struct str_Nodo { Nodo *prox; Produto dado; }; typedef struct { Nodo *frente; Nodo *final; } FilaEnc;
void inicializa(FilaEnc *f) { f->frente = NULL; f->final = NULL; } int estaVazia(FilaEnc *f) { return f->frente == NULL; } int enfileira(FilaEnc *f, Produto valor) { Nodo *novo = (Nodo*) malloc(sizeof(Nodo)); if (novo == NULL) return 0; novo->dado = valor; novo->prox = NULL; if (f->final != NULL) f->final->prox = novo; // o antigo final passa a apontar para o novo nodo else f->frente = novo; // fila estava vazia: novo nodo também é a frente f->final = novo; return 1; } int desenfileira(FilaEnc *f, Produto *valorRemovido) { Nodo *removido; if (estaVazia(f)) // underflow return 0; removido = f->frente; *valorRemovido = removido->dado; f->frente = removido->prox; if (f->frente == NULL) // a fila ficou vazia f->final = NULL; free(removido); return 1; } int tamanho(FilaEnc *f) { Nodo *aux = f->frente; int contador = 0; while (aux != NULL) { contador++; aux = aux->prox; } return contador; }
Atenção ao caso da fila ficar vazia. Ao desenfileirar o último elemento, f->frente passa a ser NULL — mas se não zerarmos também f->final, ele continuaria "pendurado" apontando para um nodo já liberado (dangling pointer, como vimos no Material Extra — Conceitos de Linguagens de Programação), causando erro na próxima chamada de enfileira.
|
Com os dois ponteiros (frente e final) sempre disponíveis na struct, tanto enfileira quanto desenfileira são O(1), e a fila cresce dinamicamente sem limite predefinido. Por isso adotamos esta implementação como principal para o TAD Fila.
E a implementação por vetor? Uma fila também pode ser implementada sobre um vetor de capacidade fixa, mas exige mais cuidado do que a pilha: se implementada de forma ingênua, desenfileirar se torna O(n) (por deslocar elementos) ou desperdiça espaço já liberado. A solução — uma fila circular, usando aritmética modular — é proposta como exercício ao final da aula.
|
3. Aplicações
- Fila de impressão: documentos são impressos na ordem em que chegaram.
- Escalonamento de processos: um sistema operacional pode atender processos na ordem de chegada (política FCFS, First-Come, First-Served).
- Atendimento e simulações: filas de banco, call centers, simulações de tráfego.
- Buffers de dados: comunicação entre um produtor e um consumidor que trabalham em velocidades diferentes (ex: leitura de rede, áudio/vídeo em streaming).
- Percurso em largura (BFS) em árvores e grafos: um algoritmo que veremos mais adiante na disciplina, e que usa uma fila para visitar elementos "por camadas".
Resumo
|
Exercícios
-
Explique, com suas próprias palavras, o que significa a sigla FIFO e como ela se relaciona com a operação
desenfileira. -
Na implementação por lista encadeada, por que é necessário verificar se
f->frente == NULLapós umdesenfileirabem-sucedido, e atualizarf->finalnesse caso? - Compare pilha (Aula 9a) e fila: as duas restringem onde inserções e remoções podem ocorrer, mas de formas diferentes. Explique essa diferença, e dê um exemplo de problema em que usar uma pilha no lugar de uma fila (ou vice-versa) produziria um resultado incorreto.
-
Por que
enfileiraedesenfileiracontinuam sendoO(1)mesmo a fila guardando dois ponteiros (frenteefinal) em vez de um só, como na pilha? -
Implemente uma função
int filaParaVetor(FilaEnc *f, Produto vetor[])que copie os elementos da fila (da frente para o final) paravetor, sem remover nenhum elemento da fila, e retorne quantos elementos foram copiados. -
Implemente uma função
void inverterFila(FilaEnc *f)que inverta a ordem dos elementos de umaFilaEnc, utilizando umaPilhaEnc(Aula 9a) como estrutura auxiliar. Dica: desenfileire tudo empilhando, depois desempilhe enfileirando de volta. -
Implemente uma função
int filasIguais(FilaEnc *f1, FilaEnc *f2)que retorne1se as duas filas possuem exatamente os mesmos elementos, na mesma ordem, ou0caso contrário — sem alterar (nem remover elementos de) nenhuma das duas filas. -
Implemente a fila circular por vetor. Defina a struct
FilaVet, com um vetor de capacidade fixa (CAPACIDADE), um índicefrentee uma quantidadeqtdde elementos, e implementeinicializa,estaVazia,enfileiraedesenfileira, usando o operador módulo (%) para calcular as posições. Explique por que a struct guardaqtdem vez de um índicefinal.
Sugestões de Respostas dos Exercícios
Exercício 1
FIFO significa First-In, First-Out — "o primeiro a entrar é o primeiro a sair". Isso se relaciona diretamente com desenfileira: essa operação sempre remove o elemento que está na fila há mais tempo (a frente), nunca o elemento inserido mais recentemente.
Exercício 2
Porque, se a fila ficou vazia após a remoção, f->final ainda estaria apontando para o nodo que acabou de ser removido e liberado com free() — um dangling pointer (ver Material Extra — Conceitos de Linguagens de Programação). Se uma próxima chamada de enfileira tentasse usar f->final->prox = novo sem essa verificação, estaria escrevendo em um bloco de memória já liberado, um comportamento indefinido.
Exercício 3
A pilha restringe tudo a uma única extremidade (o topo): tanto inserção quanto remoção acontecem ali, gerando o comportamento LIFO. A fila usa duas extremidades diferentes — uma para inserir (final), outra para remover (frente) — gerando o comportamento FIFO.
Um exemplo prático: em um sistema de atendimento (como uma fila de banco), usar uma pilha por engano faria com que o último cliente a chegar fosse sempre o primeiro a ser atendido, deixando os clientes mais antigos esperando indefinidamente enquanto novos clientes continuassem chegando — claramente incorreto para esse tipo de problema, que exige o comportamento FIFO de uma fila de verdade.
Exercício 4
Porque tanto enfileira quanto desenfileira continuam operando sobre um único ponteiro por vez, sem percorrer a lista: enfileira só lê e escreve em f->final, e desenfileira só lê e escreve em f->frente. Ter dois ponteiros na struct, em vez de um, não significa que uma operação precise mexer nos dois ao mesmo tempo em todos os casos (exceto quando a fila está vazia ou fica vazia) — cada ponteiro atende a uma ponta, de forma independente.
Exercício 5
int filaParaVetor(FilaEnc *f, Produto vetor[]) { Nodo *aux = f->frente; int i = 0; while (aux != NULL) { vetor[i] = aux->dado; i++; aux = aux->prox; } return i; }
Basta percorrer a lista com um ponteiro auxiliar, do mesmo jeito que já fazíamos em imprime (Aula 4) — sem alterar f->frente nem remover nenhum nodo, a fila permanece intacta após a chamada.
Exercício 6
void inverterFila(FilaEnc *f) { PilhaEnc p; Produto valor; inicializa(&p); // esvazia a fila, empilhando cada elemento removido while (!estaVazia(f)) { desenfileira(f, &valor); empilha(&p, valor); } // desempilha de volta para a fila, já na ordem inversa while (!estaVazia(&p)) { desempilha(&p, &valor); enfileira(f, valor); } }
Como a pilha inverte a ordem (LIFO) e a fila preserva a ordem de entrada (FIFO), o truque é usar a pilha como uma etapa intermediária: tudo que sai da fila em uma ordem, entra na pilha e sai dela na ordem inversa — que é justamente o que queremos ao devolver os elementos para a fila.
Exercício 7
int filasIguais(FilaEnc *f1, FilaEnc *f2) { Nodo *aux1 = f1->frente; Nodo *aux2 = f2->frente; while (aux1 != NULL && aux2 != NULL) { // structs não podem ser comparadas com "!=" em C; comparamos campo a campo if (aux1->dado.cod != aux2->dado.cod) return 0; aux1 = aux1->prox; aux2 = aux2->prox; } // iguais somente se as duas terminaram ao mesmo tempo (mesmo tamanho) return aux1 == NULL && aux2 == NULL; }
Usamos dois ponteiros auxiliares, um para cada fila, avançando os dois em paralelo. Como dado é um Produto (uma struct), não podemos compará-lo diretamente com != — por simplicidade, comparamos apenas o campo cod, assumindo que ele identifica o produto de forma única. Se em algum momento os códigos diferirem, as filas já não são iguais. Ao final, é preciso conferir se ambos os ponteiros chegaram a NULL ao mesmo tempo — se um deles ainda tiver elementos e o outro não, as filas têm tamanhos diferentes e não podem ser iguais.
Exercício 8
#define CAPACIDADE 5 typedef struct { Produto dados[CAPACIDADE]; int frente; int qtd; // quantidade de elementos atualmente na fila } FilaVet; void inicializa(FilaVet *f) { f->frente = 0; f->qtd = 0; } int estaVazia(FilaVet *f) { return f->qtd == 0; } int enfileira(FilaVet *f, Produto valor) { int posicaoFinal; if (f->qtd == CAPACIDADE) // overflow return 0; posicaoFinal = (f->frente + f->qtd) % CAPACIDADE; // "dá a volta" no vetor f->dados[posicaoFinal] = valor; f->qtd++; return 1; } int desenfileira(FilaVet *f, Produto *valorRemovido) { if (estaVazia(f)) // underflow return 0; *valorRemovido = f->dados[f->frente]; f->frente = (f->frente + 1) % CAPACIDADE; // "dá a volta" também aqui f->qtd--; return 1; }
A struct guarda qtd em vez de um índice final porque, em uma fila circular, a condição frente == final seria ambígua: poderia significar tanto "fila vazia" quanto "fila cheia" (quando o índice deu a volta inteira no vetor e coincidiu de novo com frente). Guardar a quantidade de elementos diretamente resolve essa ambiguidade: fila vazia é qtd == 0, fila cheia é qtd == CAPACIDADE, independente da posição relativa dos índices.
Código para Download
O TAD FilaEnc completo, exatamente como visto nesta aula, dividido em interface (filaEnc.h) e implementação (filaEnc.c), junto com um main.c de exemplo:
filaEnc.zip — filaEnc.h, filaEnc.c e main.c.
|