Aula 9c - Deques
Para fechar esta unidade, vemos o TAD Deque (double-ended queue, fila de duas pontas): uma generalização de pilhas e filas, que permite inserir e remover em ambas as extremidades.
1. O TAD Deque
Um deque não impõe a restrição LIFO da pilha nem a restrição FIFO da fila — ele simplesmente permite operar nas duas pontas, deixando a cargo de quem usa o deque decidir que disciplina (LIFO, FIFO, ou uma mistura) faz sentido para o problema:
insere/remove aqui insere/remove aqui
| |
v v
+-------+ +-------+ +-------+
| A | --> | B | --> | C |
+-------+ +-------+ +-------+
Operações
| Operação | Descrição |
|---|---|
| insereInicio | Insere um elemento na frente do deque. |
| insereFim | Insere um elemento no final do deque. |
| removeInicio | Remove e retorna o elemento da frente. |
| removeFim | Remove e retorna o elemento do final. |
| estaVazio | Indica se o deque não possui elementos. |
Repare que uma pilha nada mais é do que um deque em que só usamos insereInicio/removeInicio (ou só insereFim/removeFim); e uma fila é um deque em que usamos insereFim para inserir e removeInicio para remover. O deque é, portanto, mais geral que as duas estruturas anteriores.
2. Implementação: reaproveitando a lista duplamente encadeada
Para que todas as quatro operações de inserção/remoção sejam O(1), precisamos de acesso direto às duas pontas e capacidade de andar nos dois sentidos — exatamente o que a ListaDupla da Aula 6a já oferece. Não é por acaso: um deque é, na prática, uma lista duplamente encadeada usada através de uma interface mais restrita.
// Reaproveitando NodoD e ListaDupla da Aula 6a typedef ListaDupla Deque; void inicializa(Deque *d) { inicializa(d); } int estaVazio(Deque *d) { return d->ini == NULL; } int insereInicioDeque(Deque *d, Produto prod) { return insereInicio(d, prod); } int insereFimDeque(Deque *d, Produto prod) { return insereFim(d, prod); }
Removendo pelas pontas. As Aulas 4/6a implementaram remoção por código (removePorCod), não pelas pontas. Para um deque de verdade, seriam necessárias duas novas funções — removeInicio e removeFim — que removem especificamente o primeiro ou o último nodo, sem buscar por código. A lógica é parecida com removePorCod, mas mais simples: já sabemos exatamente qual nodo remover (d->ini ou d->fim), sem precisar percorrer a lista.
|
3. Aplicações
- Histórico de navegação com "voltar" e "avançar" nos dois sentidos (como já mencionado na Aula 6a) pode ser modelado como um deque;
- Algoritmos de janela deslizante (por exemplo, encontrar o máximo em cada sub-intervalo de tamanho fixo de um vetor) costumam usar um deque para inserir/remover elementos em ambas as pontas conforme a janela se move;
- Escalonadores com work stealing: cada processo/thread mantém uma fila de tarefas em formato de deque — insere e remove suas próprias tarefas de uma ponta, enquanto outros processos ociosos podem "roubar" tarefas da ponta oposta.
Resumo
|
Exercícios
- Explique por que uma pilha e uma fila podem ser vistas como "casos particulares" de um deque.
- Por que a lista duplamente encadeada da Aula 6a é uma base natural para implementar um deque, e não a lista simplesmente encadeada da Aula 4?
- Se você tivesse que implementar um deque usando um vetor (em vez de lista encadeada), que problema da fila por vetor (Aula 9b) você esperaria enfrentar novamente, e como o resolveria?
-
Implemente uma função
int removeInicio(Deque *d, Produto *prodRemovido)que remova o primeiro elemento de um deque (baseado emListaDupla), devolvendo o produto removido viaprodRemovido. -
Implemente o deque por vetor circular. Defina a struct
DequeVet, com um vetor de capacidade fixa (CAPACIDADE), um índicefrentee uma quantidadeqtdde elementos (no mesmo estilo daFilaVetda Aula 9b), e implementeinicializa,estaVazio,insereFimeremoveInicio. Depois, implemente tambéminsereInicioeremoveFim— as duas novas operações que um deque tem e uma fila não tem. Dica: para "andar para trás" a partir defrentesem obter um índice negativo, use(frente - 1 + CAPACIDADE) % CAPACIDADE.
Sugestões de Respostas dos Exercícios
Exercício 1
Porque ambas podem ser expressas usando apenas um subconjunto das operações de um deque: uma pilha usa insereInicio/removeInicio (ou, de forma equivalente, só insereFim/removeFim) — sempre a mesma ponta. Uma fila usa insereFim para inserir e removeInicio para remover — pontas diferentes para cada operação. O deque não impõe nenhuma dessas restrições, permitindo os dois comportamentos (e combinações deles) através da mesma estrutura.
Exercício 2
Porque um deque exige acesso O(1) às duas pontas para inserção e remoção. A lista simplesmente encadeada só oferece isso para o início (Aula 4); remover do final exigiria percorrer a lista inteira para encontrar o penúltimo nodo, tornando essa operação O(n). A lista duplamente encadeada já resolve isso, guardando ini e fim, e permitindo andar nos dois sentidos com ant/prox.
Exercício 3
O mesmo problema da fila ingênua por vetor (Aula 9b): sem tratamento especial, os índices das duas pontas só andariam em uma direção, desperdiçando espaço já liberado. A solução seria a mesma: tratar o vetor como circular, usando aritmética modular (% CAPACIDADE) para calcular as posições das duas pontas, de forma parecida com a fila circular vista na Aula 9b.
Exercício 4
int removeInicio(Deque *d, Produto *prodRemovido) { NodoD *removido; if (d->ini == NULL) // deque vazio return 0; removido = d->ini; *prodRemovido = removido->dado; d->ini = removido->prox; if (d->ini != NULL) d->ini->ant = NULL; else d->fim = NULL; // deque ficou vazio free(removido); return 1; }
A lógica é uma versão simplificada de removePorCod (Aula 6a): como já sabemos que o nodo a remover é exatamente d->ini, não é preciso percorrer a lista para localizá-lo — só tratar a atualização de d->ini (e, se necessário, d->fim, no caso de o deque ficar vazio).
Exercício 5
#define CAPACIDADE 5 typedef struct { Produto dados[CAPACIDADE]; int frente; int qtd; } DequeVet; void inicializa(DequeVet *d) { d->frente = 0; d->qtd = 0; } int estaVazio(DequeVet *d) { return d->qtd == 0; } int insereFim(DequeVet *d, Produto valor) { int posicaoFinal; if (d->qtd == CAPACIDADE) return 0; posicaoFinal = (d->frente + d->qtd) % CAPACIDADE; d->dados[posicaoFinal] = valor; d->qtd++; return 1; } int removeInicio(DequeVet *d, Produto *valorRemovido) { if (estaVazio(d)) return 0; *valorRemovido = d->dados[d->frente]; d->frente = (d->frente + 1) % CAPACIDADE; d->qtd--; return 1; } // As duas operações "novas" em relação à FilaVet da Aula 9b: int insereInicio(DequeVet *d, Produto valor) { if (d->qtd == CAPACIDADE) return 0; d->frente = (d->frente - 1 + CAPACIDADE) % CAPACIDADE; // "anda para trás" d->dados[d->frente] = valor; d->qtd++; return 1; } int removeFim(DequeVet *d, Produto *valorRemovido) { int posicaoFinal; if (estaVazio(d)) return 0; posicaoFinal = (d->frente + d->qtd - 1) % CAPACIDADE; *valorRemovido = d->dados[posicaoFinal]; d->qtd--; return 1; }
insereFim e removeInicio são idênticas às da FilaVet (Aula 9b). As novidades são insereInicio, que precisa "andar para trás" a partir de frente — daí o truque (frente - 1 + CAPACIDADE) % CAPACIDADE, que evita obter um índice negativo do operador módulo em C — e removeFim, que calcula diretamente a posição do último elemento a partir de frente e qtd, sem precisar de um índice fim separado.