← Todas as estruturas

Queue / Deque

Linear · ver código-fonte no GitHub

Leia em: English · Português · Español

Categoria: Linear

O problema

Um array comum (ou o próprio Dynamic Array deste repositório) só é eficiente numa ponta: adicionar no final é O(1) amortizado, mas remover ou inserir no início é O(n), porque todo elemento restante precisa deslocar. Algumas cargas de trabalho reais realmente precisam das duas pontas — processamento FIFO que também precisa, ocasionalmente, furar a fila — e deslocar o buffer inteiro a cada operação no início não é aceitável assim que o buffer cresce.

A solução

Mantenha um array bruto como um buffer circular: em vez de sempre começar os dados ativos no índice 0, acompanhe um cursor head e um size, e deixe que o final lógico dê a volta pelo fim do array de volta ao início via aritmética modular. Adicionar ou remover em qualquer ponta então sempre toca apenas um slot e move um cursor — sem deslocamento, não importa a qual ponta a operação se destina. O crescimento ainda dobra a capacidade da mesma forma que os módulos Dynamic Array e Stack deste repositório fazem, mas um redimensionamento aqui tem um passo a mais: os elementos ativos não estão necessariamente dispostos contiguamente a partir do índice 0 (um buffer cheio e que deu a volta pode ter sua frente lógica em qualquer lugar), então crescer precisa percorrer o buffer em ordem lógica a partir de head e copiá-lo para um array novo começando no índice 0.

flowchart LR
    subgraph "capacity 8, wrapped around"
        direction LR
        I0["[0] c"] --- I1["[1] d"] --- I2["[2] ·"] --- I3["[3] ·"]
        I3 --- I4["[4] ·"] --- I5["[5] ·"] --- I6["[6] a  ← head"] --- I7["[7] b"]
        I7 -.wraps to.-> I0
    end
Operação Custo Por quê
addFirst / addLast O(1) amortizado escreve em um slot, move um cursor; dobrar a capacidade mantém a frequência de redimensionamento exponencialmente pequena
removeFirst / removeLast O(1) o mesmo — um slot, um cursor, sem deslocamento
peekFirst / peekLast O(1) leitura direta de índice em head ou no índice derivado da tail

Exemplo clássico

classic/ArrayDeque é construído sobre um Object[] bruto usado como buffer circular — sem java.util.ArrayDeque. addFirst, addLast, removeFirst, removeLast, peekFirst e peekLast são todos implementados à mão em torno de um cursor head e de aritmética modular de índices, em vez do deslocamento que o Dynamic Array deste repositório precisa para operações no início. ArrayDequeTest cobre especificamente o crescimento enquanto o buffer está com a volta dada em torno do fim do array subjacente (head longe do índice 0), verificando que a cópia em ordem lógica do redimensionamento não embaralha a ordem dos elementos.

Exemplo aplicado: triagem de tickets de suporte de telecom

applied/SupportTicketQueue modela uma fila de suporte ao cliente: um SupportTicket normal entra no fim da fila via addLast (FIFO), mas um ticket VIP pula direto para o início via addFirst, e um agente sempre retira o próximo ticket a atender via removeFirst. Tanto o enfileiramento normal quanto o fast-track VIP são O(1) — uma escalação nunca precisa deslocar ou reescanear o que já está esperando, ela simplesmente se torna a nova frente. SupportTicketQueueTest cobre a ordem FIFO comum, um ticket VIP furando a fila à frente de tickets normais já esperando, e um segundo VIP furando a fila à frente do primeiro.

Benchmark

./gradlew :linear:queue-deque:jmh

Execução real nesta máquina (JMH 1.37, JDK 26.0.2, 2 iterações de warmup + 3 de medição, 1 fork). Cada chamada medida remove o elemento da frente de uma estrutura recém-populada com exatamente size elementos — a reconstrução é excluída da medição de tempo, só a única chamada de removeFirst/remove(0) conta:

Custo de removeFirst() size=100 size=10,000 size=100,000
deque circular (ArrayDeque.removeFirst) 13.09 ns 12.00 ns 12.83 ns
array comum (DynamicArray.remove(0)) 30.51 ns 1,442.95 ns 18,585.58 ns

O deque circular se mantém estável em ~12–13 ns independentemente do tamanho — a afirmação de O(1), falseável e confirmada. DynamicArray.remove(0), por outro lado, sobe acentuadamente: ~47x mais lento ao ir de size=100 para size=10,000 (um aumento de 100x no tamanho) e ~13x mais lento de novo ao ir de size=10,000 para size=100,000 (um aumento de 10x no tamanho) — ruidoso na ponta pequena, onde o overhead fixo por chamada ainda domina, mas crescendo inconfundivelmente em conjunto com o tamanho, que é exatamente a cara de "deslocar cada elemento restante uma posição para a esquerda" assim que esse overhead deixa de ser o gargalo.

Quando não usar

Cobertura de testes

100% de cobertura de instruções, 100% de cobertura de branches (JaCoCo). Reproduza você mesmo:

./gradlew :linear:queue-deque:jacocoTestReport

Relatório em linear/queue-deque/build/reports/jacoco/test/html/index.html.

Testes unitários

src/test/java/com/datastructures/linear/queuedeque/classic/ArrayDequeTest.java
package com.datastructures.linear.queuedeque.classic;

import org.junit.jupiter.api.Test;

import java.util.NoSuchElementException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class ArrayDequeTest {

    @Test
    void startsEmpty() {
        ArrayDeque<String> deque = new ArrayDeque<>();

        assertThat(deque.isEmpty()).isTrue();
        assertThat(deque.size()).isZero();
    }

    @Test
    void addFirstOnAnEmptyDequeBecomesTheOnlyElement() {
        ArrayDeque<String> deque = new ArrayDeque<>();

        deque.addFirst("a");

        assertThat(deque.isEmpty()).isFalse();
        assertThat(deque.size()).isEqualTo(1);
        assertThat(deque.peekFirst()).isEqualTo("a");
        assertThat(deque.peekLast()).isEqualTo("a");
    }

    @Test
    void addLastOnAnEmptyDequeBecomesTheOnlyElement() {
        ArrayDeque<String> deque = new ArrayDeque<>();

        deque.addLast("a");

        assertThat(deque.size()).isEqualTo(1);
        assertThat(deque.peekFirst()).isEqualTo("a");
        assertThat(deque.peekLast()).isEqualTo("a");
    }

    @Test
    void addLastQueuesElementsInFifoOrder() {
        ArrayDeque<String> deque = new ArrayDeque<>();
        deque.addLast("a");
        deque.addLast("b");
        deque.addLast("c");

        assertThat(deque.removeFirst()).isEqualTo("a");
        assertThat(deque.removeFirst()).isEqualTo("b");
        assertThat(deque.removeFirst()).isEqualTo("c");
        assertThat(deque.isEmpty()).isTrue();
    }

    @Test
    void addFirstPrependsSoTheMostRecentlyAddedIsRemovedFirst() {
        ArrayDeque<String> deque = new ArrayDeque<>();
        deque.addFirst("a");
        deque.addFirst("b");
        deque.addFirst("c");

        assertThat(deque.removeFirst()).isEqualTo("c");
        assertThat(deque.removeFirst()).isEqualTo("b");
        assertThat(deque.removeFirst()).isEqualTo("a");
    }

    @Test
    void removeLastUnlinksFromTheBack() {
        ArrayDeque<String> deque = new ArrayDeque<>();
        deque.addLast("a");
        deque.addLast("b");
        deque.addLast("c");

        assertThat(deque.removeLast()).isEqualTo("c");
        assertThat(deque.removeLast()).isEqualTo("b");
        assertThat(deque.removeLast()).isEqualTo("a");
        assertThat(deque.isEmpty()).isTrue();
    }

    @Test
    void removeFirstOnAnEmptyDequeThrows() {
        ArrayDeque<String> deque = new ArrayDeque<>();

        assertThatThrownBy(deque::removeFirst).isInstanceOf(NoSuchElementException.class);
    }

    @Test
    void removeLastOnAnEmptyDequeThrows() {
        ArrayDeque<String> deque = new ArrayDeque<>();

        assertThatThrownBy(deque::removeLast).isInstanceOf(NoSuchElementException.class);
    }

    @Test
    void peekFirstOnAnEmptyDequeReturnsNull() {
        ArrayDeque<String> deque = new ArrayDeque<>();

        assertThat(deque.peekFirst()).isNull();
    }

    @Test
    void peekLastOnAnEmptyDequeReturnsNull() {
        ArrayDeque<String> deque = new ArrayDeque<>();

        assertThat(deque.peekLast()).isNull();
    }

    @Test
    void peekFirstAndPeekLastDoNotRemoveElements() {
        ArrayDeque<String> deque = new ArrayDeque<>();
        deque.addLast("a");
        deque.addLast("b");

        assertThat(deque.peekFirst()).isEqualTo("a");
        assertThat(deque.peekLast()).isEqualTo("b");
        assertThat(deque.size()).isEqualTo(2);
    }

    @Test
    void growsPastInitialCapacityWhileWrappedAroundAndPreservesLogicalOrder() {
        ArrayDeque<Integer> deque = new ArrayDeque<>();
        int initialCapacity = deque.capacity();

        // Fill to exactly the default capacity via a mix of addLast and addFirst, so the
        // logical front (head) sits away from index 0 by the time the buffer is full and the
        // next add has to trigger a resize while wrapped around the end of the backing array.
        deque.addLast(0);
        deque.addLast(1);
        deque.addLast(2);
        deque.addLast(3);
        deque.addFirst(-1);
        deque.addFirst(-2);
        deque.addFirst(-3);
        deque.addFirst(-4);
        assertThat(deque.size()).isEqualTo(initialCapacity);

        // One more add overflows the full, wrapped-around buffer and forces growIfFull() to
        // copy every element out in logical order into a fresh array.
        deque.addLast(4);

        assertThat(deque.capacity()).isGreaterThan(initialCapacity);
        assertThat(deque.size()).isEqualTo(9);
        assertThat(deque.removeFirst()).isEqualTo(-4);
        assertThat(deque.removeFirst()).isEqualTo(-3);
        assertThat(deque.removeFirst()).isEqualTo(-2);
        assertThat(deque.removeFirst()).isEqualTo(-1);
        assertThat(deque.removeFirst()).isEqualTo(0);
        assertThat(deque.removeFirst()).isEqualTo(1);
        assertThat(deque.removeFirst()).isEqualTo(2);
        assertThat(deque.removeFirst()).isEqualTo(3);
        assertThat(deque.removeFirst()).isEqualTo(4);
        assertThat(deque.isEmpty()).isTrue();
    }

    @Test
    void continuesToWorkCorrectlyAfterMultipleResizes() {
        ArrayDeque<Integer> deque = new ArrayDeque<>();
        for (int i = 0; i < 500; i++) {
            if (i % 2 == 0) {
                deque.addLast(i);
            } else {
                deque.addFirst(-i);
            }
        }

        assertThat(deque.size()).isEqualTo(500);
        int drained = 0;
        while (!deque.isEmpty()) {
            deque.removeFirst();
            drained++;
        }
        assertThat(drained).isEqualTo(500);
    }
}
src/test/java/com/datastructures/linear/queuedeque/applied/SupportTicketQueueTest.java
package com.datastructures.linear.queuedeque.applied;

import org.junit.jupiter.api.Test;

import java.util.NoSuchElementException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class SupportTicketQueueTest {

    @Test
    void startsEmpty() {
        SupportTicketQueue queue = new SupportTicketQueue();

        assertThat(queue.isEmpty()).isTrue();
        assertThat(queue.pendingCount()).isZero();
    }

    @Test
    void normalTicketsAreServedFirstInFirstOut() {
        SupportTicketQueue queue = new SupportTicketQueue();
        queue.submit(new SupportTicket("cust-1", "no signal"));
        queue.submit(new SupportTicket("cust-2", "billing question"));

        assertThat(queue.nextTicket().customerId()).isEqualTo("cust-1");
        assertThat(queue.nextTicket().customerId()).isEqualTo("cust-2");
        assertThat(queue.isEmpty()).isTrue();
    }

    @Test
    void aVipTicketJumpsAheadOfAlreadyWaitingNormalTickets() {
        SupportTicketQueue queue = new SupportTicketQueue();
        queue.submit(new SupportTicket("cust-1", "no signal"));
        queue.submit(new SupportTicket("cust-2", "billing question"));

        queue.submitVip(new SupportTicket("vip-1", "outage escalation"));

        assertThat(queue.nextTicket().customerId()).isEqualTo("vip-1");
        assertThat(queue.nextTicket().customerId()).isEqualTo("cust-1");
        assertThat(queue.nextTicket().customerId()).isEqualTo("cust-2");
    }

    @Test
    void aSecondVipTicketJumpsAheadOfTheFirstVipTicket() {
        SupportTicketQueue queue = new SupportTicketQueue();
        queue.submit(new SupportTicket("cust-1", "no signal"));
        queue.submitVip(new SupportTicket("vip-1", "first escalation"));
        queue.submitVip(new SupportTicket("vip-2", "second escalation"));

        assertThat(queue.nextTicket().customerId()).isEqualTo("vip-2");
        assertThat(queue.nextTicket().customerId()).isEqualTo("vip-1");
        assertThat(queue.nextTicket().customerId()).isEqualTo("cust-1");
    }

    @Test
    void pendingCountTracksSubmittedAndHandledTickets() {
        SupportTicketQueue queue = new SupportTicketQueue();
        queue.submit(new SupportTicket("cust-1", "no signal"));
        queue.submitVip(new SupportTicket("vip-1", "outage"));

        assertThat(queue.pendingCount()).isEqualTo(2);

        queue.nextTicket();

        assertThat(queue.pendingCount()).isEqualTo(1);
    }

    @Test
    void pullingFromAnEmptyQueueThrows() {
        SupportTicketQueue queue = new SupportTicketQueue();

        assertThatThrownBy(queue::nextTicket).isInstanceOf(NoSuchElementException.class);
    }
}

Ver relatório completo de cobertura JaCoCo →