← Todas as estruturas

Stack

Linear · ver código-fonte no GitHub

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

Categoria: Linear

O problema

Alguns problemas são naturalmente "desfaça a coisa mais recente primeiro": casar um colchete de fechamento com qualquer colchete de abertura que ainda esteja sem par, retroceder (backtrack) a partir da última decisão tomada, desenrolar chamadas de função aninhadas. Nada disso é acesso indexado ou travessia ordenada — é estritamente last-in-first-out.

A solução

Restrinja o acesso a apenas uma ponta: você só pode olhar, adicionar ou remover no topo. Essa única restrição é o que torna toda operação trivial e O(1) — nunca há dúvida sobre qual elemento tocar, é sempre o que está no topo. Este módulo reaproveita a mesma estratégia de crescimento por duplicação de array do Dynamic Array: push é O(1) amortizado.

flowchart TB
    subgraph Stack
        direction TB
        C["C  ← top"]
        B["B"]
        A["A  ← bottom"]
    end
Operação Custo Por quê
push O(1) amortizado mesmo truque de array por duplicação do Dynamic Array
pop / peek O(1) sempre o último índice, nada para buscar

Exemplo clássico

classic/Stack é baseado em array (sem java.util.Stack/ArrayDeque), expondo apenas push, pop, peek, size, isEmptypop/peek numa stack vazia lançam EmptyStackException, seguindo a própria convenção do JDK para exatamente esse modo de falha. StackTest cobre a ordenação LIFO, os dois casos de falha por stack vazia, e o crescimento além da capacidade inicial.

Exemplo aplicado: validação de colchetes em copybooks COBOL legados

applied/CopybookBracketValidator é o clássico exercício de stack de "colchetes balanceados" do livro-texto, aplicado a um problema real: uma ferramenta construída durante a modernização de mainframe para microsserviços de um banco legado precisa validar que os parênteses em cláusulas PICTURE e expressões COMPUTE estão balanceados antes de um parser automatizado tentar traduzir a linha — uma linha de copybook malformada deve falhar de forma escancarada aqui, não produzir uma tradução silenciosamente errada mais adiante. Todo colchete de abertura é empilhado (push); todo colchete de fechamento precisa casar com o que está no topo, e a stack precisa estar vazia novamente ao final da linha. CopybookBracketValidatorTest cobre linhas balanceadas, um colchete de fechamento inesperado, um tipo de colchete incompatível, e um colchete não fechado ao final da linha.

Benchmark

./gradlew :linear:stack:jmh

Execução real (JMH 1.37, JDK 26.0.2, 2 iterações de warmup + 3 de medição, 1 fork):

Benchmark size=100 size=10,000 size=1,000,000
push (total para N pushes) 619 ns 71,850 ns 35.5 ms
peek 2.39 ns 1.79 ns 2.36 ns

peek se mantém estável independentemente do tamanho — O(1), confirmado. O custo total de push escala com o tamanho da mesma forma O(1)-amortizado-por-elemento que o append do Dynamic Array, já que por baixo dos panos é a mesma estratégia de crescimento.

Quando não usar

Cobertura de testes

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

./gradlew :linear:stack:jacocoTestReport

Relatório em linear/stack/build/reports/jacoco/test/html/index.html.

Testes unitários

src/test/java/com/datastructures/linear/stack/classic/StackTest.java
package com.datastructures.linear.stack.classic;

import org.junit.jupiter.api.Test;

import java.util.EmptyStackException;

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

class StackTest {

    @Test
    void startsEmpty() {
        Stack<String> stack = new Stack<>();

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

    @Test
    void pushMakesTheStackNonEmptyAndPeekableWithoutRemoving() {
        Stack<String> stack = new Stack<>();

        stack.push("a");

        assertThat(stack.isEmpty()).isFalse();
        assertThat(stack.peek()).isEqualTo("a");
        assertThat(stack.size()).isEqualTo(1);
    }

    @Test
    void popReturnsElementsInLastInFirstOutOrder() {
        Stack<String> stack = new Stack<>();
        stack.push("a");
        stack.push("b");
        stack.push("c");

        assertThat(stack.pop()).isEqualTo("c");
        assertThat(stack.pop()).isEqualTo("b");
        assertThat(stack.pop()).isEqualTo("a");
        assertThat(stack.isEmpty()).isTrue();
    }

    @Test
    void popOnAnEmptyStackThrows() {
        Stack<String> stack = new Stack<>();

        assertThatThrownBy(stack::pop).isInstanceOf(EmptyStackException.class);
    }

    @Test
    void peekOnAnEmptyStackThrows() {
        Stack<String> stack = new Stack<>();

        assertThatThrownBy(stack::peek).isInstanceOf(EmptyStackException.class);
    }

    @Test
    void growsPastInitialCapacityWithoutLosingOrder() {
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < 100; i++) {
            stack.push(i);
        }

        assertThat(stack.size()).isEqualTo(100);
        for (int i = 99; i >= 0; i--) {
            assertThat(stack.pop()).isEqualTo(i);
        }
    }
}
src/test/java/com/datastructures/linear/stack/applied/CopybookBracketValidatorTest.java
package com.datastructures.linear.stack.applied;

import org.junit.jupiter.api.Test;

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

class CopybookBracketValidatorTest {

    private final CopybookBracketValidator validator = new CopybookBracketValidator();

    @Test
    void anEmptyLineIsTriviallyBalanced() {
        assertThat(validator.validate("")).isEqualTo(BracketValidationResult.ok());
    }

    @Test
    void aLineWithNoBracketsIsBalanced() {
        assertThat(validator.validate("MOVE A TO B").valid()).isTrue();
    }

    @Test
    void picClauseParenthesesAreBalanced() {
        BracketValidationResult result = validator.validate("05 CUSTOMER-NAME PIC X(30).");

        assertThat(result.valid()).isTrue();
    }

    @Test
    void nestedMixedBracketTypesAreBalanced() {
        BracketValidationResult result = validator.validate("COMPUTE X = ([A + B] * {C - D})");

        assertThat(result.valid()).isTrue();
    }

    @Test
    void aClosingBracketWithNothingOpenIsRejected() {
        BracketValidationResult result = validator.validate("MOVE A) TO B");

        assertThat(result.valid()).isFalse();
        assertThat(result.message()).contains("unexpected").contains(")");
    }

    @Test
    void mismatchedBracketTypesAreRejected() {
        BracketValidationResult result = validator.validate("COMPUTE X = (A + B]");

        assertThat(result.valid()).isFalse();
        assertThat(result.message()).contains("expected").contains("]");
    }

    @Test
    void anUnclosedBracketAtEndOfLineIsRejected() {
        BracketValidationResult result = validator.validate("PIC X(30");

        assertThat(result.valid()).isFalse();
        assertThat(result.message()).contains("unclosed").contains("(");
    }

    @Test
    void anUnclosedBraceIsAlsoRejected() {
        BracketValidationResult result = validator.validate("COMPUTE X = {A + B");

        assertThat(result.valid()).isFalse();
        assertThat(result.message()).contains("unclosed");
    }
}

Ver relatório completo de cobertura JaCoCo →