🧩 Verificación de paréntesis balanceados con Pilas

🎯 Objetivo

Determinar si una cadena de texto tiene los paréntesis, corchetes o llaves balanceados.

Ejemplo:

"(1 + 2)"Balanceado

"(1 + 2"No balanceado

💡 Motivación

Este problema aparece todo el tiempo en programación: en compiladores, editores de texto o validadores de sintaxis.

Podemos usar una pila (stack) para resolverlo fácilmente, ya que las pilas nos permiten volver hacia atrás y verificar el orden de apertura y cierre.

📚 Ejemplo visual

graph TD
    A["'('"] --> B["'1'"]
    B --> C["'+'"]
    C --> D["'2'"]
    D --> E["')' ✅"]

    style A fill:#fefae0,stroke:#dda15e;
    style E fill:#d9ed92,stroke:#606c38,stroke-width:2px;

📌 La expresión comienza con un paréntesis que se abre, y termina con un paréntesis que se cierra correctamente → balanceado ✔️

🪄 Simulación paso a paso

Vamos a ver cómo la pila cambia en cada paso al analizar la expresión:

{ [ ( ) ] }

🧩 Paso 1 — Pila vacía

graph BT
    base["⬜ (vacía)"]:::vacia
    classDef vacia fill:#edf2fb,stroke:#4361ee,stroke-width:2px;

📌 Al inicio, la pila está vacía. Comenzamos a leer la expresión carácter por carácter.

🧩 Paso 2 — Leemos { (símbolo de apertura)

graph BT
    A["{"]:::abrir
    base["⬜ (vacía)"]:::vacia
    A --> base
    classDef abrir fill:#fefae0,stroke:#dda15e,stroke-width:2px;
    classDef vacia fill:#edf2fb,stroke:#4361ee,stroke-width:1px;

📥 Vemos una llave de apertura {, así que la apilamos.

🧩 Paso 3 — Leemos [

graph BT
    B["["]:::abrir
    A["{"]:::abrir
    base["⬜"]:::vacia
    B --> A --> base
    classDef abrir fill:#fefae0,stroke:#dda15e,stroke-width:2px;
    classDef vacia fill:#edf2fb,stroke:#4361ee,stroke-width:1px;

📥 Otro símbolo de apertura, lo apilamos sobre la llave anterior. La pila crece hacia arriba ⬆️

🧩 Paso 4 — Leemos (

graph BT
    C["("]:::abrir
    B["["]:::abrir
    A["{"]:::abrir
    base["⬜"]:::vacia
    C --> B --> A --> base
    classDef abrir fill:#fefae0,stroke:#dda15e,stroke-width:2px;
    classDef vacia fill:#edf2fb,stroke:#4361ee,stroke-width:1px;

📥 Otro símbolo de apertura → lo apilamos. Ahora la pila tiene tres símbolos: { [ (

🧩 Paso 5 — Leemos ) (símbolo de cierre)

graph BT
    B["["]:::abrir
    A["{"]:::abrir
    base["⬜"]:::vacia
    B --> A --> base
    classDef abrir fill:#d9ed92,stroke:#606c38,stroke-width:2px;
    classDef vacia fill:#edf2fb,stroke:#4361ee,stroke-width:1px;

📤 Al ver ), quitamos el último símbolo apilado(. Coinciden, así que seguimos ✅

🧩 Paso 6 — Leemos ] (símbolo de cierre)

graph BT
    A["{"]:::abrir
    base["⬜"]:::vacia
    A --> base
    classDef abrir fill:#d9ed92,stroke:#606c38,stroke-width:2px;
    classDef vacia fill:#edf2fb,stroke:#4361ee,stroke-width:1px;

📤 Cerramos el corchete. El último símbolo abierto era [ → coincide ✅

🧩 Paso 7 — Leemos } (último cierre)

graph BT
    base["⬜ (vacía)"]:::vacia
    classDef vacia fill:#d9ed92,stroke:#606c38,stroke-width:2px;

📤 Quitamos la llave { → coincide ✅ La pila ahora está vacía, lo que significa que toda la expresión está balanceada ✔️

🎯 Resultado final

✅ Expresión balanceada:

{ [ ( ) ] }

🧠 Cada vez que se encontró un símbolo de cierre, se eliminó el que estaba en el tope de la pila. Como al final no quedaron símbolos pendientes → está correctamente balanceada.

⚙️ Idea general del algoritmo

  1. Recorremos la cadena carácter por carácter.
  2. Cuando encontramos un símbolo de apertura ((, [, {, <) → lo apilamos.
  3. Cuando encontramos un símbolo de cierre (), ], }, >) → desapilamos el último símbolo y comprobamos si coinciden.
  4. Si en algún momento no coinciden o la pila queda vacía antes de tiempo → ❌ no balanceado.
  1. Si al final la pila está vacía → ✅ balanceado.

🧱 Implementación paso a paso

import java.util.*;

public class VerificadorBalanceo {

    private final List<Character> apertura = Arrays.asList('(', '[', '{', '<');
    private final List<Character> cierre   = Arrays.asList(')', ']', '}', '>');

    public boolean estaBalanceado(String expresion) {
        Stack<Character> pila = new Stack<>();

        for (char c : expresion.toCharArray()) {
            if (apertura.contains(c))
                pila.push(c);

            else if (cierre.contains(c)) {
                if (pila.isEmpty()) return false;
                char tope = pila.pop();
                if (apertura.indexOf(tope) != cierre.indexOf(c))
                    return false;
            }
        }
        return pila.isEmpty();
    }
}

🧱 Implementación paso a paso

import java.util.*;

📦 Importamos las clases necesarias del paquete java.util, porque vamos a usar Stack y List para manejar los símbolos.

🧩 Paso 1 — Definir los símbolos válidos

private final List<Character> apertura = Arrays.asList('(', '[', '{', '<');
private final List<Character> cierre   = Arrays.asList(')', ']', '}', '>');

📋 Creamos dos listas paralelas:

  • apertura: contiene los símbolos que abren.

  • cierre: contiene los símbolos que cierran.

👉 Así podemos comparar sus posiciones: por ejemplo, '(' está en la posición 0 y su par ')' también.

🧩 Paso 2 — Crear la pila

Stack<Character> pila = new Stack<>();

🧱 La pila guardará los símbolos de apertura que vayamos encontrando. Cada vez que abrimos algo, lo apilamos (push); cuando cerramos algo, lo desapilamos (pop).

🧩 Paso 3 — Recorrer la expresión

for (char c : expresion.toCharArray()) {
    if (apertura.contains(c))
        pila.push(c);

🔹 Convertimos la cadena en un arreglo de caracteres.

🔹 Si el carácter c es un símbolo de apertura, lo colocamos encima de la pila.

📥 Ejemplo: Expresión: { [ ( 1 + 2 ) ] } → al leer {, [, ( → se apilan en orden.

🧩 Paso 4 — Detectar cierres

else if (cierre.contains(c)) {
    if (pila.isEmpty()) return false;
    char tope = pila.pop();
    if (apertura.indexOf(tope) != cierre.indexOf(c))
        return false;
}

🔹 Si encontramos un símbolo de cierre, debemos verificar tres cosas:

  1. ❌ Si la pila está vacía → no hay nada que cerrar → falla.

  2. 📤 Quitamos el último símbolo de apertura (pop).

  3. 🔍 Comparamos las posiciones:

    • Si '(' está en la misma posición que ')', coinciden ✅
    • Si no, error ❌ (por ejemplo, [ con )).

🧩 Paso 5 — Resultado final

return pila.isEmpty();

📌 Al final, si la pila queda vacía, significa que todos los símbolos abiertos se cerraron correctamente → ✅ balanceado.

Si aún hay elementos → ❌ no balanceado.

🧩 Probando el método

public class Principal {
    public static void main(String[] args) {
        VerificadorBalanceo verificador = new VerificadorBalanceo();

        System.out.println(verificador.estaBalanceado("(1 + 2)"));   // ✅
        System.out.println(verificador.estaBalanceado("((3 + 2]"));  // ❌
        System.out.println(verificador.estaBalanceado("{[()]}"));    // ✅
        System.out.println(verificador.estaBalanceado("(1 + 2"));    // ❌
    }
}

🖥️ Salida esperada:

true
false
true
false

🧠 Explicación del flujo

graph TD
    A["Expresión: (1 + 2)"]
    A --> B["push('(')"]
    B --> C["ignora '1', '+', '2'"]
    C --> D["pop('(') al ver ')' ✅"]
    D --> E["Pila vacía → Balanceado ✅"]

    classDef good fill:#d9ed92,stroke:#606c38,stroke-width:2px;
    classDef mid fill:#fefae0,stroke:#dda15e;
    class A,B,C,D,E good;

⚠️ Caso no balanceado

graph TD
    A["Expresión: ((3 + 2]"]
    A --> B["push('(')"]
    B --> C["push('(')"]
    C --> D["pop('(') al ver ']' ❌"]
    D --> E["Paréntesis abierto ≠ corchete cerrado → Error"]

    classDef bad fill:#f7cad0,stroke:#e63946,stroke-width:2px;
    class A,B,C,D,E bad;

🧩 Resumen

Situación Acción Resultado
push() cuando hay apertura guarda símbolo pila crece
pop() cuando hay cierre compara símbolos deben coincidir
Pila vacía al final ✅ balanceado
Error de coincidencia ❌ no balanceado

🎯 Conclusión

  • Este es un problema clásico en entrevistas técnicas.
  • Muestra el uso real de las pilas para analizar sintaxis.
  • Se puede extender fácilmente a lenguajes de programación reales.
  • Todas las operaciones (push, pop, isEmpty) tienen complejidad O(1).