Filas
Autor(es): Jairo Litman
Bem-vindo ao capítulo sobre filas! As filas (ou queues em inglês) são outra estrutura de dados linear fundamental, muito parecida com as pilhas, mas com um princípio de funcionamento diferente. Elas são a base de muitos processos computacionais que gerenciam tarefas e recursos de forma ordenada.
As filas são usadas em toda parte em ciência da computação. Elas seguem o princípio FIFO (First In, First Out), ou “Primeiro a Entrar, Primeiro a Sair”. Pense em uma fila de pessoas esperando para comprar ingressos: a primeira pessoa que chegou é a primeira a ser atendida. Este conceito é crucial para sistemas de agendamento, gerenciamento de buffers (como em streaming de vídeo) e algoritmos de busca em grafos. Entender filas ajudará você a projetar sistemas mais justos e eficientes.
Neste capítulo, vamos explorar os conceitos fundamentais das filas. Abordaremos:
o princípio FIFO.
enqueue (enfileirar, ou inserir um elemento), dequeue (desenfileirar, ou remover um elemento), peek (visualizar o elemento da frente) e isEmpty (verificar se a fila está vazia).
(filas estáticas e circulares) e ponteiros (filas dinâmicas com listas encadeadas).
de processos de sistemas operacionais, gerenciamento de buffers de impressão e algoritmos de busca em largura (BFS).
Uma fila é uma estrutura de dados linear que segue o princípio FIFO (First In, First Out), que significa “Primeiro a Entrar, Primeiro a Sair”. Em uma fila, os elementos são inseridos em uma extremidade (chamada de “traseira” ou “fim”) e removidos da outra extremidade (chamada de “frente” ou “início”).
Imagine uma fila em um supermercado. Novos clientes entram no final da fila, e o caixa atende quem está no início da fila. O primeiro cliente a entrar na fila é o primeiro a sair.
removido.
Vamos visualizar o processo de enfileirar (enqueue) e desenfileirar (dequeue) com números. Começamos com uma fila vazia.
Filas são essenciais em muitos sistemas computacionais:
estão esperando pelo tempo de CPU.
armazenar dados temporariamente enquanto são processados.
ordem em que foram recebidos.
e grafos usa uma fila para rastrear os nós a visitar.
feitas).
overflow (fila cheia) ou desperdício de espaço.
Assim como as pilhas, as filas podem ser implementadas em C usando arrays ou listas encadeadas. Vamos explorar ambas as abordagens.
Uma implementação simples usa um array de tamanho fixo. Precisamos de dois índices: front (frente) e rear (traseira).
Quando front == rear, a fila está vazia.
#define MAX_SIZE 100
typedef struct {
int items[MAX_SIZE];
int front;
int rear;
} Queue;
// Inicializa a fila
void initQueue(Queue *q) {
q->front = 0;
q->rear = 0;
}
// Verifica se a fila está vazia
int isEmpty(Queue *q) {
return q->front == q->rear;
}
// Verifica se a fila está cheia
int isFull(Queue *q) {
return q->rear == MAX_SIZE;
}
// Adiciona um elemento (enqueue)
void enqueue(Queue *q, int data) {
if (isFull(q)) {
// Tratar erro de fila cheia
return;
}
q->items[q->rear] = data;
q->rear++;
}
// Remove um elemento (dequeue)
int dequeue(Queue *q) {
if (isEmpty(q)) {
// Tratar erro de fila vazia
return -1; // Valor de erro
}
int data = q->items[q->front];
q->front++;
return data;
}Problema: Nessa implementação simples, front e rear só avançam. Mesmo depois de desenfileirar elementos, o espaço no início do array não é reutilizado. Isso é ineficiente.
Para resolver o problema anterior, usamos uma fila circular. Quando front ou rear atingem o final do array, eles “dão a volta” para o início (índice 0). Usamos o operador módulo (%) para isso.
typedef struct {
int items[MAX_SIZE];
int front;
int rear;
int count; // Número de elementos na fila
} CircularQueue;
void initCircularQueue(CircularQueue *q) {
q->front = 0;
q->rear = 0;
q->count = 0;
}
int isCircularEmpty(CircularQueue *q) {
return q->count == 0;
}
int isCircularFull(CircularQueue *q) {
return q->count == MAX_SIZE;
}
void enqueueCircular(CircularQueue *q, int data) {
if (isCircularFull(q)) {
return; // Fila cheia
}
q->items[q->rear] = data;
q->rear = (q->rear + 1) % MAX_SIZE; // Avança circularmente
q->count++;
}
int dequeueCircular(CircularQueue *q) {
if (isCircularEmpty(q)) {
return -1; // Fila vazia
}
int data = q->items[q->front];
q->front = (q->front + 1) % MAX_SIZE; // Avança circularmente
q->count--;
return data;
}Ambas as implementações de filas usando arrays são eficientes, mas têm a limitação do tamanho fixo. Para resolver isso podemos usar arrays dinâmicos (realocação) ou listas encadeadas.
A implementação mais flexível usa uma lista encadeada. Usamos ponteiros head e tail (ou front e rear) como vimos no capítulo de Listas Encadeadas.
tail).
Usaremos a struct Node do capítulo anterior.
typedef struct {
Node *front; // Ponteiro para o início (head)
Node *rear; // Ponteiro para o fim (tail)
} LinkedQueue;
void initLinkedQueue(LinkedQueue *q) {
q->front = NULL;
q->rear = NULL;
}
int isLinkedQueueEmpty(LinkedQueue *q) {
return q->front == NULL;
}
void enqueueLinked(LinkedQueue *q, int data) {
Node *newNode = createNode(data); // createNode aloca e inicializa
if (newNode == NULL) {
return; // Falha na alocação
}
if (isLinkedQueueEmpty(q)) {
// Se a fila está vazia, novo nó é frente e traseira
q->front = newNode;
q->rear = newNode;
} else {
// Se não, adiciona após a traseira atual
q->rear->next = newNode;
q->rear = newNode; // Atualiza a traseira
}
}
int dequeueLinked(LinkedQueue *q) {
if (isLinkedQueueEmpty(q)) {
return -1; // Fila vazia
}
Node *temp = q->front;
int data = temp->data;
q->front = q->front->next; // Avança a frente
// Se a fila ficou vazia, atualiza a traseira para NULL
if (q->front == NULL) {
q->rear = NULL;
}
free(temp); // Libera o nó removido
return data;
}